diff options
Diffstat (limited to 'alot/widgets/thread.py')
-rw-r--r-- | alot/widgets/thread.py | 388 |
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) |