问题
Is there a way to show a user that a urwid listbox has additional items above / below the dispalyed section?
I'm thinking of something like a scrollbar that gives an idea of the number of entries.
Or a separate bar at the top / bottom of the list box.
If this behavior can not be implemented, what approaches are there to achieve this notification?
During my research, I found this question, which tried to achieve eventually the same. The given answer seems to check if all elements are visible. Unfortunately, this loses its functionality if some elements are hidden at any time because the terminal is not resized.
回答1:
I think I've found an implementation for the second visualization concept (bars at the top and bottom of the list box).
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import urwid
ENTRIES = [letter for letter in "abcdefghijklmnopqrstuvwxyz"]
PALETTE = [
("notifier_active", "dark cyan", "light gray"),
("notifier_inactive", "black", "dark gray"),
("reveal_focus", "black", "dark cyan", "standout")
]
class MyListBox(urwid.ListBox):
def __init__(self, body, on_focus_change=None):
super().__init__(body)
self.on_focus_change = on_focus_change
# Overriden
def change_focus(self, size, position, offset_inset=0, coming_from=None, cursor_coords=None, snap_rows=None):
super().change_focus(size,
position,
offset_inset,
coming_from,
cursor_coords,
snap_rows)
# Implement a hook to be able to deposit additional logic
if self.on_focus_change != None:
self.on_focus_change(size,
position,
offset_inset,
coming_from,
cursor_coords,
snap_rows)
class App(object):
def __init__(self, entries):
# Get terminal dimensions
terminal_cols, terminal_rows = urwid.raw_display.Screen().get_cols_rows()
list_rows = (terminal_rows - 2) if (terminal_rows > 7) else 5
# (available_rows - notifier_rows) OR my preferred minimum size
# At the beginning, "top" is always visible
self.notifier_top = urwid.AttrMap(urwid.Text('^', align="center"),
"notifier_inactive")
# Determine presentation depending on size and number of elements
self.notifier_bottom = urwid.AttrMap(urwid.Text('v', align="center"),
"notifier_inactive" if (len(entries) <= list_rows) else "notifier_active")
contents = [urwid.AttrMap(urwid.Button(entry), "", "reveal_focus")
for entry in entries]
self.listbox = MyListBox(urwid.SimpleFocusListWalker(contents),
self.update_notifiers) # Pass the hook
master_pile = urwid.Pile([
self.notifier_top,
urwid.BoxAdapter(self.listbox, list_rows),
self.notifier_bottom,
])
widget = urwid.Filler(master_pile,
'top')
self.loop = urwid.MainLoop(widget,
PALETTE,
unhandled_input=self.handle_input)
# Implementation for hook
def update_notifiers(self, size, position, offset_inset, coming_from, cursor_coords, snap_rows):
# which ends are visible? returns "top", "bottom", both or neither.
result = self.listbox.ends_visible(size)
if ("top" in result) and ("bottom" in result):
self.notifier_top.set_attr_map({None:"notifier_inactive"})
self.notifier_bottom.set_attr_map({None:"notifier_inactive"})
elif "top" in result:
self.notifier_top.set_attr_map({None:"notifier_inactive"})
self.notifier_bottom.set_attr_map({None:"notifier_active"})
elif "bottom" in result:
self.notifier_top.set_attr_map({None:"notifier_active"})
self.notifier_bottom.set_attr_map({None:"notifier_inactive"})
else:
self.notifier_top.set_attr_map({None:"notifier_active"})
self.notifier_bottom.set_attr_map({None:"notifier_active"})
def handle_input(self, key):
if key in ('q', 'Q', 'esc'):
self.exit()
def start(self):
self.loop.run()
def exit(self):
raise urwid.ExitMainLoop()
if __name__ == '__main__':
app = App(ENTRIES)
app.start()
Basically, I create a subclass of urwid.Listbox
and override its change_focus()
method to add a hook. Obviously, this method is called internally when the focus changes.
The actual logic uses the result of the ends_visible()
method, which returns the currently visible ends of the listbox (top, bottom, both or neither). Depending on that, I modify the presentation of the two surrounding urwid.Text
elements.
The code generates the following TUI:
I have also written a variant of the code, which is based on the original specification:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import urwid
HEADERS = ["column 1",
"column 2",
"column 3",
"column 4"]
ENTRIES = [["{}1".format(letter),
"{}2".format(letter),
"{}3".format(letter),
"{}4".format(letter)] for letter in "abcdefghijklmnopqrstuvwxyz"]
PALETTE = [
("column_headers", "white, bold", ""),
("notifier_active", "dark cyan", "light gray"),
("notifier_inactive", "black", "dark gray"),
("reveal_focus", "black", "dark cyan", "standout")
]
class SelectableRow(urwid.WidgetWrap):
def __init__(self, contents, on_select=None):
self.contents = contents
self.on_select = on_select
self._columns = urwid.Columns([urwid.Text(c) for c in contents])
self._focusable_columns = urwid.AttrMap(self._columns, '', 'reveal_focus')
super(SelectableRow, self).__init__(self._focusable_columns)
def selectable(self):
return True
def update_contents(self, contents):
# update the list record inplace...
self.contents[:] = contents
# ... and update the displayed items
for t, (w, _) in zip(contents, self._columns.contents):
w.set_text(t)
def keypress(self, size, key):
if self.on_select and key in ('enter',):
self.on_select(self)
return key
def __repr__(self):
return '%s(contents=%r)' % (self.__class__.__name__, self.contents)
class MyListBox(urwid.ListBox):
def __init__(self, body, on_focus_change=None):
super().__init__(body)
self.on_focus_change = on_focus_change
# Overriden
def change_focus(self, size, position, offset_inset=0, coming_from=None, cursor_coords=None, snap_rows=None):
super().change_focus(size,
position,
offset_inset,
coming_from,
cursor_coords,
snap_rows)
# Implement a hook to be able to deposit additional logic
if self.on_focus_change != None:
self.on_focus_change(size,
position,
offset_inset,
coming_from,
cursor_coords,
snap_rows)
class App(object):
def __init__(self, entries):
# Get terminal dimensions
terminal_cols, terminal_rows = urwid.raw_display.Screen().get_cols_rows()
list_rows = (terminal_rows - 6) if (terminal_rows > 11) else 5
# (available_rows - divider_rows - column_headers_row - notifier_rows) OR my preferred minimum size
column_headers = urwid.AttrMap(urwid.Columns([urwid.Text(c) for c in HEADERS]),
"column_headers")
# At the beginning, "top" is always visible
self.notifier_top = urwid.AttrMap(urwid.Text('^', align="center"),
"notifier_inactive")
# Determine presentation depending on size and number of elements
self.notifier_bottom = urwid.AttrMap(urwid.Text('v', align="center"),
"notifier_inactive" if (len(entries) <= list_rows) else "notifier_active")
contents = [SelectableRow(entry) for entry in entries]
self.listbox = MyListBox(urwid.SimpleFocusListWalker(contents),
self.update_notifiers) # Pass the hook
master_pile = urwid.Pile([
urwid.Divider(u'─'),
column_headers,
urwid.Divider(u'─'),
self.notifier_top,
urwid.BoxAdapter(self.listbox, list_rows),
self.notifier_bottom,
urwid.Divider(u'─'),
])
widget = urwid.Filler(master_pile,
'top')
self.loop = urwid.MainLoop(widget,
PALETTE,
unhandled_input=self.handle_input)
# Implementation for hook
def update_notifiers(self, size, position, offset_inset, coming_from, cursor_coords, snap_rows):
# which ends are visible? returns "top", "bottom", both or neither.
result = self.listbox.ends_visible(size)
if ("top" in result) and ("bottom" in result):
self.notifier_top.set_attr_map({None:"notifier_inactive"})
self.notifier_bottom.set_attr_map({None:"notifier_inactive"})
elif "top" in result:
self.notifier_top.set_attr_map({None:"notifier_inactive"})
self.notifier_bottom.set_attr_map({None:"notifier_active"})
elif "bottom" in result:
self.notifier_top.set_attr_map({None:"notifier_active"})
self.notifier_bottom.set_attr_map({None:"notifier_inactive"})
else:
self.notifier_top.set_attr_map({None:"notifier_active"})
self.notifier_bottom.set_attr_map({None:"notifier_active"})
def handle_input(self, key):
if key in ('q', 'Q', 'esc'):
self.exit()
def start(self):
self.loop.run()
def exit(self):
raise urwid.ExitMainLoop()
if __name__ == '__main__':
app = App(ENTRIES)
app.start()
The only real difference is that, I use instances of SelectableRow
instead of urwid.Button
. (SelectableRow
was taken from this answer of user elias.)
Here is the corresponding TUI:
回答2:
I've implemented a list box that applies the second visualization concept (bars at the top and bottom) by default.
It is called additional_urwid_widgets.IndicativeListBox and can be installed via pip.
For a stand alone example, which illustrates the functionality of the widget, see here.
For more (and simpler) examples, see here.
For a more detailed explanation of the parameters and options, see the corresponding github wiki entry.
Some Examples
Minimal
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
from additional_urwid_widgets import IndicativeListBox # installed via pip
import urwid # installed via pip
# Color schemes that specify the appearance off focus and on focus.
PALETTE = [("reveal_focus", "black", "light cyan", "standout")]
# The list box is filled with buttons.
body = [urwid.Button(letter) for letter in "abcdefghijklmnopqrstuvwxyz"]
# Wrap the list items into an 'urwid.AttrMap', so that they have an other appearance when focused.
# Instead of an simple list-like object you can/should create a 'urwid.ListWalker'.
attr_body = [urwid.AttrMap(entry, None, "reveal_focus") for entry in body]
ilb = IndicativeListBox(attr_body)
loop = urwid.MainLoop(ilb,
PALETTE)
loop.run()
Display items above/below
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
from additional_urwid_widgets import IndicativeListBox # installed via pip
import urwid # installed via pip
# Color schemes that specify the appearance off focus and on focus.
PALETTE = [("reveal_focus", "black", "light cyan", "standout")]
# The list box is filled with buttons.
body = [urwid.Button(letter) for letter in "abcdefghijklmnopqrstuvwxyz"]
# Wrap the list items into an 'urwid.AttrMap', so that they have an other appearance when focused.
# Instead of an simple list-like object you can/should create a 'urwid.ListWalker'.
attr_body = [urwid.AttrMap(entry, None, "reveal_focus") for entry in body]
ilb = IndicativeListBox(attr_body,
topBar_endCovered_prop=("{} above ...", None, None),
bottomBar_endCovered_prop=("{} below ...", None, None))
loop = urwid.MainLoop(ilb,
PALETTE)
loop.run()
In contex with other widgets (also styled)
In this example, ctrl must be additionally pressed so that the list box responds to the input.
This allows the widget to be used in vertical containers (such as urwid.Pile).
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
from additional_urwid_widgets import IndicativeListBox, MODIFIER_KEY # installed via pip
import urwid # installed via pip
# Color schemes that specify the appearance off focus and on focus.
PALETTE = [("reveal_focus", "black", "light cyan", "standout"),
("ilb_barActive_focus", "dark cyan", "light gray"),
("ilb_barActive_offFocus", "light gray", "dark gray"),
("ilb_barInactive_focus", "light cyan", "dark gray"),
("ilb_barInactive_offFocus", "black", "dark gray"),
("ilb_highlight_offFocus", "black", "dark cyan")]
# The list box is filled with buttons.
body = [urwid.Button(letter) for letter in "abcdefghijklmnopqrstuvwxyz"]
# Wrap the list items into an 'urwid.AttrMap', so that they have an other appearance when focused.
# Instead of an simple list-like object you can/should create a 'urwid.ListWalker'.
attr_body = [urwid.AttrMap(entry, None, "reveal_focus") for entry in body]
ilb = ilb = IndicativeListBox(attr_body,
modifier_key=MODIFIER_KEY.CTRL,
return_unused_navigation_input=False,
topBar_endCovered_prop=("ᐃ", "ilb_barActive_focus", "ilb_barActive_offFocus"),
topBar_endExposed_prop=("───", "ilb_barInactive_focus", "ilb_barInactive_offFocus"),
bottomBar_endCovered_prop=("ᐁ", "ilb_barActive_focus", "ilb_barActive_offFocus"),
bottomBar_endExposed_prop=("───", "ilb_barInactive_focus", "ilb_barInactive_offFocus"),
highlight_offFocus="ilb_highlight_offFocus")
pile = urwid.Pile([urwid.Text("The listbox responds only if 'ctrl' is pressed."),
urwid.Divider(" "),
urwid.Button("a button"),
urwid.BoxAdapter(ilb, 6), # Wrap flow widget in box adapter
urwid.Button("another button")])
loop = urwid.MainLoop(urwid.Filler(pile, "top"),
PALETTE)
loop.run()
来源:https://stackoverflow.com/questions/52428684/how-to-indicate-that-a-urwid-listbox-has-more-items-than-displayed-at-the-moment