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), border_opacity=0.5, ) 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)
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum