Мне кажется, я нашел реализацию для второй концепции визуализации (столбцы вверху и внизу списка).
#!/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()
По сути, я создаю подкласс urwid.Listbox
и переопределяюего change_focus()
метод для добавления крючка.Очевидно, этот метод вызывается изнутри при изменении фокуса.
Фактическая логика использует результат метода ends_visible()
, который возвращает видимые в данный момент концы списка (верхний, нижний, оба или ни одного).В зависимости от этого я изменяю представление двух окружающих элементов urwid.Text
.
Код генерирует следующий TUI:
Я также написал вариант кода, основанный на оригинальной спецификации:
#!/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()
Единственная реальная разница в том, что я использую экземплярыSelectableRow
вместо urwid.Button
.(SelectableRow
взято из этого ответа пользователя elias .)
Вот соответствующий TUI: