diff options
Diffstat (limited to 'alot/buffers/thread.py')
-rw-r--r-- | alot/buffers/thread.py | 334 |
1 files changed, 334 insertions, 0 deletions
diff --git a/alot/buffers/thread.py b/alot/buffers/thread.py new file mode 100644 index 00000000..c89d4688 --- /dev/null +++ b/alot/buffers/thread.py @@ -0,0 +1,334 @@ +# Copyright (C) 2011-2018 Patrick Totzke <patricktotzke@gmail.com> +# This file is released under the GNU GPL, version 3 or a later revision. +# For further details see the COPYING file +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 .. import commands + + + +class ThreadBuffer(Buffer): + """displays a thread as a tree of messages""" + + modename = 'thread' + + def __init__(self, ui, thread): + """ + :param ui: main UI + :type ui: :class:`~alot.ui.UI` + :param thread: thread to display + :type thread: :class:`~alot.db.Thread` + """ + self.thread = thread + self.message_count = thread.get_total_messages() + + # two semaphores for auto-removal of unread tag + self._auto_unread_dont_touch_mids = set([]) + self._auto_unread_writing = False + + self._indent_width = settings.get('thread_indent_replies') + self.rebuild() + Buffer.__init__(self, ui, self.body) + + def __str__(self): + return '[thread] %s (%d message%s)' % (self.thread.get_subject(), + self.message_count, + 's' * (self.message_count > 1)) + + def get_info(self): + info = {} + info['subject'] = self.thread.get_subject() + info['authors'] = self.thread.get_authors_string() + info['tid'] = self.thread.get_thread_id() + info['message_count'] = self.message_count + return info + + def get_selected_thread(self): + """returns the displayed :class:`~alot.db.Thread`""" + return self.thread + + def rebuild(self): + try: + self.thread.refresh() + except NonexistantObjectError: + self.body = urwid.SolidFill() + self.message_count = 0 + return + + self._tree = ThreadTree(self.thread) + + # 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 = u'\u27a4' + 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.get_total_messages() + + def render(self, size, focus=False): + if self.message_count == 0: + return self.body.render(size, focus) + + if settings.get('auto_remove_unread'): + logging.debug('Tbuffer: auto remove unread tag from msg?') + msg = self.get_selected_message() + mid = msg.get_message_id() + focus_pos = self.body.get_focus()[1] + summary_pos = (self.body.get_focus()[1][0], (0,)) + cursor_on_non_summary = (focus_pos != summary_pos) + if cursor_on_non_summary: + if mid not in self._auto_unread_dont_touch_mids: + if 'unread' in msg.get_tags(): + logging.debug('Tbuffer: removing unread') + + def clear(): + self._auto_unread_writing = False + + self._auto_unread_dont_touch_mids.add(mid) + self._auto_unread_writing = True + msg.remove_tags(['unread'], afterwards=clear) + fcmd = commands.globals.FlushCommand(silent=True) + self.ui.apply_command(fcmd) + else: + logging.debug('Tbuffer: No, msg not unread') + else: + logging.debug('Tbuffer: No, mid locked for autorm-unread') + else: + if not self._auto_unread_writing and \ + mid in self._auto_unread_dont_touch_mids: + self._auto_unread_dont_touch_mids.remove(mid) + logging.debug('Tbuffer: No, cursor on summary') + return self.body.render(size, focus) + + def get_selected_mid(self): + """returns Message ID of focussed message""" + return self.body.get_focus()[1][0] + + def get_selected_message_position(self): + """returns position of focussed message in the thread tree""" + return self._sanitize_position((self.get_selected_mid(),)) + + def get_selected_messagetree(self): + """returns currently focussed :class:`MessageTree`""" + return self._nested_tree[self.body.get_focus()[1][:1]] + + def get_selected_message(self): + """returns focussed :class:`~alot.db.message.Message`""" + return self.get_selected_messagetree()._message + + def get_messagetree_positions(self): + """ + returns 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()] + + def messagetrees(self): + """ + returns a Generator of all :class:`MessageTree` in the + :class:`ThreadTree` of this buffer. + """ + for pos in self._tree.positions(): + yield self._tree[pos] + + def refresh(self): + """refresh and flushe caches of Thread tree""" + self.body.refresh() + + # needed for ui.get_deep_focus.. + def get_focus(self): + "Get the focus from the underlying body widget." + return self.body.get_focus() + + def set_focus(self, pos): + "Set the focus in the underlying body widget." + logging.debug('setting focus to %s ', pos) + self.body.set_focus(pos) + + def focus_first(self): + """set focus to first message of thread""" + self.body.set_focus(self._nested_tree.root) + + 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) + + def focus_selected_message(self): + """focus the summary line of currently focussed message""" + # move focus to summary (root of current MessageTree) + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + 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) + + 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) + + 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) + + 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) + + 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]] + + def expand_all(self): + """expand all messages in thread""" + for MT in self.messagetrees(): + MT.expand(MT.root) + + def collapse(self, msgpos): + """collapse message at given position""" + MT = self._tree[msgpos] + MT.collapse(MT.root) + self.focus_selected_message() + + def collapse_all(self): + """collapse all messages in thread""" + for MT in self.messagetrees(): + MT.collapse(MT.root) + self.focus_selected_message() + + def unfold_matching(self, querystring, focus_first=True): + """ + expand all messages that match a given querystring. + + :param querystring: query to match + :type querystring: str + :param focus_first: set the focus to the first matching message + :type focus_first: bool + """ + first = None + for MT in self.messagetrees(): + msg = MT._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) + else: + MT.collapse(MT.root) + self.body.refresh() |