summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2020-02-08 13:56:56 +0100
committerAnton Khirnov <anton@khirnov.net>2020-02-19 16:00:44 +0100
commitd25d788bdcf91f4066ae8e80ef7aebe85213d4d3 (patch)
treef0b35cfdd0fbeb36af971a6ea640f145771d16aa
parent48dac1d9089ce2a36c55dc4768b24293d1257a37 (diff)
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.
-rw-r--r--.travis.yml1
-rw-r--r--alot/buffers/thread.py243
-rw-r--r--alot/commands/thread.py47
-rw-r--r--alot/db/message.py16
-rw-r--r--alot/db/thread.py25
-rw-r--r--alot/widgets/thread.py388
-rw-r--r--docs/source/conf.py1
-rw-r--r--docs/source/installation.rst5
-rwxr-xr-xsetup.py1
9 files changed, 274 insertions, 453 deletions
diff --git a/.travis.yml b/.travis.yml
index d207815f..20e92f18 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -73,7 +73,6 @@ install:
### PYHON DEPS ###
#################################################
- pip install urwid
- - pip install urwidtrees
- pip install configobj
- pip install gpg
- pip install twisted
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()
diff --git a/alot/commands/thread.py b/alot/commands/thread.py
index f720e297..6ceaddd2 100644
--- a/alot/commands/thread.py
+++ b/alot/commands/thread.py
@@ -555,45 +555,40 @@ class ChangeDisplaymodeCommand(Command):
logging.debug('matching lines %s...', self.query)
if self.query is None:
- messagetrees = [tbuffer.get_selected_messagetree()]
+ msg_wgts = [tbuffer.get_selected_message_widget()]
else:
- messagetrees = tbuffer.messagetrees()
+ msg_wgts = list(tbuffer.message_widgets())
if self.query != '*':
def matches(msgt):
msg = msgt.get_message()
return msg.matches(self.query)
- messagetrees = [m for m in messagetrees if matches(m)]
+ msg_wgts = [m for m in msg_wgts if matches(m)]
- for mt in messagetrees:
+ for m in msg_wgts:
# determine new display values for this message
if self.visible == 'toggle':
- visible = mt.is_collapsed(mt.root)
+ visible = not m.display_content
else:
visible = self.visible
if self.raw == 'toggle':
tbuffer.focus_selected_message()
- raw = not mt.display_source if self.raw == 'toggle' else self.raw
- all_headers = not mt.display_all_headers \
+ raw = not m.display_source if self.raw == 'toggle' else self.raw
+ all_headers = not m.display_all_headers \
if self.all_headers == 'toggle' else self.all_headers
# collapse/expand depending on new 'visible' value
if visible is False:
- mt.collapse(mt.root)
+ m.collapse()
elif visible is True: # could be None
- mt.expand(mt.root)
+ m.expand()
tbuffer.focus_selected_message()
# set new values in messagetree obj
if raw is not None:
- mt.display_source = raw
+ m.display_source = raw
if all_headers is not None:
- mt.display_all_headers = all_headers
- mt.debug()
- # let the messagetree reassemble itself
- mt.reassemble()
- # refresh the buffer (clears Tree caches etc)
- tbuffer.refresh()
+ m.display_all_headers = all_headers
@registerCommand(MODE, 'pipeto', arguments=[
@@ -1057,26 +1052,20 @@ class TagCommand(Command):
async def apply(self, ui):
tbuffer = ui.current_buffer
if self.all:
- messagetrees = tbuffer.messagetrees()
+ msg_wgts = list(tbuffer.message_widgets())
else:
- messagetrees = [tbuffer.get_selected_messagetree()]
-
- def refresh_widgets():
- for mt in messagetrees:
- mt.refresh()
- tbuffer.refresh()
+ msg_wgts = [tbuffer.get_selected_message_widget()]
tags = [t for t in self.tagsstring.split(',') if t]
try:
- for mt in messagetrees:
+ for mt in msg_wgts:
m = mt.get_message()
if self.action == 'add':
- m.add_tags(tags, afterwards=refresh_widgets)
+ m.add_tags(tags)
if self.action == 'set':
- m.add_tags(tags, afterwards=refresh_widgets,
- remove_rest=True)
+ m.add_tags(tags, remove_rest=True)
elif self.action == 'remove':
- m.remove_tags(tags, afterwards=refresh_widgets)
+ m.remove_tags(tags)
elif self.action == 'toggle':
to_remove = []
to_add = []
@@ -1086,7 +1075,7 @@ class TagCommand(Command):
else:
to_add.append(t)
m.remove_tags(to_remove)
- m.add_tags(to_add, afterwards=refresh_widgets)
+ m.add_tags(to_add)
except DatabaseROError:
ui.notify('index in read-only mode', priority='error')
diff --git a/alot/db/message.py b/alot/db/message.py
index f9e8e37b..62b85788 100644
--- a/alot/db/message.py
+++ b/alot/db/message.py
@@ -35,24 +35,28 @@ class Message:
"""value of the Message-Id header (str)"""
id = None
+ """this message's depth in the thread tree"""
+ depth = None
+
"""A list of replies to this message"""
replies = None
- def __init__(self, dbman, msg, thread, replies):
+ def __init__(self, dbman, thread, msg, depth):
"""
:param dbman: db manager that is used for further lookups
:type dbman: alot.db.DBManager
- :param msg: the wrapped message
- :type msg: notmuch.database.Message
:param thread: this messages thread
:type thread: :class:`~alot.db.Thread`
- :param replies: a list of replies to this message
- :type replies alot.db.message.Message
+ :param msg: the wrapped message
+ :type msg: notmuch.database.Message
+ :param depth: depth of this message in the thread tree (0 for toplevel
+ messages, 1 for their replies etc.)
+ :type depth int
"""
self._dbman = dbman
self.id = msg.get_message_id()
self.thread = thread
- self.replies = replies
+ self.depth = depth
try:
self.date = datetime.fromtimestamp(msg.get_date())
except ValueError:
diff --git a/alot/db/thread.py b/alot/db/thread.py
index 2792737c..a76997f8 100644
--- a/alot/db/thread.py
+++ b/alot/db/thread.py
@@ -40,7 +40,7 @@ class Thread:
toplevel_messages = None
"""A list of ids of all messages in this thread in depth-first order"""
- message_ids = None
+ message_list = None
"""A dict mapping Message-Id strings to Message instances"""
messages = None
@@ -58,7 +58,7 @@ class Thread:
self._tags = set()
self.toplevel_messages = []
- self.message_ids = []
+ self.message_list = []
self.messages = {}
self.refresh(thread)
@@ -97,7 +97,7 @@ class Thread:
self._tags = {t for t in thread.get_tags()}
- self.messages, self.toplevel_messages, self.message_ids = self._gather_messages()
+ self.messages, self.toplevel_messages, self.message_list = self._gather_messages()
def _gather_messages(self):
query = self._dbman.query('thread:' + self.id)
@@ -105,25 +105,26 @@ class Thread:
msgs = {}
msg_tree = []
- ids = []
+ msg_list = []
- def thread_tree_walk(nm_msg):
- msg_id = nm_msg.get_message_id()
- ids.append(msg_id)
+ def thread_tree_walk(nm_msg, depth):
+ msg = Message(self._dbman, self, nm_msg, depth)
+
+ msg_list.append(msg)
replies = []
for m in nm_msg.get_replies():
- replies.append(thread_tree_walk(m))
- msg = Message(self._dbman, nm_msg, self, replies)
+ replies.append(thread_tree_walk(m, depth + 1))
- msgs[msg_id] = msg
+ msg.replies = replies
+ msgs[msg.id] = msg
return msg
for m in nm_thread.get_toplevel_messages():
- msg_tree.append(thread_tree_walk(m))
+ msg_tree.append(thread_tree_walk(m, 0))
- return msgs, msg_tree, ids
+ return msgs, msg_tree, msg_list
def __str__(self):
return "thread:%s: %s" % (self.id, self.subject)
diff --git a/alot/widgets/thread.py b/alot/widgets/thread.py
index 97a34417..0b4e2404 100644
--- a/alot/widgets/thread.py
+++ b/alot/widgets/thread.py
@@ -6,7 +6,6 @@ Widgets specific to thread mode
"""
import logging
import urwid
-from urwidtrees import Tree, SimpleTree, CollapsibleTree
from .globals import TagWidget
from .globals import AttachmentWidget
@@ -20,19 +19,19 @@ class MessageSummaryWidget(urwid.WidgetWrap):
one line summary of a :class:`~alot.db.message.Message`.
"""
- def __init__(self, message, even=True):
+ def __init__(self, message, odd):
"""
:param message: a message
:type message: alot.db.Message
- :param even: even entry in a pile of messages? Used for theming.
- :type even: bool
+ :param odd: odd entry in a pile of messages? Used for theming.
+ :type odd: bool
"""
self.message = message
- self.even = even
- if even:
- attr = settings.get_theming_attribute('thread', 'summary', 'even')
- else:
+ self.odd = odd
+ if odd:
attr = settings.get_theming_attribute('thread', 'summary', 'odd')
+ else:
+ attr = settings.get_theming_attribute('thread', 'summary', 'even')
focus_att = settings.get_theming_attribute('thread', 'summary',
'focus')
cols = []
@@ -54,7 +53,7 @@ class MessageSummaryWidget(urwid.WidgetWrap):
cols.append(('fixed', tag_widget.width(), tag_widget))
line = urwid.AttrMap(urwid.Columns(cols, dividechars=1), attr,
focus_att)
- urwid.WidgetWrap.__init__(self, line)
+ super().__init__(line)
def __str__(self):
mail = self.message.get_email()
@@ -81,7 +80,7 @@ class FocusableText(urwid.WidgetWrap):
def __init__(self, txt, att, att_focus):
t = urwid.Text(txt)
w = urwid.AttrMap(t, att, att_focus)
- urwid.WidgetWrap.__init__(self, w)
+ super().__init__(w)
def selectable(self):
return True
@@ -90,27 +89,16 @@ class FocusableText(urwid.WidgetWrap):
return key
-class TextlinesList(SimpleTree):
- def __init__(self, content, attr=None, attr_focus=None):
- """
- :class:`SimpleTree` that contains a list of all-level-0 Text widgets
- for each line in content.
- """
- structure = [(FocusableText(content, attr, attr_focus), None)]
- SimpleTree.__init__(self, structure)
-
-
-class DictList(SimpleTree):
+class HeadersWidget(urwid.WidgetWrap):
"""
- :class:`SimpleTree` that displays key-value pairs.
-
- The structure will obey the Tree API but will not actually be a tree
- but a flat list: It contains one top-level node (displaying the k/v pair in
- Columns) per pair. That is, the root will be the first pair,
- its sibblings will be the other pairs and first|last_child will always
- be None.
+ A flow widget displaying message headers.
"""
- def __init__(self, content, key_attr, value_attr, gaps_attr=None):
+
+ _key_attr = None
+ _value_attr = None
+ _gaps_attr = None
+
+ def __init__(self, key_attr, value_attr, gaps_attr):
"""
:param headerslist: list of key/value pairs to display
:type headerslist: list of (str, str)
@@ -121,189 +109,145 @@ class DictList(SimpleTree):
:param gaps_attr: theming attribute to wrap lines in
:type gaps_attr: urwid.AttrSpec
"""
- max_key_len = 1
- structure = []
- # calc max length of key-string
- for key, value in content:
- if len(key) > max_key_len:
- max_key_len = len(key)
- for key, value in content:
- # todo : even/odd
- keyw = ('fixed', max_key_len + 1,
- urwid.Text((key_attr, key)))
- valuew = urwid.Text((value_attr, value))
- line = urwid.Columns([keyw, valuew])
- if gaps_attr is not None:
- line = urwid.AttrMap(line, gaps_attr)
- structure.append((line, None))
- SimpleTree.__init__(self, structure)
-
-
-class MessageTree(CollapsibleTree):
- """
- :class:`Tree` that displays contents of a single :class:`alot.db.Message`.
+ self._key_attr = key_attr
+ self._value_attr = value_attr
+ self._gaps_attr = gaps_attr
- Its root node is a :class:`MessageSummaryWidget`, and its child nodes
- reflect the messages content (parts for headers/attachments etc).
+ super().__init__(urwid.Pile([]))
+
+ def set_headers(self, headers):
+ if len(headers) == 0:
+ self._w.contents.clear()
+ return
+
+ max_key_len = max(map(lambda x: len(x[0]), headers))
+
+ widgets = []
+ for key, value in headers:
+ # TODO even/odd
+ keyw = ('fixed', max_key_len + 1,
+ urwid.Text((self._key_attr, key)))
+ valuew = urwid.Text((self._value_attr, decode_header(value)))
+ linew = urwid.AttrMap(urwid.Columns([keyw, valuew]), self._gaps_attr)
+ widgets.append(linew)
+
+ self._w.contents = [(w, ('pack', None)) for w in widgets]
+
+class MessageWidget(urwid.WidgetWrap):
+ """
+ A flow widget displaying the contents of a single :class:`alot.db.Message`.
Collapsing this message corresponds to showing the summary only.
"""
- def __init__(self, message, odd=True):
+
+ _display_all_headers = None
+ _display_content = None
+ _display_source = None
+
+ _headers_wgt = None
+ _summary_wgt = None
+ _source_wgt = None
+ _body_wgt = None
+ _attach_wgt = None
+
+ def __init__(self, message, odd):
"""
:param message: Message to display
:type message: alot.db.Message
- :param odd: theme summary widget as if this is an odd line
- (in the message-pile)
+ :param odd: theme summary widget as if this is an odd line in the thread order
:type odd: bool
"""
- self._message = message
- self._odd = odd
- self.display_source = False
- self._summaryw = None
- self._bodytree = None
- self._sourcetree = None
- self.display_all_headers = False
- self._all_headers_tree = None
- self._default_headers_tree = None
- self.display_attachments = True
- self._attachments = None
- self._maintree = SimpleTree(self._assemble_structure(True))
- CollapsibleTree.__init__(self, self._maintree)
+ self._message = message
- def get_message(self):
- return self._message
+ self.display_attachments = True
- def reassemble(self):
- self._maintree._treelist = self._assemble_structure()
+ self._headers_wgt = self._get_headers()
+ self._summary_wgt = MessageSummaryWidget(message, odd)
+ self._source_wgt = self._get_source()
+ self._body_wgt = self._get_body()
+ self._attach_wgt = self._get_attachments()
- def refresh(self):
- self._summaryw = None
- self.reassemble()
+ super().__init__(urwid.Pile([]))
- def debug(self):
- logging.debug('collapsed %s', self.is_collapsed(self.root))
- logging.debug('display_source %s', self.display_source)
- logging.debug('display_all_headers %s', self.display_all_headers)
- logging.debug('display_attachements %s', self.display_attachments)
- logging.debug('AHT %s', str(self._all_headers_tree))
- logging.debug('DHT %s', str(self._default_headers_tree))
- logging.debug('MAINTREE %s', str(self._maintree._treelist))
+ self.display_all_headers = False
+ self.display_source = False
+ self.display_content = False
- def expand(self, pos):
- """
- overload CollapsibleTree.expand method to ensure all parts are present.
- Initially, only the summary widget is created to avoid reading the
- messafe file and thus speed up the creation of this object. Once we
- expand = unfold the message, we need to make sure that body/attachments
- exist.
- """
- logging.debug("MT expand")
- if not self._bodytree:
- self.reassemble()
- CollapsibleTree.expand(self, pos)
-
- def _assemble_structure(self, summary_only=False):
- if summary_only:
- return [(self._get_summary(), None)]
-
- mainstruct = []
- if self.display_source:
- mainstruct.append((self._get_source(), None))
- else:
- mainstruct.append((self._get_headers(), None))
+ def get_message(self):
+ return self._message
- attachmenttree = self._get_attachments()
- if attachmenttree is not None:
- mainstruct.append((attachmenttree, None))
+ def _reassemble(self, display_content, display_source):
+ widgets = [self._summary_wgt]
- bodytree = self._get_body()
- if bodytree is not None:
- mainstruct.append((bodytree, None))
+ if display_content:
+ if display_source:
+ widgets.append(self._source_wgt)
+ else:
+ widgets.append(self._headers_wgt)
- structure = [
- (self._get_summary(), mainstruct)
- ]
- return structure
+ if self._attach_wgt is not None:
+ widgets.append(self._attach_wgt)
- def collapse_if_matches(self, querystring):
- """
- collapse (and show summary only) if the :class:`alot.db.Message`
- matches given `querystring`
- """
- self.set_position_collapsed(
- self.root, self._message.matches(querystring))
+ widgets.append(self._body_wgt)
- def _get_summary(self):
- if self._summaryw is None:
- self._summaryw = MessageSummaryWidget(
- self._message, even=(not self._odd))
- return self._summaryw
+ self._w.contents = [(w, ('pack', None)) for w in widgets]
def _get_source(self):
- if self._sourcetree is None:
- sourcetxt = self._message.get_email().as_string()
- sourcetxt = string_sanitize(sourcetxt)
- att = settings.get_theming_attribute('thread', 'body')
- att_focus = settings.get_theming_attribute('thread', 'body_focus')
- self._sourcetree = TextlinesList(sourcetxt, att, att_focus)
- return self._sourcetree
+ sourcetxt = self._message.get_email().as_string()
+ sourcetxt = string_sanitize(sourcetxt)
+ att = settings.get_theming_attribute('thread', 'body')
+ att_focus = settings.get_theming_attribute('thread', 'body_focus')
+ return FocusableText(sourcetxt, att, att_focus)
def _get_body(self):
- if self._bodytree is None:
- bodytxt = self._message.get_body_text()
- if bodytxt:
- att = settings.get_theming_attribute('thread', 'body')
- att_focus = settings.get_theming_attribute(
- 'thread', 'body_focus')
- self._bodytree = TextlinesList(bodytxt, att, att_focus)
- return self._bodytree
+ bodytxt = self._message.get_body_text()
+ att = settings.get_theming_attribute('thread', 'body')
+ att_focus = settings.get_theming_attribute(
+ 'thread', 'body_focus')
+ return FocusableText(bodytxt, att, att_focus)
def _get_headers(self):
- if self.display_all_headers is True:
- if self._all_headers_tree is None:
- self._all_headers_tree = self.construct_header_pile()
- ret = self._all_headers_tree
- else:
- if self._default_headers_tree is None:
- headers = settings.get('displayed_headers')
- self._default_headers_tree = self.construct_header_pile(
- headers)
- ret = self._default_headers_tree
- return ret
+ key_attr = settings.get_theming_attribute('thread', 'header_key')
+ value_attr = settings.get_theming_attribute('thread', 'header_value')
+ gaps_attr = settings.get_theming_attribute('thread', 'header')
+ return HeadersWidget(key_attr, value_attr, gaps_attr)
def _get_attachments(self):
- if self._attachments is None:
- alist = []
- for a in self._message.get_attachments():
- alist.append((AttachmentWidget(a), None))
- if alist:
- self._attachments = SimpleTree(alist)
- return self._attachments
-
- def construct_header_pile(self, headers=None, normalize=True):
+ alist = []
+ for a in self._message.get_attachments():
+ alist.append(AttachmentWidget(a))
+ if alist:
+ return urwid.Pile(alist)
+ return None
+
+ @property
+ def display_all_headers(self):
+ return self._display_all_headers
+ @display_all_headers.setter
+ def display_all_headers(self, val):
+ val = bool(val)
+
+ if val == self._display_all_headers:
+ return
+
mail = self._message.get_email()
lines = []
- if headers is None:
+ if val:
# collect all header/value pairs in the order they appear
- for key, value in mail.items():
- dvalue = decode_header(value, normalize=normalize)
- lines.append((key, dvalue))
+ lines = list(mail.items())
else:
# only a selection of headers should be displayed.
# use order of the `headers` parameter
- for key in headers:
+ for key in settings.get('displayed_headers'):
if key in mail:
if key.lower() in ['cc', 'bcc', 'to']:
- values = mail.get_all(key)
- values = [decode_header(
- v, normalize=normalize) for v in values]
- lines.append((key, ', '.join(values)))
+ lines.append((key, ', '.join(mail.get_all(key))))
else:
for value in mail.get_all(key):
- dvalue = decode_header(value, normalize=normalize)
- lines.append((key, dvalue))
+ lines.append((key, value))
elif key.lower() == 'tags':
+ # TODO this should be in a separate widget
logging.debug('want tags header')
values = []
for t in self._message.get_tags():
@@ -314,79 +258,35 @@ class MessageTree(CollapsibleTree):
lines.append((key, ', '.join(values)))
# OpenPGP pseudo headers
+ # TODO this should be in a separate widget
if mail[X_SIGNATURE_MESSAGE_HEADER]:
lines.append(('PGP-Signature', mail[X_SIGNATURE_MESSAGE_HEADER]))
- key_att = settings.get_theming_attribute('thread', 'header_key')
- value_att = settings.get_theming_attribute('thread', 'header_value')
- gaps_att = settings.get_theming_attribute('thread', 'header')
- return DictList(lines, key_att, value_att, gaps_att)
+ self._headers_wgt.set_headers(lines)
+ self._display_all_headers = val
-class ThreadTree(Tree):
- """
- :class:`Tree` that parses a given :class:`alot.db.Thread` into a tree of
- :class:`MessageTrees <MessageTree>` that display this threads individual
- messages. As MessageTreess are *not* urwid widgets themself this is to be
- used in combination with :class:`NestedTree` only.
- """
- def __init__(self, thread):
- self._thread = thread
- self.root = thread.toplevel_messages[0].id
- self._parent_of = {}
- self._first_child_of = {}
- self._last_child_of = {}
- self._next_sibling_of = {}
- self._prev_sibling_of = {}
- self._message = {}
-
- def accumulate(msg, odd=True):
- """recursively read msg and its replies"""
- mid = msg.id
- self._message[mid] = MessageTree(msg, odd)
- odd = not odd
- last = None
- self._first_child_of[mid] = None
- for reply in msg.replies:
- rid = reply.id
- if self._first_child_of[mid] is None:
- self._first_child_of[mid] = rid
- self._parent_of[rid] = mid
- self._prev_sibling_of[rid] = last
- self._next_sibling_of[last] = rid
- last = rid
- odd = accumulate(reply, odd)
- self._last_child_of[mid] = last
- return odd
-
- last = None
- for msg in thread.toplevel_messages:
- mid = msg.id
- self._prev_sibling_of[mid] = last
- self._next_sibling_of[last] = mid
- accumulate(msg)
- last = mid
- self._next_sibling_of[last] = None
-
- # Tree API
- def __getitem__(self, pos):
- return self._message.get(pos)
-
- def parent_position(self, pos):
- return self._parent_of.get(pos)
-
- def first_child_position(self, pos):
- return self._first_child_of.get(pos)
-
- def last_child_position(self, pos):
- return self._last_child_of.get(pos)
-
- def next_sibling_position(self, pos):
- return self._next_sibling_of.get(pos)
-
- def prev_sibling_position(self, pos):
- return self._prev_sibling_of.get(pos)
-
- @staticmethod
- def position_of_messagetree(mt):
- return mt._message.id
+ @property
+ def display_content(self):
+ return self._display_content
+ @display_content.setter
+ def display_content(self, val):
+ val = bool(val)
+
+ if val == self._display_content:
+ return
+
+ self._reassemble(val, self.display_source)
+ self._display_content = val
+
+ def expand(self):
+ self.display_content = True
+ def collapse(self):
+ self.display_content = False
+
+ def collapse_if_matches(self, querystring):
+ """
+ collapse (and show summary only) if the :class:`alot.db.Message`
+ matches given `querystring`
+ """
+ self.display_content = not self._message.matches(querystring)
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 7531098a..1a141d68 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -27,7 +27,6 @@ autodoc_mock_imports = [
'magic',
'notmuch',
'urwid',
- 'urwidtrees',
'validate',
]
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index 85a4189b..d54283c5 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -26,18 +26,17 @@ A full list of dependencies is below:
* `configobj <http://www.voidspace.org.uk/python/configobj.html>`_, ≥ `4.7.0`
* `libnotmuch <http://notmuchmail.org/>`_ and it's python bindings, ≥ `0.27`
* `urwid <http://excess.org/urwid/>`_ toolkit, ≥ `1.3.0`
-* `urwidtrees <https://github.com/pazz/urwidtrees>`_, ≥ `1.0`
* `gpg <http://www.gnupg.org/related_software/gpgme>`_ and it's python bindings, ≥ `1.9.0`
* `twisted <https://twistedmatrix.com>`_, ≥ `18.4.0`
On Debian/Ubuntu these are packaged as::
- python3-setuptools python3-magic python3-configobj python3-notmuch python3-urwid python3-urwidtrees python3-gpg python3-twisted
+ python3-setuptools python3-magic python3-configobj python3-notmuch python3-urwid python3-gpg python3-twisted
On Fedora/Redhat these are packaged as::
- python-setuptools python-magic python-configobj python-notmuch python-urwid python-urwidtrees python-gpg python-twisted
+ python-setuptools python-magic python-configobj python-notmuch python-urwid python-gpg python-twisted
To set up and install the latest development version::
diff --git a/setup.py b/setup.py
index 5e95cd3e..67628890 100755
--- a/setup.py
+++ b/setup.py
@@ -46,7 +46,6 @@ setup(
install_requires=[
'notmuch>=0.27',
'urwid>=1.3.0',
- 'urwidtrees>=1.0',
'twisted>=18.4.0',
'python-magic',
'configobj>=4.7.0',