mirror of
https://github.com/ihabunek/toot.git
synced 2024-11-03 04:17:21 -05:00
Status detail scrollbar feature
Uses scroll.py from https://github.com/rndusr/stig
This commit is contained in:
parent
c5b3724015
commit
91c1b792be
2
.flake8
2
.flake8
@ -1,4 +1,4 @@
|
|||||||
[flake8]
|
[flake8]
|
||||||
exclude=build,tests,tmp
|
exclude=build,tests,tmp,toot/tui/scroll.py
|
||||||
ignore=E128
|
ignore=E128
|
||||||
max-line-length=120
|
max-line-length=120
|
||||||
|
426
toot/tui/scroll.py
Normal file
426
toot/tui/scroll.py
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
# scroll.py
|
||||||
|
#
|
||||||
|
# Copied from the stig project by rndusr@github
|
||||||
|
# https://github.com/rndusr/stig
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details
|
||||||
|
# http://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
import urwid
|
||||||
|
from urwid.widget import BOX, FIXED, FLOW
|
||||||
|
|
||||||
|
# Scroll actions
|
||||||
|
SCROLL_LINE_UP = 'line up'
|
||||||
|
SCROLL_LINE_DOWN = 'line down'
|
||||||
|
SCROLL_PAGE_UP = 'page up'
|
||||||
|
SCROLL_PAGE_DOWN = 'page down'
|
||||||
|
SCROLL_TO_TOP = 'to top'
|
||||||
|
SCROLL_TO_END = 'to end'
|
||||||
|
|
||||||
|
# Scrollbar positions
|
||||||
|
SCROLLBAR_LEFT = 'left'
|
||||||
|
SCROLLBAR_RIGHT = 'right'
|
||||||
|
|
||||||
|
class Scrollable(urwid.WidgetDecoration):
|
||||||
|
def sizing(self):
|
||||||
|
return frozenset([BOX,])
|
||||||
|
|
||||||
|
def selectable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __init__(self, widget):
|
||||||
|
"""Box widget that makes a fixed or flow widget vertically scrollable
|
||||||
|
|
||||||
|
TODO: Focusable widgets are handled, including switching focus, but
|
||||||
|
possibly not intuitively, depending on the arrangement of widgets. When
|
||||||
|
switching focus to a widget that is outside of the visible part of the
|
||||||
|
original widget, the canvas scrolls up/down to the focused widget. It
|
||||||
|
would be better to scroll until the next focusable widget is in sight
|
||||||
|
first. But for that to work we must somehow obtain a list of focusable
|
||||||
|
rows in the original canvas.
|
||||||
|
"""
|
||||||
|
if not any(s in widget.sizing() for s in (FIXED, FLOW)):
|
||||||
|
raise ValueError('Not a fixed or flow widget: %r' % widget)
|
||||||
|
self._trim_top = 0
|
||||||
|
self._scroll_action = None
|
||||||
|
self._forward_keypress = None
|
||||||
|
self._old_cursor_coords = None
|
||||||
|
self._rows_max_cached = 0
|
||||||
|
self.__super.__init__(widget)
|
||||||
|
|
||||||
|
def render(self, size, focus=False):
|
||||||
|
maxcol, maxrow = size
|
||||||
|
|
||||||
|
# Render complete original widget
|
||||||
|
ow = self._original_widget
|
||||||
|
ow_size = self._get_original_widget_size(size)
|
||||||
|
canv_full = ow.render(ow_size, focus)
|
||||||
|
|
||||||
|
# Make full canvas editable
|
||||||
|
canv = urwid.CompositeCanvas(canv_full)
|
||||||
|
canv_cols, canv_rows = canv.cols(), canv.rows()
|
||||||
|
|
||||||
|
if canv_cols <= maxcol:
|
||||||
|
pad_width = maxcol - canv_cols
|
||||||
|
if pad_width > 0:
|
||||||
|
# Canvas is narrower than available horizontal space
|
||||||
|
canv.pad_trim_left_right(0, pad_width)
|
||||||
|
|
||||||
|
if canv_rows <= maxrow:
|
||||||
|
fill_height = maxrow - canv_rows
|
||||||
|
if fill_height > 0:
|
||||||
|
# Canvas is lower than available vertical space
|
||||||
|
canv.pad_trim_top_bottom(0, fill_height)
|
||||||
|
|
||||||
|
if canv_cols <= maxcol and canv_rows <= maxrow:
|
||||||
|
# Canvas is small enough to fit without trimming
|
||||||
|
return canv
|
||||||
|
|
||||||
|
self._adjust_trim_top(canv, size)
|
||||||
|
|
||||||
|
# Trim canvas if necessary
|
||||||
|
trim_top = self._trim_top
|
||||||
|
trim_end = canv_rows - maxrow - trim_top
|
||||||
|
trim_right = canv_cols - maxcol
|
||||||
|
if trim_top > 0:
|
||||||
|
canv.trim(trim_top)
|
||||||
|
if trim_end > 0:
|
||||||
|
canv.trim_end(trim_end)
|
||||||
|
if trim_right > 0:
|
||||||
|
canv.pad_trim_left_right(0, -trim_right)
|
||||||
|
|
||||||
|
# Disable cursor display if cursor is outside of visible canvas parts
|
||||||
|
if canv.cursor is not None:
|
||||||
|
curscol, cursrow = canv.cursor
|
||||||
|
if cursrow >= maxrow or cursrow < 0:
|
||||||
|
canv.cursor = None
|
||||||
|
|
||||||
|
# Figure out whether we should forward keypresses to original widget
|
||||||
|
if canv.cursor is not None:
|
||||||
|
# Trimmed canvas contains the cursor, e.g. in an Edit widget
|
||||||
|
self._forward_keypress = True
|
||||||
|
else:
|
||||||
|
if canv_full.cursor is not None:
|
||||||
|
# Full canvas contains the cursor, but scrolled out of view
|
||||||
|
self._forward_keypress = False
|
||||||
|
else:
|
||||||
|
# Original widget does not have a cursor, but may be selectable
|
||||||
|
|
||||||
|
# FIXME: Using ow.selectable() is bad because the original
|
||||||
|
# widget may be selectable because it's a container widget with
|
||||||
|
# a key-grabbing widget that is scrolled out of view.
|
||||||
|
# ow.selectable() returns True anyway because it doesn't know
|
||||||
|
# how we trimmed our canvas.
|
||||||
|
#
|
||||||
|
# To fix this, we need to resolve ow.focus and somehow
|
||||||
|
# ask canv whether it contains bits of the focused widget. I
|
||||||
|
# can't see a way to do that.
|
||||||
|
if ow.selectable():
|
||||||
|
self._forward_keypress = True
|
||||||
|
else:
|
||||||
|
self._forward_keypress = False
|
||||||
|
|
||||||
|
return canv
|
||||||
|
|
||||||
|
def keypress(self, size, key):
|
||||||
|
# Maybe offer key to original widget
|
||||||
|
if self._forward_keypress:
|
||||||
|
ow = self._original_widget
|
||||||
|
ow_size = self._get_original_widget_size(size)
|
||||||
|
|
||||||
|
# Remember previous cursor position if possible
|
||||||
|
if hasattr(ow, 'get_cursor_coords'):
|
||||||
|
self._old_cursor_coords = ow.get_cursor_coords(ow_size)
|
||||||
|
|
||||||
|
key = ow.keypress(ow_size, key)
|
||||||
|
if key is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Handle up/down, page up/down, etc
|
||||||
|
command_map = self._command_map
|
||||||
|
if command_map[key] == urwid.CURSOR_UP:
|
||||||
|
self._scroll_action = SCROLL_LINE_UP
|
||||||
|
elif command_map[key] == urwid.CURSOR_DOWN:
|
||||||
|
self._scroll_action = SCROLL_LINE_DOWN
|
||||||
|
|
||||||
|
elif command_map[key] == urwid.CURSOR_PAGE_UP:
|
||||||
|
self._scroll_action = SCROLL_PAGE_UP
|
||||||
|
elif command_map[key] == urwid.CURSOR_PAGE_DOWN:
|
||||||
|
self._scroll_action = SCROLL_PAGE_DOWN
|
||||||
|
|
||||||
|
elif command_map[key] == urwid.CURSOR_MAX_LEFT: # 'home'
|
||||||
|
self._scroll_action = SCROLL_TO_TOP
|
||||||
|
elif command_map[key] == urwid.CURSOR_MAX_RIGHT: # 'end'
|
||||||
|
self._scroll_action = SCROLL_TO_END
|
||||||
|
|
||||||
|
else:
|
||||||
|
return key
|
||||||
|
|
||||||
|
self._invalidate()
|
||||||
|
|
||||||
|
def mouse_event(self, size, event, button, col, row, focus):
|
||||||
|
ow = self._original_widget
|
||||||
|
if hasattr(ow, 'mouse_event'):
|
||||||
|
ow_size = self._get_original_widget_size(size)
|
||||||
|
row += self._trim_top
|
||||||
|
return ow.mouse_event(ow_size, event, button, col, row, focus)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _adjust_trim_top(self, canv, size):
|
||||||
|
"""Adjust self._trim_top according to self._scroll_action"""
|
||||||
|
action = self._scroll_action
|
||||||
|
self._scroll_action = None
|
||||||
|
|
||||||
|
maxcol, maxrow = size
|
||||||
|
trim_top = self._trim_top
|
||||||
|
canv_rows = canv.rows()
|
||||||
|
|
||||||
|
if trim_top < 0:
|
||||||
|
# Negative trim_top values use bottom of canvas as reference
|
||||||
|
trim_top = canv_rows - maxrow + trim_top + 1
|
||||||
|
|
||||||
|
if canv_rows <= maxrow:
|
||||||
|
self._trim_top = 0 # Reset scroll position
|
||||||
|
return
|
||||||
|
|
||||||
|
def ensure_bounds(new_trim_top):
|
||||||
|
return max(0, min(canv_rows - maxrow, new_trim_top))
|
||||||
|
|
||||||
|
if action == SCROLL_LINE_UP:
|
||||||
|
self._trim_top = ensure_bounds(trim_top - 1)
|
||||||
|
elif action == SCROLL_LINE_DOWN:
|
||||||
|
self._trim_top = ensure_bounds(trim_top + 1)
|
||||||
|
|
||||||
|
elif action == SCROLL_PAGE_UP:
|
||||||
|
self._trim_top = ensure_bounds(trim_top - maxrow + 1)
|
||||||
|
elif action == SCROLL_PAGE_DOWN:
|
||||||
|
self._trim_top = ensure_bounds(trim_top + maxrow - 1)
|
||||||
|
|
||||||
|
elif action == SCROLL_TO_TOP:
|
||||||
|
self._trim_top = 0
|
||||||
|
elif action == SCROLL_TO_END:
|
||||||
|
self._trim_top = canv_rows - maxrow
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._trim_top = ensure_bounds(trim_top)
|
||||||
|
|
||||||
|
# If the cursor was moved by the most recent keypress, adjust trim_top
|
||||||
|
# so that the new cursor position is within the displayed canvas part.
|
||||||
|
# But don't do this if the cursor is at the top/bottom edge so we can still scroll out
|
||||||
|
if self._old_cursor_coords is not None and self._old_cursor_coords != canv.cursor:
|
||||||
|
self._old_cursor_coords = None
|
||||||
|
curscol, cursrow = canv.cursor
|
||||||
|
if cursrow < self._trim_top:
|
||||||
|
self._trim_top = cursrow
|
||||||
|
elif cursrow >= self._trim_top + maxrow:
|
||||||
|
self._trim_top = max(0, cursrow - maxrow + 1)
|
||||||
|
|
||||||
|
def _get_original_widget_size(self, size):
|
||||||
|
ow = self._original_widget
|
||||||
|
sizing = ow.sizing()
|
||||||
|
if FIXED in sizing:
|
||||||
|
return ()
|
||||||
|
elif FLOW in sizing:
|
||||||
|
return (size[0],)
|
||||||
|
|
||||||
|
def get_scrollpos(self, size=None, focus=False):
|
||||||
|
"""Current scrolling position
|
||||||
|
|
||||||
|
Lower limit is 0, upper limit is the maximum number of rows with the
|
||||||
|
given maxcol minus maxrow.
|
||||||
|
|
||||||
|
NOTE: The returned value may be too low or too high if the position has
|
||||||
|
changed but the widget wasn't rendered yet.
|
||||||
|
"""
|
||||||
|
return self._trim_top
|
||||||
|
|
||||||
|
def set_scrollpos(self, position):
|
||||||
|
"""Set scrolling position
|
||||||
|
|
||||||
|
If `position` is positive it is interpreted as lines from the top.
|
||||||
|
If `position` is negative it is interpreted as lines from the bottom.
|
||||||
|
|
||||||
|
Values that are too high or too low values are automatically adjusted
|
||||||
|
during rendering.
|
||||||
|
"""
|
||||||
|
self._trim_top = int(position)
|
||||||
|
self._invalidate()
|
||||||
|
|
||||||
|
def rows_max(self, size=None, focus=False):
|
||||||
|
"""Return the number of rows for `size`
|
||||||
|
|
||||||
|
If `size` is not given, the currently rendered number of rows is returned.
|
||||||
|
"""
|
||||||
|
if size is not None:
|
||||||
|
ow = self._original_widget
|
||||||
|
ow_size = self._get_original_widget_size(size)
|
||||||
|
sizing = ow.sizing()
|
||||||
|
if FIXED in sizing:
|
||||||
|
self._rows_max_cached = ow.pack(ow_size, focus)[1]
|
||||||
|
elif FLOW in sizing:
|
||||||
|
self._rows_max_cached = ow.rows(ow_size, focus)
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Not a flow/box widget: %r' % self._original_widget)
|
||||||
|
return self._rows_max_cached
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollBar(urwid.WidgetDecoration):
|
||||||
|
def sizing(self):
|
||||||
|
return frozenset((BOX,))
|
||||||
|
|
||||||
|
def selectable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __init__(self, widget, thumb_char=u'\u2588', trough_char=' ',
|
||||||
|
side=SCROLLBAR_RIGHT, width=1):
|
||||||
|
"""Box widget that adds a scrollbar to `widget`
|
||||||
|
|
||||||
|
`widget` must be a box widget with the following methods:
|
||||||
|
- `get_scrollpos` takes the arguments `size` and `focus` and returns
|
||||||
|
the index of the first visible row.
|
||||||
|
- `set_scrollpos` (optional; needed for mouse click support) takes the
|
||||||
|
index of the first visible row.
|
||||||
|
- `rows_max` takes `size` and `focus` and returns the total number of
|
||||||
|
rows `widget` can render.
|
||||||
|
|
||||||
|
`thumb_char` is the character used for the scrollbar handle.
|
||||||
|
`trough_char` is used for the space above and below the handle.
|
||||||
|
`side` must be 'left' or 'right'.
|
||||||
|
`width` specifies the number of columns the scrollbar uses.
|
||||||
|
"""
|
||||||
|
if BOX not in widget.sizing():
|
||||||
|
raise ValueError('Not a box widget: %r' % widget)
|
||||||
|
self.__super.__init__(widget)
|
||||||
|
self._thumb_char = thumb_char
|
||||||
|
self._trough_char = trough_char
|
||||||
|
self.scrollbar_side = side
|
||||||
|
self.scrollbar_width = max(1, width)
|
||||||
|
self._original_widget_size = (0, 0)
|
||||||
|
|
||||||
|
def render(self, size, focus=False):
|
||||||
|
maxcol, maxrow = size
|
||||||
|
|
||||||
|
sb_width = self._scrollbar_width
|
||||||
|
ow_size = (max(0, maxcol - sb_width), maxrow)
|
||||||
|
sb_width = maxcol - ow_size[0]
|
||||||
|
|
||||||
|
ow = self._original_widget
|
||||||
|
ow_base = self.scrolling_base_widget
|
||||||
|
ow_rows_max = ow_base.rows_max(size, focus)
|
||||||
|
if ow_rows_max <= maxrow:
|
||||||
|
# Canvas fits without scrolling - no scrollbar needed
|
||||||
|
self._original_widget_size = size
|
||||||
|
return ow.render(size, focus)
|
||||||
|
ow_rows_max = ow_base.rows_max(ow_size, focus)
|
||||||
|
|
||||||
|
ow_canv = ow.render(ow_size, focus)
|
||||||
|
self._original_widget_size = ow_size
|
||||||
|
|
||||||
|
pos = ow_base.get_scrollpos(ow_size, focus)
|
||||||
|
posmax = ow_rows_max - maxrow
|
||||||
|
|
||||||
|
# Thumb shrinks/grows according to the ratio of
|
||||||
|
# <number of visible lines> / <number of total lines>
|
||||||
|
thumb_weight = min(1, maxrow / max(1, ow_rows_max))
|
||||||
|
thumb_height = max(1, round(thumb_weight * maxrow))
|
||||||
|
|
||||||
|
# Thumb may only touch top/bottom if the first/last row is visible
|
||||||
|
top_weight = float(pos) / max(1, posmax)
|
||||||
|
top_height = int((maxrow - thumb_height) * top_weight)
|
||||||
|
if top_height == 0 and top_weight > 0:
|
||||||
|
top_height = 1
|
||||||
|
|
||||||
|
# Bottom part is remaining space
|
||||||
|
bottom_height = maxrow - thumb_height - top_height
|
||||||
|
assert thumb_height + top_height + bottom_height == maxrow
|
||||||
|
|
||||||
|
# Create scrollbar canvas
|
||||||
|
# Creating SolidCanvases of correct height may result in "cviews do not
|
||||||
|
# fill gaps in shard_tail!" or "cviews overflow gaps in shard_tail!"
|
||||||
|
# exceptions. Stacking the same SolidCanvas is a workaround.
|
||||||
|
# https://github.com/urwid/urwid/issues/226#issuecomment-437176837
|
||||||
|
top = urwid.SolidCanvas(self._trough_char, sb_width, 1)
|
||||||
|
thumb = urwid.SolidCanvas(self._thumb_char, sb_width, 1)
|
||||||
|
bottom = urwid.SolidCanvas(self._trough_char, sb_width, 1)
|
||||||
|
sb_canv = urwid.CanvasCombine(
|
||||||
|
[(top, None, False)] * top_height +
|
||||||
|
[(thumb, None, False)] * thumb_height +
|
||||||
|
[(bottom, None, False)] * bottom_height,
|
||||||
|
)
|
||||||
|
|
||||||
|
combinelist = [(ow_canv, None, True, ow_size[0]),
|
||||||
|
(sb_canv, None, False, sb_width)]
|
||||||
|
if self._scrollbar_side != SCROLLBAR_LEFT:
|
||||||
|
return urwid.CanvasJoin(combinelist)
|
||||||
|
else:
|
||||||
|
return urwid.CanvasJoin(reversed(combinelist))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scrollbar_width(self):
|
||||||
|
"""Columns the scrollbar uses"""
|
||||||
|
return max(1, self._scrollbar_width)
|
||||||
|
|
||||||
|
@scrollbar_width.setter
|
||||||
|
def scrollbar_width(self, width):
|
||||||
|
self._scrollbar_width = max(1, int(width))
|
||||||
|
self._invalidate()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scrollbar_side(self):
|
||||||
|
"""Where to display the scrollbar; must be 'left' or 'right'"""
|
||||||
|
return self._scrollbar_side
|
||||||
|
|
||||||
|
@scrollbar_side.setter
|
||||||
|
def scrollbar_side(self, side):
|
||||||
|
if side not in (SCROLLBAR_LEFT, SCROLLBAR_RIGHT):
|
||||||
|
raise ValueError('scrollbar_side must be "left" or "right", not %r' % side)
|
||||||
|
self._scrollbar_side = side
|
||||||
|
self._invalidate()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scrolling_base_widget(self):
|
||||||
|
"""Nearest `original_widget` that is compatible with the scrolling API"""
|
||||||
|
def orig_iter(w):
|
||||||
|
while hasattr(w, 'original_widget'):
|
||||||
|
w = w.original_widget
|
||||||
|
yield w
|
||||||
|
yield w
|
||||||
|
|
||||||
|
def is_scrolling_widget(w):
|
||||||
|
return hasattr(w, 'get_scrollpos') and hasattr(w, 'rows_max')
|
||||||
|
|
||||||
|
for w in orig_iter(self):
|
||||||
|
if is_scrolling_widget(w):
|
||||||
|
return w
|
||||||
|
raise ValueError('Not compatible to be wrapped by ScrollBar: %r' % w)
|
||||||
|
|
||||||
|
def keypress(self, size, key):
|
||||||
|
return self._original_widget.keypress(self._original_widget_size, key)
|
||||||
|
|
||||||
|
def mouse_event(self, size, event, button, col, row, focus):
|
||||||
|
ow = self._original_widget
|
||||||
|
ow_size = self._original_widget_size
|
||||||
|
handled = False
|
||||||
|
if hasattr(ow, 'mouse_event'):
|
||||||
|
handled = ow.mouse_event(ow_size, event, button, col, row, focus)
|
||||||
|
|
||||||
|
if not handled and hasattr(ow, 'set_scrollpos'):
|
||||||
|
if button == 4: # scroll wheel up
|
||||||
|
pos = ow.get_scrollpos(ow_size)
|
||||||
|
ow.set_scrollpos(pos - 1)
|
||||||
|
return True
|
||||||
|
elif button == 5: # scroll wheel down
|
||||||
|
pos = ow.get_scrollpos(ow_size)
|
||||||
|
ow.set_scrollpos(pos + 1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
@ -7,6 +7,7 @@ from toot.utils.language import language_name
|
|||||||
|
|
||||||
from .utils import highlight_hashtags, parse_datetime, highlight_keys
|
from .utils import highlight_hashtags, parse_datetime, highlight_keys
|
||||||
from .widgets import SelectableText, SelectableColumns
|
from .widgets import SelectableText, SelectableColumns
|
||||||
|
from toot.tui.scroll import Scrollable, ScrollBar
|
||||||
|
|
||||||
logger = logging.getLogger("toot")
|
logger = logging.getLogger("toot")
|
||||||
|
|
||||||
@ -43,10 +44,32 @@ class Timeline(urwid.Columns):
|
|||||||
self.can_translate = can_translate
|
self.can_translate = can_translate
|
||||||
self.status_list = self.build_status_list(statuses, focus=focus)
|
self.status_list = self.build_status_list(statuses, focus=focus)
|
||||||
self.followed_tags = followed_tags
|
self.followed_tags = followed_tags
|
||||||
|
opts_footer = urwid.Text(self.get_option_text(statuses[focus]))
|
||||||
try:
|
try:
|
||||||
self.status_details = StatusDetails(statuses[focus], is_thread, can_translate, followed_tags)
|
self.status_details = urwid.Frame(
|
||||||
|
body=ScrollBar(
|
||||||
|
Scrollable(
|
||||||
|
urwid.Padding(
|
||||||
|
StatusDetails(
|
||||||
|
statuses[focus],
|
||||||
|
is_thread,
|
||||||
|
can_translate,
|
||||||
|
followed_tags,
|
||||||
|
),
|
||||||
|
right=1,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
thumb_char="\u2588",
|
||||||
|
trough_char="\u2591",
|
||||||
|
),
|
||||||
|
footer=opts_footer,
|
||||||
|
)
|
||||||
|
|
||||||
except IndexError:
|
except IndexError:
|
||||||
self.status_details = StatusDetails(None, is_thread, can_translate, followed_tags)
|
# we have no statuses to display
|
||||||
|
self.status_details = StatusDetails(
|
||||||
|
None, is_thread, can_translate, followed_tags
|
||||||
|
)
|
||||||
|
|
||||||
super().__init__([
|
super().__init__([
|
||||||
("weight", 40, self.status_list),
|
("weight", 40, self.status_list),
|
||||||
@ -74,6 +97,25 @@ class Timeline(urwid.Columns):
|
|||||||
None: "green_selected",
|
None: "green_selected",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def get_option_text(self, status):
|
||||||
|
options = [
|
||||||
|
"[B]oost",
|
||||||
|
"[D]elete" if status.is_mine else "",
|
||||||
|
"B[o]okmark",
|
||||||
|
"[F]avourite",
|
||||||
|
"[V]iew",
|
||||||
|
"[T]hread" if not self.is_thread else "",
|
||||||
|
"[L]inks",
|
||||||
|
"[R]eply",
|
||||||
|
"So[u]rce",
|
||||||
|
"[Z]oom",
|
||||||
|
"Tra[n]slate" if self.can_translate else "",
|
||||||
|
"[H]elp",
|
||||||
|
]
|
||||||
|
options = "\n" + " ".join(o for o in options if o)
|
||||||
|
options = highlight_keys(options, "white_bold", "cyan")
|
||||||
|
return options
|
||||||
|
|
||||||
def get_focused_status(self):
|
def get_focused_status(self):
|
||||||
try:
|
try:
|
||||||
return self.statuses[self.status_list.body.focus]
|
return self.statuses[self.status_list.body.focus]
|
||||||
@ -104,8 +146,23 @@ class Timeline(urwid.Columns):
|
|||||||
self.draw_status_details(status)
|
self.draw_status_details(status)
|
||||||
|
|
||||||
def draw_status_details(self, status):
|
def draw_status_details(self, status):
|
||||||
self.status_details = StatusDetails(status, self.is_thread, self.can_translate, self.followed_tags)
|
opts_footer = urwid.Text(self.get_option_text(status))
|
||||||
self.contents[2] = urwid.Padding(self.status_details, left=1), ("weight", 60, False)
|
self.status_details = StatusDetails(
|
||||||
|
status, self.is_thread, self.can_translate, self.followed_tags
|
||||||
|
)
|
||||||
|
self.contents[2] = (
|
||||||
|
urwid.Padding(
|
||||||
|
urwid.Frame(
|
||||||
|
body=ScrollBar(
|
||||||
|
Scrollable(urwid.Padding(self.status_details, right=1)),
|
||||||
|
thumb_char="\u2588",
|
||||||
|
trough_char="\u2591",
|
||||||
|
),
|
||||||
|
footer=opts_footer,
|
||||||
|
),
|
||||||
|
left=1,
|
||||||
|
)
|
||||||
|
), ("weight", 60, False)
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
status = self.get_focused_status()
|
status = self.get_focused_status()
|
||||||
@ -339,26 +396,7 @@ class StatusDetails(urwid.Pile):
|
|||||||
]))
|
]))
|
||||||
|
|
||||||
# Push things to bottom
|
# Push things to bottom
|
||||||
yield ("weight", 1, urwid.SolidFill(" "))
|
yield ("weight", 1, urwid.BoxAdapter(urwid.SolidFill(" "), 1))
|
||||||
|
|
||||||
options = [
|
|
||||||
"[B]oost",
|
|
||||||
"[D]elete" if status.is_mine else "",
|
|
||||||
"[F]avourite",
|
|
||||||
"B[o]okmark",
|
|
||||||
"[V]iew",
|
|
||||||
"[T]hread" if not self.in_thread else "",
|
|
||||||
"[L]inks",
|
|
||||||
"[R]eply",
|
|
||||||
"So[u]rce",
|
|
||||||
"[Z]oom",
|
|
||||||
"Tra[n]slate" if self.can_translate else "",
|
|
||||||
"[H]elp",
|
|
||||||
]
|
|
||||||
options = " ".join(o for o in options if o)
|
|
||||||
|
|
||||||
options = highlight_keys(options, "white_bold", "cyan")
|
|
||||||
yield ("pack", urwid.Text(options))
|
|
||||||
|
|
||||||
def build_linebox(self, contents):
|
def build_linebox(self, contents):
|
||||||
contents = urwid.Pile(list(contents))
|
contents = urwid.Pile(list(contents))
|
||||||
|
Loading…
Reference in New Issue
Block a user