Commit aca5b0f9 authored by Lysander Trischler's avatar Lysander Trischler

Implement live preview for composing twts

parent e4a9fe71
......@@ -75,14 +75,11 @@ looks like:
``tt``'s main view, the conversations.
.. figure:: reply.png
The reply form with the twt to reply to shown above as reference.
.. figure:: preview.png
The crude preview of the new twt to proofread.
.. figure:: compose.png
The reply form with the conversation to reply to shown above as reference.
The live preview on the bottom updates as you type to help you proofread
your twts before publishing them.
Features
========
......@@ -105,7 +102,7 @@ Features
* Open and copy links in URL view.
* Folding of mentions and twtxt subject hashes.
* Subject hashes can be hidden.
* Compose and preview new or reply twts.
* Compose and live preview new or reply twts.
TODO
====
......@@ -330,6 +327,8 @@ Default configuration::
subject-focus = black; dark red
link-normal = light blue,underline; black
link-focus = light blue,underline; white
error-normal = white,bold; dark red
error-focus = dark red,bold; white
The ``-normal`` suffix indicates a list entry which doesn't have a focus, the
``-focus`` suffix on the other hand will be applied, when the list entry is
......
......@@ -439,8 +439,10 @@ class URLListBox(widgets.VimListBox):
subprocess.run(["twtxt", "follow", token.nick, token.url])
class TwtForm(urwid.ListBox):
class TwtForm(urwid.ListBox, TwtFormatter):
def __init__(self, manager, original_twt=None, rendered_conversation=[]):
self._manager = manager
self._original_twt = original_twt
if original_twt:
authors = "".join(["@<%s %s> " % (a.nick, a.url)
for a in self._collect_authors(original_twt)])
......@@ -455,20 +457,22 @@ class TwtForm(urwid.ListBox):
microsecond=0)
.astimezone()
.isoformat())
preview = urwid.Button("Preview Twt", on_press=self.preview_twt)
preview_widgets, rows_calculating_delegate_widget = self._render_new_twt()
preview_list_walker = urwid.SimpleFocusListWalker(preview_widgets)
preview_box_adapter = widgets.DelegatingBoxAdapter(widgets.VimListBox(preview_list_walker),
rows_calculating_delegate_widget)
publish = urwid.Button("Publish Twt", on_press=self.publish_twt)
cancel = urwid.Button("Cancel", on_press=lambda *_: frame.body.pop_widget())
super().__init__(rendered_conversation + [
urwid.LineBox(self._text_edit, title="Reply" if original_twt else "New Twt", title_align="left"),
urwid.LineBox(self._created_at_edit, title="Created at", title_align="left"),
urwid.LineBox(widgets.Unselectable(preview_box_adapter),
title="Reply Preview" if original_twt else "New Twt Preview",
title_align="left"),
urwid.Columns([urwid.Text(""),
(len(preview.label) + 4, preview),
(len(publish.label) + 4, publish),
(len(cancel.label) + 4, cancel)],
dividechars=2)])
self._manager = manager
self._original_twt = original_twt
self._rendered_conversation = rendered_conversation
# Focus the text edit, which follows directly the conversation. This
# way, the user can immediately start typing. This scrolls down in the
......@@ -477,6 +481,40 @@ class TwtForm(urwid.ListBox):
# below should also be visible. :-(
self.focus_position = len(rendered_conversation)
def update_preview(*_):
preview_widgets, rows_calculating_delegate_widget = self._render_new_twt()
preview_box_adapter._delegate_widget = rows_calculating_delegate_widget
preview_list_walker[:] = preview_widgets
self._connected_signals = (
urwid.connect_signal(self._text_edit, "postchange", update_preview),
urwid.connect_signal(self._created_at_edit, "postchange", update_preview),
)
def _render_new_twt(self):
try:
created_at = self._created_at
except dateutil.parser.ParserError:
created_at = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
created_at_error = True
else:
created_at_error = False
# The Tweet constructor will fail for empty texts, so we need to guard
# against this.
new_twt = manager.enhance_twt(twtxt.models.Tweet(created_at=created_at,
text=self._text or " ",
source=manager.own_source))
preview_widgets = []
self.render_conversation(manager, new_twt, preview_widgets, [],
level=0 if self._original_twt is None else 1)
rows_calculating_delegate_widget = preview_widgets[-1].original_widget
if created_at_error:
created_at_text = rows_calculating_delegate_widget[0]
created_at_text.set_text(("error-normal", "Invalid timestamp"))
return preview_widgets, rows_calculating_delegate_widget
def _collect_authors(self, twt):
authors = []
......@@ -502,16 +540,16 @@ class TwtForm(urwid.ListBox):
return self._text_edit.edit_text
def preview_twt(self, *_):
new_twt = self._manager.enhance_twt(twtxt.models.Tweet(created_at=self._created_at, text=self._text, source=self._manager.own_source))
frame.body.push_widget(PreviewForm(self._manager, new_twt, self._original_twt, self._rendered_conversation))
def publish_twt(self, *_):
self._manager.publish_twt(self._created_at, self._text)
frame.body.pop_widget()
def close(self):
urwid.disconnect_signal(self._text_edit, "postchange", self._connected_signals[0])
urwid.disconnect_signal(self._created_at_edit, "postchange", self._connected_signals[1])
__debug = []
def debug(s):
__debug.append("%d: %s" % (len(__debug) + 1, s))
......@@ -519,28 +557,6 @@ def debug(s):
header.set_text("\n".join(__debug[-20:]))
class PreviewForm(urwid.ListBox, TwtFormatter):
def __init__(self, manager, new_twt, original_twt=None, rendered_conversation=[]):
back = urwid.Button("Back", on_press=lambda *_: frame.body.pop_widget())
rendered_widgets = []
twts = []
self.render_conversation(manager, new_twt, rendered_widgets, twts,
level=0 if original_twt is None else 1)
rows_calculating_delegate_widget = rendered_widgets[-1].original_widget
super().__init__(list(map(widgets.Unselectable, rendered_conversation)) + [
urwid.LineBox(widgets.DelegatingBoxAdapter(widgets.VimListBox(urwid.SimpleFocusListWalker(rendered_widgets)),
rows_calculating_delegate_widget),
title="Reply Preview" if original_twt else "New Twt Preview",
title_align="left"),
urwid.Columns([urwid.Text(""),
(len(back.label) + 4, back)],
dividechars=2)])
# Focus the back button which is after the rendered conversation and
# the preview.
self.focus_position = len(rendered_conversation) + 1
show_subjects = False
fold_subjects = True
fold_mentions = True
......@@ -554,7 +570,7 @@ def generate_header_text():
search_history = ["subject"]
cmdline = widgets.CommandLine()
frame = urwid.Frame(body=widgets.Stack([TwtsListBox(manager, cmdline)]),
frame = urwid.Frame(body=widgets.ClosingStack([TwtsListBox(manager, cmdline)]),
header=urwid.AttrMap(urwid.Text(generate_header_text()), "header"),
footer=urwid.Pile([urwid.AttrMap(urwid.Text("q: quit r: toggle read u: urls n: next unread N: prev unread J: next not missing unread K: prev not missing unread"), "footer")]))
......
......@@ -74,6 +74,8 @@ class Renderer:
entry("subject-focus", "black", "dark red"),
entry("link-normal", "light blue,underline", "black"),
entry("link-focus", "light blue,underline", "white"),
entry("error-normal", "white,bold", "dark red"),
entry("error-focus", "dark red,bold", "white")
]
self.unread_normal_map = {None: "unread-normal"}
......@@ -83,6 +85,7 @@ class Renderer:
"mentioned-nick-normal": "mentioned-nick-focus",
"subject-normal": "subject-focus",
"link-normal": "link-focus",
"error-normal": "error-focus",
}
self._nicks = set()
for nick, colors in config.nick_colors:
......
......@@ -402,6 +402,19 @@ class Stack(urwid.Widget):
return self.current_widget.mouse_event(size, event, button, col, row, focus)
class ClosingStack(Stack):
"""
Stack widget that calls `close` methods on widgets, when removing them from
the stack using `pop_widget`.
"""
def pop_widget(self):
widget = super().pop_widget()
if hasattr(widget, "close"):
widget.close()
return widget
class PrefixText(urwid.Text):
"""
Text whith a prefix rendered in front of each line. The prefix in the first
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment