summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2020-03-01 21:49:28 +0100
committerAnton Khirnov <anton@khirnov.net>2020-03-01 21:49:28 +0100
commitd035e850d3dde1fbc8d556861f7ef72d581c5506 (patch)
treece691c85f28caac28f32f453484d5ab3b3dff74f
parentc6e3144579bd8310b04d326f4ee32035fe237925 (diff)
buffers/thread: make the widget split-window
The top part displayes the thread structure, the bottom half the message body. This makes more sense then displaying the message inside the tree structure and makes it easier to implement features such as folding a part of the message body. Drop commands related to folding, since that functionality does not exist anymore.
-rw-r--r--alot/buffers/thread.py137
-rw-r--r--alot/commands/search.py2
-rw-r--r--alot/commands/thread.py35
-rw-r--r--alot/completion/command.py6
-rw-r--r--alot/defaults/default.bindings6
-rw-r--r--alot/widgets/thread.py66
6 files changed, 102 insertions, 150 deletions
diff --git a/alot/buffers/thread.py b/alot/buffers/thread.py
index 8615b37d..c60b123c 100644
--- a/alot/buffers/thread.py
+++ b/alot/buffers/thread.py
@@ -18,7 +18,20 @@ class ThreadBuffer(Buffer):
modename = 'thread'
- _msg_widgets = None
+ # list of the widgets containing the message body
+ # indexed by its depth-first position in the thread tree
+ _msg_widgets = None
+ # widget showing the thread tree
+ _msgtree_widget = None
+
+ # WidgetPlaceholder that wraps currently displayed message
+ _cur_msg_holder = None
+ # WidgetPlaceholder that wraps current divider
+ _divider_holder = None
+
+ # divider widgets placed between the thread tree and the message
+ _divider_up = None
+ _divider_down = None
def __init__(self, ui, thread):
"""
@@ -27,10 +40,29 @@ class ThreadBuffer(Buffer):
:param thread: thread to display
:type thread: :class:`~alot.db.Thread`
"""
- self.thread = thread
-
+ self.thread = thread
self._indent_width = settings.get('thread_indent_replies')
+
+ # create the widgets composing the buffer
+ self._msgtree_widget = urwid.ListBox(urwid.SimpleFocusListWalker([]))
+ self._divider_up = urwid.Divider('↑')
+ self._divider_down = urwid.Divider('↓')
+ self._divider_holder = urwid.WidgetPlaceholder(self._divider_up)
+ self._cur_msg_holder = urwid.WidgetPlaceholder(urwid.SolidFill())
+
+ self.body = urwid.Pile([
+ # 0 is a dummy value and is overridden later rebuild()
+ ('weight', 0, self._msgtree_widget),
+ ('pack', self._divider_holder),
+ # fixed weight for the message body
+ # TODO: should it depend on the message body length (or perhaps something else)?
+ ('weight', 50, self._cur_msg_holder),
+ ])
+
+ urwid.connect_signal(self._msgtree_widget.body, "modified", self._update_cur_msg)
+
self.rebuild()
+
super().__init__(ui, self.body)
def __str__(self):
@@ -38,6 +70,12 @@ class ThreadBuffer(Buffer):
self.thread.total_messages,
's' * (self.thread.total_messages > 1))
+ def _update_cur_msg(self):
+ pos = self._msgtree_widget.body.focus
+ if pos is not None and pos < len(self._msg_widgets):
+ logging.debug('displaying message %s ', pos)
+ self._cur_msg_holder.original_widget = self._msg_widgets[pos]
+
def translated_tags_str(self, intersection=False):
tags = self.thread.get_tags(intersection=intersection)
trans = [settings.get_tagstring_representation(tag)['translated']
@@ -55,27 +93,35 @@ class ThreadBuffer(Buffer):
return info
def rebuild(self):
+ self._msg_widgets = []
+ self._msgtree_widget.body.clear()
+ self._cur_msg_holder.original_widget = urwid.SolidFill()
+
try:
self.thread.refresh()
except NonexistantObjectError:
- self.body = urwid.SolidFill()
return
- self._msg_widgets = []
-
- body_walker = urwid.SimpleFocusListWalker([])
+ list_walker = self._msgtree_widget.body
for pos, msg in enumerate(self.thread.message_list):
- msg_wgt = MessageWidget(msg, pos & 1)
- wgt = ThreadNode(msg_wgt, self.thread, pos, self._indent_width)
+ msg_wgt = MessageWidget(msg)
+ wgt = ThreadNode(msg, self.thread, pos, self._indent_width)
self._msg_widgets.append(msg_wgt)
- body_walker.append(wgt)
+ list_walker.append(wgt)
+
+ # the weight given to the thread-tree widget is equal to the number of
+ # messages in it, up to the limit of 50 (when it is equal to the message
+ # body widget)
+ tree_weight = min(len(self._msg_widgets), 50)
+ self.body.contents[0] = (self._msgtree_widget, ('weight', tree_weight))
- self.body = urwid.ListBox(body_walker)
+ if len(self._msg_widgets) > 0:
+ self._cur_msg_holder.original_widget = self._msg_widgets[0]
def get_selected_message_position(self):
"""Return position of focussed message in the thread tree."""
- return self.body.focus_position
+ return self._msgtree_widget.focus_position
def get_selected_message_widget(self):
"""Return currently focused :class:`MessageWidget`."""
@@ -107,7 +153,7 @@ class ThreadBuffer(Buffer):
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)
+ self._msgtree_widget.set_focus(pos)
def focus_first(self):
"""set focus to first message of thread"""
@@ -193,51 +239,20 @@ class ThreadBuffer(Buffer):
"""focus previous matching message in depth first order"""
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: x.display_content, 1)
-
- def focus_prev_unfolded(self):
- """focus previous unfolded message in depth first order"""
- self._focus_property(lambda x: x.display_content, -1)
-
- def expand(self, msgpos):
- """expand message at given position"""
- self.body.body[msgpos].expand()
-
- def expand_all(self):
- """expand all messages in thread"""
- for msg in self.message_widgets():
- msg.expand()
-
- def collapse(self, msgpos):
- """collapse message at given position"""
- self.body.body[msgpos].collapse()
- self.focus_selected_message()
-
- def collapse_all(self):
- """collapse all messages in thread"""
- for msg in self.message_widgets():
- msg.collapse()
- 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
- """
- focus_set = False
- first = None
- for pos, msg_wgt in enumerate(self.message_widgets()):
- msg = msg_wgt.get_message()
- if msg.matches(querystring):
- msg_wgt.expand()
- if focus_first and not focus_set:
- self.set_focus(pos)
- focus_set = True
- else:
- msg_wgt.collapse()
+ def focus_thread_widget(self):
+ """set focus on the thread widget"""
+ logging.debug('setting focus to thread widget')
+ self.body.focus_position = 0
+ self._divider_holder.original_widget = self._divider_up
+ def focus_msg_widget(self):
+ """set focus on the message widget"""
+ logging.debug('setting focus to message widget')
+ self.body.focus_position = 2
+ self._divider_holder.original_widget = self._divider_down
+ def focus_toggle(self):
+ if self.body.focus_position == 0:
+ self.focus_msg_widget()
+ elif self.body.focus_position == 2:
+ self.focus_thread_widget()
+ else:
+ raise ValueError('Invalid focus position: %s' % str(self.body.focus_position))
diff --git a/alot/commands/search.py b/alot/commands/search.py
index a2d598df..a20b2220 100644
--- a/alot/commands/search.py
+++ b/alot/commands/search.py
@@ -40,7 +40,7 @@ class OpenThreadCommand(Command):
tb = buffers.ThreadBuffer(ui, self.thread)
ui.buffer_open(tb)
- tb.unfold_matching(query)
+ tb.focus_next_matching(query)
@registerCommand(MODE, 'refine', help='refine query', arguments=[
diff --git a/alot/commands/thread.py b/alot/commands/thread.py
index a0f148bb..512db7d4 100644
--- a/alot/commands/thread.py
+++ b/alot/commands/thread.py
@@ -484,14 +484,6 @@ class EditNewCommand(Command):
@registerCommand(
- MODE, 'fold', help='fold message(s)', forced={'visible': False},
- arguments=[(['query'], {'help': 'query used to filter messages to affect',
- 'nargs': '*'})])
-@registerCommand(
- MODE, 'unfold', help='unfold message(s)', forced={'visible': True},
- arguments=[(['query'], {'help': 'query used to filter messages to affect',
- 'nargs': '*'})])
-@registerCommand(
MODE, 'togglesource', help='display message source',
forced={'raw': 'toggle'},
arguments=[(['query'], {'help': 'query used to filter messages to affect',
@@ -507,16 +499,13 @@ class EditNewCommand(Command):
'validator': cargparse.is_int_or_pm})])
class ChangeDisplaymodeCommand(Command):
- """fold or unfold messages"""
repeatable = True
- def __init__(self, query=None, visible=None, raw=None, all_headers=None,
+ def __init__(self, query=None, raw=None, all_headers=None,
indent=None, **kwargs):
"""
:param query: notmuch query string used to filter messages to affect
:type query: str
- :param visible: unfold if `True`, fold if `False`, ignore if `None`
- :type visible: True, False, 'toggle' or None
:param raw: display raw message text.
:type raw: True, False, 'toggle' or None
:param all_headers: show all headers (only visible if not in raw mode)
@@ -527,7 +516,6 @@ class ChangeDisplaymodeCommand(Command):
self.query = None
if query:
self.query = ' '.join(query)
- self.visible = visible
self.raw = raw
self.all_headers = all_headers
self.indent = indent
@@ -549,7 +537,6 @@ class ChangeDisplaymodeCommand(Command):
# make sure indent remains non-negative
tbuffer._indent_width = max(newindent, 0)
tbuffer.rebuild()
- tbuffer.collapse_all()
ui.update()
logging.debug('matching lines %s...', self.query)
@@ -567,21 +554,12 @@ class ChangeDisplaymodeCommand(Command):
for m in msg_wgts:
# determine new display values for this message
- if self.visible == 'toggle':
- visible = not m.display_content
- else:
- visible = self.visible
if self.raw == 'toggle':
tbuffer.focus_selected_message()
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:
- m.collapse()
- elif visible is True: # could be None
- m.expand()
tbuffer.focus_selected_message()
# set new values in messagetree obj
if raw is not None:
@@ -927,7 +905,6 @@ class OpenAttachmentCommand(Command):
'help': '''up, down, [half]page up, [half]page down, first, last, \
parent, first reply, last reply, \
next sibling, previous sibling, next, previous, \
- next unfolded, previous unfolded, \
next NOTMUCH_QUERY, previous NOTMUCH_QUERY'''})])
class MoveFocusCommand(MoveCommand):
@@ -948,16 +925,16 @@ class MoveFocusCommand(MoveCommand):
tbuffer.focus_next()
elif self.movement == 'previous':
tbuffer.focus_prev()
- elif self.movement == 'next unfolded':
- tbuffer.focus_next_unfolded()
- elif self.movement == 'previous unfolded':
- tbuffer.focus_prev_unfolded()
elif self.movement.startswith('next '):
query = self.movement[5:].strip()
tbuffer.focus_next_matching(query)
elif self.movement.startswith('previous '):
query = self.movement[9:].strip()
tbuffer.focus_prev_matching(query)
+ elif self.movement.startswith('thread'):
+ tbuffer.focus_thread_widget()
+ elif self.movement.startswith('msg'):
+ tbuffer.focus_msg_widget()
else:
MoveCommand.apply(self, ui)
# TODO add 'next matching' if threadbuffer stores the original query
@@ -976,7 +953,7 @@ class ThreadSelectCommand(Command):
logging.info('open attachment')
await ui.apply_command(OpenAttachmentCommand(attachment))
else:
- await ui.apply_command(ChangeDisplaymodeCommand(visible='toggle'))
+ ui.current_buffer.focus_toggle()
RetagPromptCommand = registerCommand(MODE, 'retagprompt')(RetagPromptCommand)
diff --git a/alot/completion/command.py b/alot/completion/command.py
index 2ba6d541..a320bf93 100644
--- a/alot/completion/command.py
+++ b/alot/completion/command.py
@@ -191,8 +191,7 @@ class CommandCompleter(Completer):
# thread
elif self.mode == 'thread' and cmd == 'save':
res = self._pathcompleter.complete(params, localpos)
- elif self.mode == 'thread' and cmd in ['fold', 'unfold',
- 'togglesource',
+ elif self.mode == 'thread' and cmd in ['togglesource',
'toggleheaders']:
res = self._querycompleter.complete(params, localpos)
elif self.mode == 'thread' and cmd in ['tag', 'retag', 'untag',
@@ -207,8 +206,7 @@ class CommandCompleter(Completer):
if self.mode == 'thread':
directions += ['parent', 'first reply', 'last reply',
'next sibling', 'previous sibling',
- 'next', 'previous', 'next unfolded',
- 'previous unfolded']
+ 'next', 'previous' ]
localcompleter = StringlistCompleter(directions)
res = localcompleter.complete(params, localpos)
diff --git a/alot/defaults/default.bindings b/alot/defaults/default.bindings
index 24c90043..9d743d63 100644
--- a/alot/defaults/default.bindings
+++ b/alot/defaults/default.bindings
@@ -63,12 +63,6 @@ q = exit
[thread]
enter = select
- C = fold *
- E = unfold *
- c = fold
- e = unfold
- < = fold
- > = unfold
[ = indent -
] = indent +
'g f' = togglesource
diff --git a/alot/widgets/thread.py b/alot/widgets/thread.py
index 7c423383..0096188d 100644
--- a/alot/widgets/thread.py
+++ b/alot/widgets/thread.py
@@ -142,11 +142,9 @@ class MessageWidget(urwid.WidgetWrap):
"""
_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
@@ -154,45 +152,40 @@ class MessageWidget(urwid.WidgetWrap):
_QUOTE_CHARS = '>|:}#'
_QUOTE_REGEX = '(([ \t]*[{quote_chars}])+)'.format(quote_chars = _QUOTE_CHARS)
- def __init__(self, message, odd):
+ def __init__(self, message):
"""
:param message: Message to display
:type message: alot.db.Message
- :param odd: theme summary widget as if this is an odd line in the thread order
- :type odd: bool
"""
self._message = message
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()
- super().__init__(urwid.Pile([]))
+ super().__init__(urwid.ListBox(urwid.SimpleListWalker([])))
self.display_all_headers = False
self.display_source = False
- self.display_content = False
def get_message(self):
return self._message
- def _reassemble(self, display_content, display_source):
- widgets = [self._summary_wgt]
+ def _reassemble(self, display_source):
+ widgets = []
- if display_content:
- if display_source:
- widgets.append(self._source_wgt)
- else:
- widgets.append(self._headers_wgt)
+ if display_source:
+ widgets.append(self._source_wgt)
+ else:
+ widgets.append(self._headers_wgt)
- if self._attach_wgt is not None:
- widgets.append(self._attach_wgt)
+ if self._attach_wgt is not None:
+ widgets.append(self._attach_wgt)
- widgets.append(self._body_wgt)
+ widgets.append(self._body_wgt)
- self._w.contents = [(w, ('pack', None)) for w in widgets]
+ self._w.body[:] = widgets
def _get_source(self):
sourcetxt = self._message.get_email().as_string()
@@ -290,19 +283,6 @@ class MessageWidget(urwid.WidgetWrap):
self._display_all_headers = val
@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
-
- @property
def display_source(self):
return self._display_source
@display_source.setter
@@ -312,21 +292,9 @@ class MessageWidget(urwid.WidgetWrap):
if val == self._display_source:
return
- self._reassemble(self.display_content, val)
+ self._reassemble(val)
self._display_source = 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)
-
def get_selected_attachment(self):
"""
If an AttachmentWidget is currently focused, return it. Otherwise return
@@ -346,14 +314,14 @@ class ThreadNode(urwid.WidgetWrap):
_decor_text = None
_have_next_sibling = None
- def __init__(self, msg_wgt, thread, pos, indent):
- msg = msg_wgt.get_message()
+ def __init__(self, msg, thread, pos, indent):
+ msg_summary = MessageSummaryWidget(msg, pos & 1)
if msg.depth == 0:
- wgt = msg_wgt
+ wgt = msg_summary
else:
self._decor_text = urwid.Text('')
- wgt = urwid.Columns([('pack', self._decor_text), msg_wgt])
+ wgt = urwid.Columns([('pack', self._decor_text), msg_summary])
ancestor_chain = [msg]
for p in msg.parents():