From d25d788bdcf91f4066ae8e80ef7aebe85213d4d3 Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Sat, 8 Feb 2020 13:56:56 +0100 Subject: thread: drop the use of urwidtrees Their API is misdesigned - forces the use of trees for nontree objects and mixes data relationships with display properties. The result is a mess that is hard to understand/maintain/extend. Replace the use of urwidtrees with urwid Pile and ListBox. This temporarily removes tree-style indentation and decorations for thread buffers. That will be reimplemented in following commits. --- alot/buffers/thread.py | 243 ++++++++++++++++++------------------------------- 1 file changed, 87 insertions(+), 156 deletions(-) (limited to 'alot/buffers') diff --git a/alot/buffers/thread.py b/alot/buffers/thread.py index 6434bb04..bc467ded 100644 --- a/alot/buffers/thread.py +++ b/alot/buffers/thread.py @@ -5,11 +5,10 @@ import asyncio import urwid import logging -from urwidtrees import ArrowTree, TreeBox, NestedTree from .buffer import Buffer from ..settings.const import settings -from ..widgets.thread import ThreadTree +from ..widgets.thread import MessageWidget from .. import commands from ..db.errors import NonexistantObjectError @@ -27,16 +26,15 @@ class ThreadBuffer(Buffer): :type thread: :class:`~alot.db.Thread` """ self.thread = thread - self.message_count = thread.total_messages self._indent_width = settings.get('thread_indent_replies') self.rebuild() - Buffer.__init__(self, ui, self.body) + super().__init__(ui, self.body) def __str__(self): return '[thread] %s (%d message%s)' % (self.thread.subject, - self.message_count, - 's' * (self.message_count > 1)) + self.thread.total_messages, + 's' * (self.thread.total_messages > 1)) def translated_tags_str(self, intersection=False): tags = self.thread.get_tags(intersection=intersection) @@ -49,7 +47,7 @@ class ThreadBuffer(Buffer): info['subject'] = self.thread.subject info['authors'] = self.thread.get_authors_string() info['tid'] = self.thread.id - info['message_count'] = self.message_count + info['message_count'] = self.thread.total_messages info['thread_tags'] = self.translated_tags_str() info['intersection_tags'] = self.translated_tags_str(intersection=True) return info @@ -59,78 +57,37 @@ class ThreadBuffer(Buffer): self.thread.refresh() except NonexistantObjectError: self.body = urwid.SolidFill() - self.message_count = 0 return - self._tree = ThreadTree(self.thread) + self._msg_walker = urwid.SimpleFocusListWalker( + [MessageWidget(msg, idx & 1) for (idx, msg) in + enumerate(self.thread.message_list)]) - # define A to be the tree to be wrapped by a NestedTree and displayed. - # We wrap the thread tree into an ArrowTree for decoration if - # indentation was requested and otherwise use it as is. - if self._indent_width == 0: - A = self._tree - else: - # we want decoration. - bars_att = settings.get_theming_attribute('thread', 'arrow_bars') - # only add arrow heads if there is space (indent > 1). - heads_char = None - heads_att = None - if self._indent_width > 1: - heads_char = '➤' - heads_att = settings.get_theming_attribute('thread', - 'arrow_heads') - A = ArrowTree( - self._tree, - indent=self._indent_width, - childbar_offset=0, - arrow_tip_att=heads_att, - arrow_tip_char=heads_char, - arrow_att=bars_att) - - self._nested_tree = NestedTree(A, interpret_covered=True) - self.body = TreeBox(self._nested_tree) - - self.message_count = self.thread.total_messages - - def get_selected_mid(self): - """Return Message ID of focussed message.""" - return self.body.get_focus()[1][0] + self.body = urwid.ListBox(self._msg_walker) def get_selected_message_position(self): """Return position of focussed message in the thread tree.""" - return self._sanitize_position((self.get_selected_mid(),)) + return self.body.focus_position - def get_selected_messagetree(self): - """Return currently focussed :class:`MessageTree`.""" - return self._nested_tree[self.body.get_focus()[1][:1]] + def get_selected_message_widget(self): + """Return currently focused :class:`MessageWidget`.""" + return self.body.focus def get_selected_message(self): """Return focussed :class:`~alot.db.message.Message`.""" - return self.get_selected_messagetree()._message - - def get_messagetree_positions(self): - """ - Return a Generator to walk through all positions of - :class:`MessageTree` in the :class:`ThreadTree` of this buffer. - """ - return [(pos,) for pos in self._tree.positions()] + return self.get_selected_message_widget().get_message() - def messagetrees(self): + def message_widgets(self): """ - returns a Generator of all :class:`MessageTree` in the - :class:`ThreadTree` of this buffer. + Iterate over all the message widgets in this buffer """ - for pos in self._tree.positions(): - yield self._tree[pos] - - def refresh(self): - """Refresh and flush caches of Thread tree.""" - self.body.refresh() + for w in self._msg_walker: + yield w # needed for ui.get_deep_focus.. def get_focus(self): "Get the focus from the underlying body widget." - return self.body.get_focus() + return self.body.focus def set_focus(self, pos): "Set the focus in the underlying body widget." @@ -139,145 +96,119 @@ class ThreadBuffer(Buffer): def focus_first(self): """set focus to first message of thread""" - self.body.set_focus(self._nested_tree.root) + self.set_focus(0) def focus_last(self): - self.body.set_focus(next(self._nested_tree.positions(reverse=True))) - - def _sanitize_position(self, pos): - return self._nested_tree._sanitize_position(pos, - self._nested_tree._tree) + self.set_focus(max(self.thread.total_messages - 1), 0) def focus_selected_message(self): - """focus the summary line of currently focussed message""" - # move focus to summary (root of current MessageTree) + """focus the summary line of currently focused message""" self.set_focus(self.get_selected_message_position()) def focus_parent(self): - """move focus to parent of currently focussed message""" - mid = self.get_selected_mid() - newpos = self._tree.parent_position(mid) - if newpos is not None: - newpos = self._sanitize_position((newpos,)) - self.body.set_focus(newpos) + """move focus to parent of currently focused message""" + pos = self.get_selected_message_position() + cur_depth = self.get_selected_message().depth + + for idx in reversed(range(0, pos)): + msg = self.thread.message_list[idx] + if msg.depth < cur_depth: + self.set_focus(idx) + break def focus_first_reply(self): - """move focus to first reply to currently focussed message""" - mid = self.get_selected_mid() - newpos = self._tree.first_child_position(mid) - if newpos is not None: - newpos = self._sanitize_position((newpos,)) - self.body.set_focus(newpos) + """move focus to first reply to currently focused message""" + msg = self.get_selected_message() + if len(msg.replies) > 0: + new_focus = self.thread.message_list.index(msg.replies[0]) + self.set_focus(new_focus) def focus_last_reply(self): - """move focus to last reply to currently focussed message""" - mid = self.get_selected_mid() - newpos = self._tree.last_child_position(mid) - if newpos is not None: - newpos = self._sanitize_position((newpos,)) - self.body.set_focus(newpos) + """move focus to last reply to currently focused message""" + msg = self.get_selected_message() + if len(msg.replies) > 0: + new_focus = self.thread.message_list.index(msg.replies[-1]) + self.set_focus(new_focus) def focus_next_sibling(self): """focus next sibling of currently focussed message in thread tree""" - mid = self.get_selected_mid() - newpos = self._tree.next_sibling_position(mid) - if newpos is not None: - newpos = self._sanitize_position((newpos,)) - self.body.set_focus(newpos) + pos_next = self.get_selected_message_position() + 1 + depth = self.get_selected_message().depth + if (pos_next < self.thread.total_messages and + self.thread.message_list[pos_next].depth == depth): + self.set_focus(pos_next) def focus_prev_sibling(self): """ focus previous sibling of currently focussed message in thread tree """ - mid = self.get_selected_mid() - localroot = self._sanitize_position((mid,)) - if localroot == self.get_focus()[1]: - newpos = self._tree.prev_sibling_position(mid) - if newpos is not None: - newpos = self._sanitize_position((newpos,)) - else: - newpos = localroot - if newpos is not None: - self.body.set_focus(newpos) + pos_next = self.get_selected_message_position() - 1 + depth = self.get_selected_message().depth + if (pos_next < self.thread.total_messages and + self.thread.message_list[pos_next].depth == depth): + self.set_focus(pos_next) def focus_next(self): """focus next message in depth first order""" - mid = self.get_selected_mid() - newpos = self._tree.next_position(mid) - if newpos is not None: - newpos = self._sanitize_position((newpos,)) - self.body.set_focus(newpos) + next_focus = self.get_selected_message_position() + 1 + if next_focus >= 0 and next_focus < self.thread.total_messages: + self.set_focus(next_focus) def focus_prev(self): """focus previous message in depth first order""" - mid = self.get_selected_mid() - localroot = self._sanitize_position((mid,)) - if localroot == self.get_focus()[1]: - newpos = self._tree.prev_position(mid) - if newpos is not None: - newpos = self._sanitize_position((newpos,)) - else: - newpos = localroot - if newpos is not None: - self.body.set_focus(newpos) + next_focus = self.get_selected_message_position() - 1 + if next_focus >= 0 and next_focus < self.thread.total_messages: + self.set_focus(next_focus) - def focus_property(self, prop, direction): + def _focus_property(self, prop, direction): """does a walk in the given direction and focuses the - first message tree that matches the given property""" - newpos = self.get_selected_mid() - newpos = direction(newpos) - while newpos is not None: - MT = self._tree[newpos] - if prop(MT): - newpos = self._sanitize_position((newpos,)) - self.body.set_focus(newpos) + first message that matches the given property""" + cur_pos = self.get_selected_message_position() + + if direction > 0: + walk = range(cur_pos + 1, self.thread.total_messages) + else: + walk = reversed(range(0, cur_pos)) + + for pos in walk: + if prop(self._msg_walker[pos]): + self.set_focus(pos) break - newpos = direction(newpos) def focus_next_matching(self, querystring): """focus next matching message in depth first order""" - self.focus_property(lambda x: x._message.matches(querystring), - self._tree.next_position) + self._focus_property(lambda x: x.get_message().matches(querystring), 1) def focus_prev_matching(self, querystring): """focus previous matching message in depth first order""" - self.focus_property(lambda x: x._message.matches(querystring), - self._tree.prev_position) + self._focus_property(lambda x: x.get_message().matches(querystring), -1) def focus_next_unfolded(self): """focus next unfolded message in depth first order""" - self.focus_property(lambda x: not x.is_collapsed(x.root), - self._tree.next_position) + self._focus_property(lambda x: x.display_content, 1) def focus_prev_unfolded(self): """focus previous unfolded message in depth first order""" - self.focus_property(lambda x: not x.is_collapsed(x.root), - self._tree.prev_position) + self._focus_property(lambda x: x.display_content, -1) def expand(self, msgpos): """expand message at given position""" - MT = self._tree[msgpos] - MT.expand(MT.root) - - def messagetree_at_position(self, pos): - """get :class:`MessageTree` for given position""" - return self._tree[pos[0]] + self.body.body[msgpos].expand() def expand_all(self): """expand all messages in thread""" - for MT in self.messagetrees(): - MT.expand(MT.root) + for msg in self.message_widgets(): + msg.expand() def collapse(self, msgpos): """collapse message at given position""" - MT = self._tree[msgpos] - MT.collapse(MT.root) + self.body.body[msgpos].collapse() self.focus_selected_message() def collapse_all(self): """collapse all messages in thread""" - for MT in self.messagetrees(): - MT.collapse(MT.root) + for msg in self.message_widgets(): + msg.collapse() self.focus_selected_message() def unfold_matching(self, querystring, focus_first=True): @@ -289,14 +220,14 @@ class ThreadBuffer(Buffer): :param focus_first: set the focus to the first matching message :type focus_first: bool """ + focus_set = False first = None - for MT in self.messagetrees(): - msg = MT._message + for pos, msg_wgt in enumerate(self.message_widgets()): + msg = msg_wgt.get_message() if msg.matches(querystring): - MT.expand(MT.root) - if first is None: - first = (self._tree.position_of_messagetree(MT), MT.root) - self.body.set_focus(first) + msg_wgt.expand() + if focus_first and not focus_set: + self.set_focus(pos) + focus_set = True else: - MT.collapse(MT.root) - self.body.refresh() + msg_wgt.collapse() -- cgit v1.2.3