Repository URL to install this package:
Version:
2.0.2 ▾
|
urwid-uikit
/
menus.py
|
---|
"""Helper widgets for implementing dropdown menus"""
from typing import Callable, Iterable, Optional, Sequence, Tuple, TypeVar, Union
from urwid import (
AttrMap,
Button,
Divider,
ListBox,
SimpleFocusListWalker,
Text,
Widget,
WidgetWrap,
connect_signal,
disconnect_signal,
)
from .dialogs import DialogOverlay
from .graphics import PatchedLineBox
from .utils import extract_base_widget, tuplify
#: Type of menu item specifications that are accepted by `create_menu_item_from_spec()`
MenuItemSpecification = Union[None, str, tuple]
T = TypeVar("T")
class Menu(WidgetWrap):
"""urwid widget that represents a menu with a ListBox_."""
def __init__(
self,
items: Union[
Sequence[MenuItemSpecification],
Callable[[], Sequence[MenuItemSpecification]],
] = (),
):
"""Creates a menu widget from the given list of items.
Parameters:
items (List[str]): the list of menu items to show
Returns:
Widget: a menu widget
"""
if callable(items):
items = items()
self.items = [create_menu_item_from_spec(item) for item in items]
super().__init__(ListBox(SimpleFocusListWalker(self.items)))
def keypress(self, size, key: str):
if len(size) != 2:
size = self.pack(size)
return super().keypress(size, key)
def pack(self, size, focus: bool = False):
if not size:
# Fixed widget; we get to choose our own size
if self.items:
max_width = max(self._get_item_width(item) for item in self.items)
else:
max_width = 0
return max_width, self.rows(size, focus)
elif len(size) == 1:
# Flow widget; we get to choose our own height
return (size[0], self.rows(size, focus))
else:
# Box widget; parent chooses a size for us and we respect that
return size
def render(self, size, focus: bool = False):
if len(size) != 2:
size = self.pack(size, focus)
return super().render(size, focus)
def rows(self, size, focus: bool = False) -> int:
return len(self.items)
@staticmethod
def _get_item_width(item) -> int:
item = extract_base_widget(item)
if isinstance(item, Button):
return len(item.get_label()) + 5
elif isinstance(item, Text):
return len(item.text) + 3
else:
return 0
class MenuItemButton(Button):
button_left = Text("")
button_right = Text("")
class SubmenuButton(Button):
button_left = Text("")
button_right = Text(">")
def open_with(self, overlay):
overlay.open_menu(self.items, title=self.label)
class MenuOverlay(DialogOverlay):
"""Overlay that can be placed over a main application widget to provide
a cascading dropdown menu (as well as ordinary dialogs).
"""
def __init__(
self,
app_widget,
menu_factory: Callable[[Sequence[MenuItemSpecification]], Menu] = Menu,
):
"""Constructor.
Parameters:
app_widget (Widget): main application frame widget that the menu
overlay will wrap
menu_factory (callable): factory function that can be invoked with
a list of items and that will create an appropriate widget to
be shown in the overlay containing the given items
"""
super().__init__(app_widget)
self._menu_factory = menu_factory
def _on_menu_closed(self, menu: Widget) -> bool:
"""Callback to call when a menu widget was closed."""
menu = menu.base_widget
for item in menu.items:
item = extract_base_widget(item)
if isinstance(item, SubmenuButton):
disconnect_signal(item, "click", self._open_submenu)
elif isinstance(item, MenuItemButton):
disconnect_signal(item, "click", self._close_all_menus)
return True
def open_menu(
self, items: Sequence[MenuItemSpecification], title: Optional[str] = None
) -> None:
"""Opens the given menu widget on top of the overlay.
Parameters:
items (List): the items in the menu to open
title (Optional[str]): optional title of the menu
"""
menu = self._menu_factory(items)
for item in menu.items:
item = extract_base_widget(item)
if isinstance(item, SubmenuButton):
connect_signal(item, "click", self._open_submenu)
elif isinstance(item, MenuItemButton):
connect_signal(item, "click", self._close_all_menus)
widget = AttrMap(
PatchedLineBox(menu, title=title), "menu in background", focus_map="menu"
)
return self.open_dialog(widget, on_close=self._on_menu_closed, styled=True)
def _close_all_menus(self, item: MenuItemButton) -> None:
self.close()
def _open_submenu(self, item: SubmenuButton) -> None:
item.open_with(self)
def create_separator() -> Widget:
"""Creates a widget in a menu that can be used as a horizontal
separator.
"""
return Divider("\u2015")
def create_submenu(title: str, items) -> Widget:
"""Creates a widget in a menu that will open a submenu with the given
items when invoked.
Parameters:
title: the title of the submenu; it will be used both for the label of
the menu widget and the label of the submenu box.
items (Union[List, callable]): the list of items to show in the menu,
or a function that will return such a list when invoked with no
arguments. Each item in the list will be passed through to
`create_menu_item_from_spec()` so you can use anything there that
is accepted by `create_menu_item_from_spec()`.
Returns:
Widget: a widget that will open the submenu when invoked
"""
if items:
button = SubmenuButton(title)
button.items = items
return AttrMap(button, None, focus_map="menu focus")
else:
return AttrMap(Text(" " + title), "menu disabled")
def create_submenu_from_enum(
title: str,
items: Iterable[Tuple[T, str]],
getter: Union[T, Callable[[], T]],
setter: Callable[[T], None],
) -> MenuItemSpecification:
"""Creates an array representing a submenu in a menu structure where each
item corresponds to a possible element of an enum, and at most one of
these elements is marked as the currently selected one.
Parameters:
title: the title of the submenu
items (Iterable[object, str]): an iterable that yields object-string
pairs such that the first element of the pair is a possible value
of the enum and the second element is the label of the
corresponding menu item.
getter (Union[object, callable]): an object that denotes the current
value of the enum, or a callable that returns such a value when
invoked with no arguments
setter (callable): a function that can be called with the new value of
the enum in order to activate the corresponding menu item
Returns:
object: an opaque object that describes a submenu containing one item
for each possible value of the enum such that the current one is
marked. This object can safely be passed to
`create_menu_item_from_spec()`.
"""
current = getter() if callable(getter) else getter
if not title.endswith(">"):
title += ">"
return (
title,
[
(
"({0}) {1}".format("*" if item is current else " ", item_title),
setter,
item,
)
for item, item_title in items
],
)
def create_menu_item(
title: Optional[str] = None,
callback: Optional[Callable[..., None]] = None,
*args,
**kwds
) -> Widget:
"""Creates a widget in a menu with the given title.
Parameters:
title: the title of the menu item.
callback (Optional[callable]): a function to call when the menu item
was selected. When it is ``None``, the item is assumed to be
disabled.
Additional arguments are forwarded to the callback.
Returns:
Widget: the constructed menu widget
"""
if callback is not None:
def wrapper(button):
return callback(*args, **kwds)
button = MenuItemButton(title, wrapper)
return AttrMap(button, None, focus_map="menu focus")
else:
return AttrMap(Text(" " + (title or "")), "menu disabled")
def create_menu_item_from_spec(spec_: MenuItemSpecification = None):
"""Creates a widget in a menu from a specification object.
The specification object is a tuple where the first element of the tuple
specifies how the menu item will look like.
When the first element is ``None`` or a single dash, the returned widget
will be a separator.
When the first element is a string ending with `>`, the returned widget
is a menu item that opens a submenu. In this case, the second item of the
tuple must be either a list of items in the submenu (each of which will be
passed through `create_menu_item_from_spec()` when the submenu is opened,
or a function that returns such a list when invoked with no items.
In all other cases, the first element is assumed to be the title of the
menu item, and the second element must be a callback function to invoke
when the menu item is selected. Additional elements in the tuple will be
forwarded to the callback function as positional arguments.
Returns:
Widget: the constructed menu widget
"""
spec = tuplify(spec_)
title = spec[0]
if title is None or title == "-":
return create_separator()
if title.endswith(">"):
title = title[:-1].rstrip()
return create_submenu(title, spec[1] if len(spec) > 1 else None)
return create_menu_item(title, spec[1] if len(spec) > 1 else None, *spec[2:])