summaryrefslogtreecommitdiff
path: root/alot/buffers
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2018-06-19 10:40:07 +0100
committerPatrick Totzke <patricktotzke@gmail.com>2018-06-19 22:08:36 +0100
commitf0105c37556116c07f9f30c1fe960ea9e67e7229 (patch)
tree8642317be8306ea7c18ad62ef9146f507df95f9f /alot/buffers
parentd19615d2ac5bb23aca9068ca8c7644f466ce0b48 (diff)
refactor buffers
This splits buffers.py, which contained all buffer classes, into several smaller files. issue #1226
Diffstat (limited to 'alot/buffers')
-rw-r--r--alot/buffers/__init__.py10
-rw-r--r--alot/buffers/buffer.py40
-rw-r--r--alot/buffers/bufferlist.py66
-rw-r--r--alot/buffers/envelope.py98
-rw-r--r--alot/buffers/search.py125
-rw-r--r--alot/buffers/taglist.py71
-rw-r--r--alot/buffers/thread.py335
7 files changed, 745 insertions, 0 deletions
diff --git a/alot/buffers/__init__.py b/alot/buffers/__init__.py
new file mode 100644
index 00000000..2b52a544
--- /dev/null
+++ b/alot/buffers/__init__.py
@@ -0,0 +1,10 @@
+# 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
+
+from .buffer import Buffer
+from .bufferlist import BufferlistBuffer
+from .envelope import EnvelopeBuffer
+from .search import SearchBuffer
+from .taglist import TagListBuffer
+from .thread import ThreadBuffer
diff --git a/alot/buffers/buffer.py b/alot/buffers/buffer.py
new file mode 100644
index 00000000..fc0e7af7
--- /dev/null
+++ b/alot/buffers/buffer.py
@@ -0,0 +1,40 @@
+# 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
+
+
+class Buffer(object):
+ """Abstract base class for buffers."""
+
+ modename = None # mode identifier for subclasses
+
+ def __init__(self, ui, widget):
+ self.ui = ui
+ self.body = widget
+
+ def __str__(self):
+ return '[%s]' % self.modename
+
+ def render(self, size, focus=False):
+ return self.body.render(size, focus)
+
+ def selectable(self):
+ return self.body.selectable()
+
+ def rebuild(self):
+ """tells the buffer to (re)construct its visible content."""
+ pass
+
+ def keypress(self, size, key):
+ return self.body.keypress(size, key)
+
+ def cleanup(self):
+ """called before buffer is closed"""
+ pass
+
+ def get_info(self):
+ """
+ return dict of meta infos about this buffer.
+ This can be requested to be displayed in the statusbar.
+ """
+ return {}
diff --git a/alot/buffers/bufferlist.py b/alot/buffers/bufferlist.py
new file mode 100644
index 00000000..db6395de
--- /dev/null
+++ b/alot/buffers/bufferlist.py
@@ -0,0 +1,66 @@
+# 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
+from __future__ import absolute_import
+import urwid
+
+from .buffer import Buffer
+from ..widgets.bufferlist import BufferlineWidget
+from ..settings.const import settings
+
+
+class BufferlistBuffer(Buffer):
+ """lists all active buffers"""
+
+ modename = 'bufferlist'
+
+ def __init__(self, ui, filtfun=lambda x: x):
+ self.filtfun = filtfun
+ self.ui = ui
+ self.isinitialized = False
+ self.rebuild()
+ Buffer.__init__(self, ui, self.body)
+
+ def index_of(self, b):
+ """
+ returns the index of :class:`Buffer` `b` in the global list of active
+ buffers.
+ """
+ return self.ui.buffers.index(b)
+
+ def rebuild(self):
+ if self.isinitialized:
+ focusposition = self.bufferlist.get_focus()[1]
+ else:
+ focusposition = 0
+ self.isinitialized = True
+
+ lines = list()
+ displayedbuffers = [b for b in self.ui.buffers if self.filtfun(b)]
+ for (num, b) in enumerate(displayedbuffers):
+ line = BufferlineWidget(b)
+ if (num % 2) == 0:
+ attr = settings.get_theming_attribute('bufferlist',
+ 'line_even')
+ else:
+ attr = settings.get_theming_attribute('bufferlist', 'line_odd')
+ focus_att = settings.get_theming_attribute('bufferlist',
+ 'line_focus')
+ buf = urwid.AttrMap(line, attr, focus_att)
+ num = urwid.Text('%3d:' % self.index_of(b))
+ lines.append(urwid.Columns([('fixed', 4, num), buf]))
+ self.bufferlist = urwid.ListBox(urwid.SimpleListWalker(lines))
+ num_buffers = len(displayedbuffers)
+ if focusposition is not None and num_buffers > 0:
+ self.bufferlist.set_focus(focusposition % num_buffers)
+ self.body = self.bufferlist
+
+ def get_selected_buffer(self):
+ """returns currently selected :class:`Buffer` element from list"""
+ linewidget, _ = self.bufferlist.get_focus()
+ bufferlinewidget = linewidget.get_focus().original_widget
+ return bufferlinewidget.get_buffer()
+
+ def focus_first(self):
+ """Focus the first line in the buffer list."""
+ self.body.set_focus(0)
diff --git a/alot/buffers/envelope.py b/alot/buffers/envelope.py
new file mode 100644
index 00000000..ef88bed3
--- /dev/null
+++ b/alot/buffers/envelope.py
@@ -0,0 +1,98 @@
+# 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
+from __future__ import absolute_import
+import urwid
+import os
+
+from .buffer import Buffer
+from ..settings.const import settings
+from ..widgets.globals import HeadersList
+from ..widgets.globals import AttachmentWidget
+
+
+class EnvelopeBuffer(Buffer):
+ """message composition mode"""
+
+ modename = 'envelope'
+
+ def __init__(self, ui, envelope):
+ self.ui = ui
+ self.envelope = envelope
+ self.all_headers = False
+ self.rebuild()
+ Buffer.__init__(self, ui, self.body)
+
+ def __str__(self):
+ to = self.envelope.get('To', fallback='unset')
+ return '[envelope] to: %s' % (shorten_author_string(to, 400))
+
+ def get_info(self):
+ info = {}
+ info['to'] = self.envelope.get('To', fallback='unset')
+ return info
+
+ def cleanup(self):
+ if self.envelope.tmpfile:
+ os.unlink(self.envelope.tmpfile.name)
+
+ def rebuild(self):
+ displayed_widgets = []
+ hidden = settings.get('envelope_headers_blacklist')
+ # build lines
+ lines = []
+ for (k, vlist) in self.envelope.headers.items():
+ if (k not in hidden) or self.all_headers:
+ for value in vlist:
+ lines.append((k, value))
+
+ # sign/encrypt lines
+ if self.envelope.sign:
+ description = 'Yes'
+ sign_key = self.envelope.sign_key
+ if sign_key is not None and len(sign_key.subkeys) > 0:
+ description += ', with key ' + sign_key.uids[0].uid
+ lines.append(('GPG sign', description))
+
+ if self.envelope.encrypt:
+ description = 'Yes'
+ encrypt_keys = self.envelope.encrypt_keys.values()
+ if len(encrypt_keys) == 1:
+ description += ', with key '
+ elif len(encrypt_keys) > 1:
+ description += ', with keys '
+ key_ids = []
+ for key in encrypt_keys:
+ if key is not None and key.subkeys:
+ key_ids.append(key.uids[0].uid)
+ description += ', '.join(key_ids)
+ lines.append(('GPG encrypt', description))
+
+ if self.envelope.tags:
+ lines.append(('Tags', ','.join(self.envelope.tags)))
+
+ # add header list widget iff header values exists
+ if lines:
+ key_att = settings.get_theming_attribute('envelope', 'header_key')
+ value_att = settings.get_theming_attribute('envelope',
+ 'header_value')
+ gaps_att = settings.get_theming_attribute('envelope', 'header')
+ self.header_wgt = HeadersList(lines, key_att, value_att, gaps_att)
+ displayed_widgets.append(self.header_wgt)
+
+ # display attachments
+ lines = []
+ for a in self.envelope.attachments:
+ lines.append(AttachmentWidget(a, selectable=False))
+ if lines:
+ self.attachment_wgt = urwid.Pile(lines)
+ displayed_widgets.append(self.attachment_wgt)
+
+ self.body_wgt = urwid.Text(self.envelope.body)
+ displayed_widgets.append(self.body_wgt)
+ self.body = urwid.ListBox(displayed_widgets)
+
+ def toggle_all_headers(self):
+ """toggles visibility of all envelope headers"""
+ self.all_headers = not self.all_headers
+ self.rebuild()
diff --git a/alot/buffers/search.py b/alot/buffers/search.py
new file mode 100644
index 00000000..93359d5e
--- /dev/null
+++ b/alot/buffers/search.py
@@ -0,0 +1,125 @@
+# 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
+from __future__ import absolute_import
+import urwid
+
+from .buffer import Buffer
+from ..settings.const import settings
+from ..walker import PipeWalker
+from ..widgets.search import ThreadlineWidget
+
+
+class SearchBuffer(Buffer):
+ """shows a result list of threads for a query"""
+
+ modename = 'search'
+ threads = []
+ _REVERSE = {'oldest_first': 'newest_first',
+ 'newest_first': 'oldest_first'}
+
+ def __init__(self, ui, initialquery='', sort_order=None):
+ self.dbman = ui.dbman
+ self.ui = ui
+ self.querystring = initialquery
+ default_order = settings.get('search_threads_sort_order')
+ self.sort_order = sort_order or default_order
+ self.result_count = 0
+ self.isinitialized = False
+ self.proc = None # process that fills our pipe
+ self.rebuild()
+ Buffer.__init__(self, ui, self.body)
+
+ def __str__(self):
+ formatstring = '[search] for "%s" (%d message%s)'
+ return formatstring % (self.querystring, self.result_count,
+ 's' if self.result_count > 1 else '')
+
+ def get_info(self):
+ info = {}
+ info['querystring'] = self.querystring
+ info['result_count'] = self.result_count
+ info['result_count_positive'] = 's' if self.result_count > 1 else ''
+ return info
+
+ def cleanup(self):
+ self.kill_filler_process()
+
+ def kill_filler_process(self):
+ """
+ terminates the process that fills this buffers
+ :class:`~alot.walker.PipeWalker`.
+ """
+ if self.proc:
+ if self.proc.is_alive():
+ self.proc.terminate()
+
+ def rebuild(self, reverse=False):
+ self.isinitialized = True
+ self.reversed = reverse
+ self.kill_filler_process()
+
+ if reverse:
+ order = self._REVERSE[self.sort_order]
+ else:
+ order = self.sort_order
+
+ exclude_tags = settings.get_notmuch_setting('search', 'exclude_tags')
+ if exclude_tags:
+ exclude_tags = [t for t in exclude_tags.split(';') if t]
+
+ try:
+ self.result_count = self.dbman.count_messages(self.querystring)
+ self.pipe, self.proc = self.dbman.get_threads(self.querystring,
+ order,
+ exclude_tags)
+ except NotmuchError:
+ self.ui.notify('malformed query string: %s' % self.querystring,
+ 'error')
+ self.listbox = urwid.ListBox([])
+ self.body = self.listbox
+ return
+
+ self.threadlist = PipeWalker(self.pipe, ThreadlineWidget,
+ dbman=self.dbman,
+ reverse=reverse)
+
+ self.listbox = urwid.ListBox(self.threadlist)
+ self.body = self.listbox
+
+ def get_selected_threadline(self):
+ """
+ returns curently focussed :class:`alot.widgets.ThreadlineWidget`
+ from the result list.
+ """
+ threadlinewidget, _ = self.threadlist.get_focus()
+ return threadlinewidget
+
+ def get_selected_thread(self):
+ """returns currently selected :class:`~alot.db.Thread`"""
+ threadlinewidget = self.get_selected_threadline()
+ thread = None
+ if threadlinewidget:
+ thread = threadlinewidget.get_thread()
+ return thread
+
+ def consume_pipe(self):
+ while not self.threadlist.empty:
+ self.threadlist._get_next_item()
+
+ def focus_first(self):
+ if not self.reversed:
+ self.body.set_focus(0)
+ else:
+ self.rebuild(reverse=False)
+
+ def focus_last(self):
+ if self.reversed:
+ self.body.set_focus(0)
+ elif self.result_count < 200 or self.sort_order not in self._REVERSE:
+ self.consume_pipe()
+ num_lines = len(self.threadlist.get_lines())
+ self.body.set_focus(num_lines - 1)
+ else:
+ self.rebuild(reverse=True)
+
diff --git a/alot/buffers/taglist.py b/alot/buffers/taglist.py
new file mode 100644
index 00000000..c0a652f7
--- /dev/null
+++ b/alot/buffers/taglist.py
@@ -0,0 +1,71 @@
+# 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
+from __future__ import absolute_import
+import urwid
+
+from .buffer import Buffer
+from ..settings.const import settings
+from ..widgets.globals import TagWidget
+
+
+class TagListBuffer(Buffer):
+ """lists all tagstrings present in the notmuch database"""
+
+ modename = 'taglist'
+
+ def __init__(self, ui, alltags=None, filtfun=lambda x: x):
+ self.filtfun = filtfun
+ self.ui = ui
+ self.tags = alltags or []
+ self.isinitialized = False
+ self.rebuild()
+ Buffer.__init__(self, ui, self.body)
+
+ def rebuild(self):
+ if self.isinitialized:
+ focusposition = self.taglist.get_focus()[1]
+ else:
+ focusposition = 0
+ self.isinitialized = True
+
+ lines = list()
+ displayedtags = sorted((t for t in self.tags if self.filtfun(t)),
+ key=str.lower)
+ for (num, b) in enumerate(displayedtags):
+ if (num % 2) == 0:
+ attr = settings.get_theming_attribute('taglist', 'line_even')
+ else:
+ attr = settings.get_theming_attribute('taglist', 'line_odd')
+ focus_att = settings.get_theming_attribute('taglist', 'line_focus')
+
+ tw = TagWidget(b, attr, focus_att)
+ rows = [('fixed', tw.width(), tw)]
+ if tw.hidden:
+ rows.append(urwid.Text(b + ' [hidden]'))
+ elif tw.translated is not b:
+ rows.append(urwid.Text('(%s)' % b))
+ line = urwid.Columns(rows, dividechars=1)
+ line = urwid.AttrMap(line, attr, focus_att)
+ lines.append(line)
+
+ self.taglist = urwid.ListBox(urwid.SimpleListWalker(lines))
+ self.body = self.taglist
+
+ self.taglist.set_focus(focusposition % len(displayedtags))
+
+ def focus_first(self):
+ """Focus the first line in the tag list."""
+ self.body.set_focus(0)
+
+ def focus_last(self):
+ allpos = self.taglist.body.positions(reverse=True)
+ if allpos:
+ lastpos = allpos[0]
+ self.body.set_focus(lastpos)
+
+ def get_selected_tag(self):
+ """returns selected tagstring"""
+ cols, _ = self.taglist.get_focus()
+ tagwidget = cols.original_widget.get_focus()
+ return tagwidget.tag
diff --git a/alot/buffers/thread.py b/alot/buffers/thread.py
new file mode 100644
index 00000000..1ad99c32
--- /dev/null
+++ b/alot/buffers/thread.py
@@ -0,0 +1,335 @@
+# 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
+from __future__ import absolute_import
+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()