From f54719ab63d9b48f7ab2db40ec151a49f3c3f050 Mon Sep 17 00:00:00 2001 From: wmorgan Date: Fri, 12 Jan 2007 18:43:26 +0000 Subject: [PATCH] - redid thread-view layout internals to be nicer - jump_prev_open and next_open now only shift left/right if they need to (based on message width and indentation level) git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@244 5c8cc53c-5e98-4d25-b20a-d8db53a31250 --- lib/sup/message.rb | 11 +-- lib/sup/modes/scroll-mode.rb | 13 ++- lib/sup/modes/thread-view-mode.rb | 139 ++++++++++++++++++------------ 3 files changed, 103 insertions(+), 60 deletions(-) diff --git a/lib/sup/message.rb b/lib/sup/message.rb index facbc31..27fbfa9 100644 --- a/lib/sup/message.rb +++ b/lib/sup/message.rb @@ -17,6 +17,7 @@ class MessageFormatError < StandardError; end ## appropriately. class Message SNIPPET_LEN = 80 + WRAP_LEN = 80 # wrap at this width RE_PATTERN = /^((re|re[\[\(]\d[\]\)]):\s*)+/i ## some utility methods @@ -54,7 +55,7 @@ class Message attr_reader :lines def initialize lines ## do some wrapping - @lines = lines.map { |l| l.chomp.wrap 80 }.flatten + @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten end end @@ -136,7 +137,7 @@ class Message private :read_header def broken?; @source.broken?; end - def snippet; @snippet || to_chunks && @snippet; end + def snippet; @snippet || chunks && @snippet; end def is_list_message?; !@list_address.nil?; end def is_draft?; DraftLoader === @source; end def draft_filename @@ -172,7 +173,7 @@ class Message end ## this is called when the message body needs to actually be loaded. - def to_chunks + def chunks @chunks ||= if @source.broken? [Text.new(error_message(@source.broken_msg.split("\n")))] @@ -228,13 +229,13 @@ EOS to.map { |p| "#{p.name} #{p.email}" }, cc.map { |p| "#{p.name} #{p.email}" }, bcc.map { |p| "#{p.name} #{p.email}" }, - to_chunks.select { |c| c.is_a? Text }.map { |c| c.lines }, + chunks.select { |c| c.is_a? Text }.map { |c| c.lines }, Message.normalize_subj(subj), ].flatten.compact.join " " end def basic_body_lines - to_chunks.find_all { |c| c.is_a?(Text) || c.is_a?(Quote) }.map { |c| c.lines }.flatten + chunks.find_all { |c| c.is_a?(Text) || c.is_a?(Quote) }.map { |c| c.lines }.flatten end def basic_header_lines diff --git a/lib/sup/modes/scroll-mode.rb b/lib/sup/modes/scroll-mode.rb index 459c780..31ccef0 100644 --- a/lib/sup/modes/scroll-mode.rb +++ b/lib/sup/modes/scroll-mode.rb @@ -1,7 +1,16 @@ module Redwood class ScrollMode < Mode - attr_reader :status, :topline, :botline + ## we define topline and botline as the top and bottom lines of any + ## content in the currentview. + + ## we left leftcol and rightcol as the left and right columns of any + ## content in the current view. but since we're operating in a + ## line-centric fashion, rightcol is always leftcol + the buffer + ## width. (whereas botline is topline + at most the buffer height, + ## and can be == to topline in the case that there's no content.) + + attr_reader :status, :topline, :botline, :leftcol COL_JUMP = 2 @@ -25,6 +34,8 @@ class ScrollMode < Mode super() end + def rightcol; @leftcol + buffer.content_width; end + def draw ensure_mode_validity (@topline ... @botline).each { |ln| draw_line ln } diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb index da1bc7b..268892f 100644 --- a/lib/sup/modes/thread-view-mode.rb +++ b/lib/sup/modes/thread-view-mode.rb @@ -1,7 +1,13 @@ module Redwood class ThreadViewMode < LineCursorMode + ## this holds all info we need to lay out a message + class Layout + attr_accessor :top, :bot, :prev, :next, :depth, :width, :state + end + DATE_FORMAT = "%B %e %Y %l:%M%P" + INDENT_SPACES = 2 # how many spaces to indent child messages register_keymap do |k| k.add :toggle_detailed_header, "Toggle detailed header", 'd' @@ -19,31 +25,35 @@ class ThreadViewMode < LineCursorMode k.add :save_to_disk, "Save message/attachment to disk", 's' end + ## there are three important instance variables we hold to lay out + ## the thread. @layout is a map from Message and Chunk objects to + ## Layout objects. (for chunks, we only use the state field right + ## now.) @message_lines is a map from row #s to Message objects. + ## @chunk_lines is a map from row #s to Chunk objects. + def initialize thread, hidden_labels=[] super() @thread = thread - @state = {} @hidden_labels = hidden_labels + @layout = {} earliest, latest = nil, nil latest_date = nil @thread.each do |m, d, p| next unless m earliest ||= m - @state[m] = initial_state_for m + @layout[m] = Layout.new + @layout[m].state = initial_state_for m if latest_date.nil? || m.date > latest_date latest_date = m.date latest = m end end - @state[latest] = :open if @state[latest] == :closed - @state[earliest] = :detailed if earliest.has_label?(:unread) || @thread.size == 1 + @layout[latest].state = :open if @layout[latest].state == :closed + @layout[earliest].state = :detailed if earliest.has_label?(:unread) || @thread.size == 1 - BufferManager.say "Loading message bodies..." do - regen_chunks - regen_text - end + BufferManager.say("Loading message bodies...") { regen_text } end def draw_line ln, opts={} @@ -99,8 +109,9 @@ class ThreadViewMode < LineCursorMode return unless(chunk = @chunk_lines[curpos]) case chunk when Message, Message::Quote, Message::Signature - @state[chunk] = (@state[chunk] != :closed ? :closed : :open) - cursor_down if @state[chunk] == :closed + l = @layout[chunk] + l.state = (l.state != :closed ? :closed : :open) + cursor_down if l.state == :closed when Message::Attachment view_attachment chunk end @@ -133,8 +144,8 @@ class ThreadViewMode < LineCursorMode def jump_to_next_open return unless(m = @message_lines[curpos]) - while nextm = @messages[m][3] - break if @state[nextm] == :open + while nextm = @layout[m].next + break if @layout[nextm].state == :open m = nextm end jump_to_message nextm if nextm @@ -144,10 +155,11 @@ class ThreadViewMode < LineCursorMode return unless(m = @message_lines[curpos]) ## jump to the top of the current message if we're in the body; ## otherwise, to the previous message - top = @messages[m][0] + + top = @layout[m].top if curpos == top - while(prevm = @messages[m][2]) - break if @state[prevm] != :closed + while(prevm = @layout[m].prev) + break if @layout[prevm].state != :closed m = prevm end jump_to_message prevm if prevm @@ -157,32 +169,38 @@ class ThreadViewMode < LineCursorMode end def jump_to_message m - top, bot, prevm, nextm, depth = @messages[m] - jump_to_line top unless top >= topline && - top <= botline && bot >= topline && bot <= botline - jump_to_col depth * 2 # sorry!!!! TODO: make this a constant - set_cursor_pos top + l = @layout[m] + left = l.depth * INDENT_SPACES + right = left + l.width + + ## jump to the top line unless both top and bottom fit in the current view + jump_to_line l.top unless l.top >= topline && l.top <= botline && l.bot >= topline && l.bot <= botline + + ## jump to the left columns unless both left and right fit in the current view + jump_to_col left unless left >= leftcol && left <= rightcol && right >= leftcol && right <= rightcol + + ## either way, move the cursor to the first line + set_cursor_pos l.top end def expand_all_messages @global_message_state ||= :closed @global_message_state = (@global_message_state == :closed ? :open : :closed) - @state.each { |m, v| @state[m] = @global_message_state if m.is_a? Message } + @layout.each { |m, l| l.state = @global_message_state if m.is_a? Message } update end - def collapse_non_new_messages - @messages.each { |m, v| @state[m] = m.has_label?(:unread) ? :open : :closed } + @layout.each { |m, l| l.state = m.has_label?(:unread) ? :open : :closed } update end def expand_all_quotes if(m = @message_lines[curpos]) - quotes = @chunks[m].select { |c| c.is_a?(Message::Quote) || c.is_a?(Message::Signature) } - open, closed = quotes.partition { |c| @state[c] == :open } + quotes = m.chunks.select { |c| c.is_a?(Message::Quote) || c.is_a?(Message::Signature) } + open, closed = quotes.partition { |c| @layout[c].state == :open } newstate = open.length > closed.length ? :closed : :open - quotes.each { |c| @state[c] = newstate } + quotes.each { |c| @layout[c].state = newstate } update end end @@ -197,7 +215,7 @@ class ThreadViewMode < LineCursorMode end end end - @messages = @chunks = @text = nil + @layout = @text = nil end private @@ -215,18 +233,15 @@ private buffer.mark_dirty if buffer end - def regen_chunks - @chunks = {} - @thread.each { |m, d, p| @chunks[m] = m.to_chunks if m.is_a?(Message) } - end - + ## here we generate the actual content lines. we accumulate + ## everything into @text, and we set @chunk_lines and + ## @message_lines, and we update @layout. def regen_text @text = [] @chunk_lines = [] @message_lines = [] - @messages = {} - prev_m = nil + prevm = nil @thread.each do |m, depth, parent| ## we're occasionally called on @threads that have had messages ## added to them since initialization. luckily we regen_text on @@ -234,34 +249,50 @@ private ## scrolling (basically), so we can just slap this on here. ## ## to pick nits, the niceness that i do in the constructor with - ## 'latest' might not be valid, but i don't see that as a huge - ## issue. - @state[m] ||= initial_state_for m if m + ## 'latest' etc. (for automatically opening just the latest + ## message if everything's been read) will not be valid, but + ## that's just a nicety and hopefully this won't happen too + ## often. + l = (@layout[m] ||= Layout.new) + l.state ||= initial_state_for m + + ## build the patina + text = chunk_to_lines m, l.state, @text.length, depth, parent + + l.top = @text.length + l.bot = @text.length + text.length # updated below + l.prev = prevm + l.next = nil + l.depth = depth + # l.state we preserve + l.width = 0 # updated below + @layout[l.prev].next = m if l.prev - text = chunk_to_lines m, @state[m], @text.length, depth, parent (0 ... text.length).each do |i| @chunk_lines[@text.length + i] = m @message_lines[@text.length + i] = m + lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.length }.sum end - ## sorry i store all this shit in an array. very, very sorry. - ## also sorry about the * 2. very, very sorry. - @messages[m] = [@text.length, @text.length + text.length, prev_m, nil, depth] - @messages[prev_m][3] = m if prev_m - prev_m = m if m.is_a? Message - @text += text - if @state[m] != :closed && @chunks.member?(m) - @chunks[m].each do |c| - @state[c] ||= :closed - text = chunk_to_lines c, @state[c], @text.length, depth - (0 ... text.length).each do |i| - @chunk_lines[@text.length + i] = c - @message_lines[@text.length + i] = m + + if m.is_a? Message + prevm = m + if @layout[m].state != :closed + m.chunks.each do |c| + cl = (@layout[c] ||= Layout.new) + cl.state ||= :closed + text = chunk_to_lines c, cl.state, @text.length, depth + (0 ... text.length).each do |i| + @chunk_lines[@text.length + i] = c + @message_lines[@text.length + i] = m + lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.length }.sum - (depth * INDENT_SPACES) + l.width = lw if lw > l.width + end + @text += text end - @text += text + @layout[m].bot = @text.length end - @messages[m][1] = @text.length end end end @@ -317,7 +348,7 @@ private def chunk_to_lines chunk, state, start, depth, parent=nil - prefix = " " * depth + prefix = " " * INDENT_SPACES * depth case chunk when :fake_root [[[:message_patina_color, "#{prefix}"]]] -- 2.45.2