summaryrefslogtreecommitdiff
path: root/alot/widgets/thread.py
diff options
context:
space:
mode:
Diffstat (limited to 'alot/widgets/thread.py')
-rw-r--r--alot/widgets/thread.py388
1 files changed, 144 insertions, 244 deletions
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)