Source code for bauiv1lib.popup

# Released under the MIT License. See LICENSE for details.
#
"""Popup window/menu related functionality."""

from __future__ import annotations

import weakref
from typing import TYPE_CHECKING, override

import bauiv1 as bui

if TYPE_CHECKING:
    from typing import Any, Sequence, Callable, Literal






[docs] class PopupMenuWindow(PopupWindow): """A menu built using popup-window functionality.""" def __init__( self, position: tuple[float, float], choices: Sequence[str], current_choice: str, *, delegate: Any = None, width: float = 230.0, maxwidth: float | None = None, scale: float = 1.0, choices_disabled: Sequence[str] | None = None, choices_display: Sequence[bui.Lstr] | None = None, ): # FIXME: Clean up a bit. # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements if choices_disabled is None: choices_disabled = [] if choices_display is None: choices_display = [] # FIXME: For the moment we base our width on these strings so we # need to flatten them. choices_display_fin: list[str] = [] for choice_display in choices_display: choices_display_fin.append(choice_display.evaluate()) if maxwidth is None: maxwidth = width * 1.5 self._transitioning_out = False self._choices = list(choices) self._choices_display = list(choices_display_fin) self._current_choice = current_choice self._choices_disabled = list(choices_disabled) self._done_building = False if not choices: raise TypeError('Must pass at least one choice') self._width = width self._scale = scale if len(choices) > 8: self._height = 280 self._use_scroll = True else: self._height = 20 + len(choices) * 33 self._use_scroll = False self._delegate = None # Don't want this stuff called just yet. # Extend width to fit our longest string (or our max-width). for index, choice in enumerate(choices): if len(choices_display_fin) == len(choices): choice_display_name = choices_display_fin[index] else: choice_display_name = choice if self._use_scroll: self._width = max( self._width, min( maxwidth, bui.get_string_width( choice_display_name, suppress_warning=True ), ) + 75, ) else: self._width = max( self._width, min( maxwidth, bui.get_string_width( choice_display_name, suppress_warning=True ), ) + 60, ) # Init parent class - this will rescale and reposition things as # needed and create our root widget. super().__init__( position, size=(self._width, self._height), scale=self._scale ) if self._use_scroll: self._scrollwidget = bui.scrollwidget( parent=self.root_widget, position=(20, 20), highlight=False, color=(0.35, 0.55, 0.15), size=(self._width - 40, self._height - 40), ) self._columnwidget = bui.columnwidget( parent=self._scrollwidget, border=2, margin=0 ) else: self._offset_widget = bui.containerwidget( parent=self.root_widget, position=(12, 12), size=(self._width - 40, self._height), background=False, ) self._columnwidget = bui.columnwidget( parent=self._offset_widget, border=2, margin=0 ) for index, choice in enumerate(choices): if len(choices_display_fin) == len(choices): choice_display_name = choices_display_fin[index] else: choice_display_name = choice inactive = choice in self._choices_disabled wdg = bui.textwidget( parent=self._columnwidget, size=(self._width - 40, 28), on_select_call=bui.Call(self._select, index), click_activate=True, color=( (0.5, 0.5, 0.5, 0.5) if inactive else ( (0.5, 1, 0.5, 1) if choice == self._current_choice else (0.8, 0.8, 0.8, 1.0) ) ), padding=0, maxwidth=maxwidth, text=choice_display_name, on_activate_call=self._activate, v_align='center', selectable=(not inactive), glow_type='uniform', ) if choice == self._current_choice: bui.containerwidget( edit=self._columnwidget, selected_child=wdg, visible_child=wdg, ) # ok from now on our delegate can be called self._delegate = weakref.ref(delegate) self._done_building = True def _select(self, index: int) -> None: if self._done_building: self._current_choice = self._choices[index] def _activate(self) -> None: bui.getsound('swish').play() bui.apptimer(0.05, self._transition_out) delegate = self._getdelegate() if delegate is not None: # Call this in a timer so it doesn't interfere with us killing # our widgets and whatnot. call = bui.Call( delegate.popup_menu_selected_choice, self, self._current_choice ) bui.apptimer(0, call) def _getdelegate(self) -> Any: return None if self._delegate is None else self._delegate() def _transition_out(self) -> None: if not self.root_widget: return if not self._transitioning_out: self._transitioning_out = True delegate = self._getdelegate() if delegate is not None: delegate.popup_menu_closing(self) bui.containerwidget(edit=self.root_widget, transition='out_scale')
[docs] @override def on_popup_cancel(self) -> None: if not self._transitioning_out: bui.getsound('swish').play() self._transition_out()
[docs] class PopupMenu: """A complete popup-menu control. This creates a button and wrangles its pop-up menu. """ def __init__( self, parent: bui.Widget, position: tuple[float, float], choices: Sequence[str], *, current_choice: str | None = None, on_value_change_call: Callable[[str], Any] | None = None, opening_call: Callable[[], Any] | None = None, closing_call: Callable[[], Any] | None = None, width: float = 230.0, maxwidth: float | None = None, scale: float | None = None, choices_disabled: Sequence[str] | None = None, choices_display: Sequence[bui.Lstr] | None = None, button_size: tuple[float, float] = (160.0, 50.0), autoselect: bool = True, ): # pylint: disable=too-many-locals if choices_disabled is None: choices_disabled = [] if choices_display is None: choices_display = [] assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale if scale is None: scale = ( 2.3 if uiscale is bui.UIScale.SMALL else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 ) if current_choice not in choices: current_choice = None self._choices = list(choices) if not choices: raise TypeError('no choices given') self._choices_display = list(choices_display) self._choices_disabled = list(choices_disabled) self._width = width self._maxwidth = maxwidth self._scale = scale self._current_choice = ( current_choice if current_choice is not None else self._choices[0] ) self._position = position self._parent = parent if not choices: raise TypeError('Must pass at least one choice') self._parent = parent self._button_size = button_size self._button = bui.buttonwidget( parent=self._parent, position=(self._position[0], self._position[1]), autoselect=autoselect, size=self._button_size, scale=1.0, label='', on_activate_call=lambda: bui.apptimer(0, self._make_popup), ) self._on_value_change_call = None # Don't wanna call for initial set. self._opening_call = opening_call self._autoselect = autoselect self._closing_call = closing_call self.set_choice(self._current_choice) self._on_value_change_call = on_value_change_call self._window_widget: bui.Widget | None = None # Complain if we outlive our button. bui.uicleanupcheck(self, self._button) def _make_popup(self) -> None: if not self._button: return if self._opening_call: self._opening_call() self._window_widget = PopupMenuWindow( position=self._button.get_screen_space_center(), delegate=self, width=self._width, maxwidth=self._maxwidth, scale=self._scale, choices=self._choices, current_choice=self._current_choice, choices_disabled=self._choices_disabled, choices_display=self._choices_display, ).root_widget
[docs] def get_button(self) -> bui.Widget: """Return the menu's button widget.""" return self._button
[docs] def get_window_widget(self) -> bui.Widget | None: """Return the menu's window widget (or None if nonexistent).""" return self._window_widget
[docs] def popup_menu_selected_choice( self, popup_window: PopupWindow, choice: str ) -> None: """Called when a choice is selected.""" del popup_window # Unused here. self.set_choice(choice) if self._on_value_change_call: self._on_value_change_call(choice)
[docs] def popup_menu_closing(self, popup_window: PopupWindow) -> None: """Called when the menu is closing.""" del popup_window # Unused here. if self._button: bui.containerwidget(edit=self._parent, selected_child=self._button) self._window_widget = None if self._closing_call: self._closing_call()
[docs] def set_choice(self, choice: str) -> None: """Set the selected choice.""" self._current_choice = choice displayname: str | bui.Lstr if len(self._choices_display) == len(self._choices): displayname = self._choices_display[self._choices.index(choice)] else: displayname = choice if self._button: bui.buttonwidget(edit=self._button, label=displayname)