bauiv1lib.popup
Popup window/menu related functionality.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Popup window/menu related functionality.""" 4 5from __future__ import annotations 6 7import weakref 8from typing import TYPE_CHECKING, override 9 10import bauiv1 as bui 11 12if TYPE_CHECKING: 13 from typing import Any, Sequence, Callable, Literal 14 15 16class PopupWindow: 17 """A transient window that positions and scales itself for visibility. 18 19 Category: UI Classes""" 20 21 def __init__( 22 self, 23 position: tuple[float, float], 24 size: tuple[float, float], 25 scale: float = 1.0, 26 *, 27 offset: tuple[float, float] = (0, 0), 28 bg_color: tuple[float, float, float] = (0.35, 0.55, 0.15), 29 focus_position: tuple[float, float] = (0, 0), 30 focus_size: tuple[float, float] | None = None, 31 toolbar_visibility: Literal[ 32 'inherit', 33 'menu_minimal_no_back', 34 'menu_store_no_back', 35 ] = 'menu_minimal_no_back', 36 edge_buffer_scale: float = 1.0, 37 ): 38 # pylint: disable=too-many-locals 39 if focus_size is None: 40 focus_size = size 41 42 # In vr mode we can't have windows going outside the screen. 43 if bui.app.env.vr: 44 focus_size = size 45 focus_position = (0, 0) 46 47 width = focus_size[0] 48 height = focus_size[1] 49 50 # Ok, we've been given a desired width, height, and scale; 51 # we now need to ensure that we're all onscreen by scaling down if 52 # need be and clamping it to the UI bounds. 53 bounds = bui.uibounds() 54 edge_buffer = 15 * edge_buffer_scale 55 bounds_width = bounds[1] - bounds[0] - edge_buffer * 2 56 bounds_height = bounds[3] - bounds[2] - edge_buffer * 2 57 58 fin_width = width * scale 59 fin_height = height * scale 60 if fin_width > bounds_width: 61 scale /= fin_width / bounds_width 62 fin_width = width * scale 63 fin_height = height * scale 64 if fin_height > bounds_height: 65 scale /= fin_height / bounds_height 66 fin_width = width * scale 67 fin_height = height * scale 68 69 x_min = bounds[0] + edge_buffer + fin_width * 0.5 70 y_min = bounds[2] + edge_buffer + fin_height * 0.5 71 x_max = bounds[1] - edge_buffer - fin_width * 0.5 72 y_max = bounds[3] - edge_buffer - fin_height * 0.5 73 74 x_fin = min(max(x_min, position[0] + offset[0]), x_max) 75 y_fin = min(max(y_min, position[1] + offset[1]), y_max) 76 77 # ok, we've calced a valid x/y position and a scale based on or 78 # focus area. ..now calc the difference between the center of our 79 # focus area and the center of our window to come up with the 80 # offset we'll need to plug in to the window 81 x_offs = ( 82 (focus_position[0] + focus_size[0] * 0.5) - (size[0] * 0.5) 83 ) * scale 84 y_offs = ( 85 (focus_position[1] + focus_size[1] * 0.5) - (size[1] * 0.5) 86 ) * scale 87 88 self.root_widget = bui.containerwidget( 89 transition='in_scale', 90 scale=scale, 91 toolbar_visibility=toolbar_visibility, 92 size=size, 93 parent=bui.get_special_widget('overlay_stack'), 94 stack_offset=(x_fin - x_offs, y_fin - y_offs), 95 scale_origin_stack_offset=(position[0], position[1]), 96 on_outside_click_call=self.on_popup_cancel, 97 claim_outside_clicks=True, 98 color=bg_color, 99 on_cancel_call=self.on_popup_cancel, 100 ) 101 # complain if we outlive our root widget 102 bui.uicleanupcheck(self, self.root_widget) 103 104 def on_popup_cancel(self) -> None: 105 """Called when the popup is canceled. 106 107 Cancels can occur due to clicking outside the window, 108 hitting escape, etc. 109 """ 110 111 112class PopupMenuWindow(PopupWindow): 113 """A menu built using popup-window functionality.""" 114 115 def __init__( 116 self, 117 position: tuple[float, float], 118 choices: Sequence[str], 119 current_choice: str, 120 *, 121 delegate: Any = None, 122 width: float = 230.0, 123 maxwidth: float | None = None, 124 scale: float = 1.0, 125 choices_disabled: Sequence[str] | None = None, 126 choices_display: Sequence[bui.Lstr] | None = None, 127 ): 128 # FIXME: Clean up a bit. 129 # pylint: disable=too-many-branches 130 # pylint: disable=too-many-locals 131 # pylint: disable=too-many-statements 132 if choices_disabled is None: 133 choices_disabled = [] 134 if choices_display is None: 135 choices_display = [] 136 137 # FIXME: For the moment we base our width on these strings so we 138 # need to flatten them. 139 choices_display_fin: list[str] = [] 140 for choice_display in choices_display: 141 choices_display_fin.append(choice_display.evaluate()) 142 143 if maxwidth is None: 144 maxwidth = width * 1.5 145 146 self._transitioning_out = False 147 self._choices = list(choices) 148 self._choices_display = list(choices_display_fin) 149 self._current_choice = current_choice 150 self._choices_disabled = list(choices_disabled) 151 self._done_building = False 152 if not choices: 153 raise TypeError('Must pass at least one choice') 154 self._width = width 155 self._scale = scale 156 if len(choices) > 8: 157 self._height = 280 158 self._use_scroll = True 159 else: 160 self._height = 20 + len(choices) * 33 161 self._use_scroll = False 162 self._delegate = None # Don't want this stuff called just yet. 163 164 # Extend width to fit our longest string (or our max-width). 165 for index, choice in enumerate(choices): 166 if len(choices_display_fin) == len(choices): 167 choice_display_name = choices_display_fin[index] 168 else: 169 choice_display_name = choice 170 if self._use_scroll: 171 self._width = max( 172 self._width, 173 min( 174 maxwidth, 175 bui.get_string_width( 176 choice_display_name, suppress_warning=True 177 ), 178 ) 179 + 75, 180 ) 181 else: 182 self._width = max( 183 self._width, 184 min( 185 maxwidth, 186 bui.get_string_width( 187 choice_display_name, suppress_warning=True 188 ), 189 ) 190 + 60, 191 ) 192 193 # Init parent class - this will rescale and reposition things as 194 # needed and create our root widget. 195 super().__init__( 196 position, size=(self._width, self._height), scale=self._scale 197 ) 198 199 if self._use_scroll: 200 self._scrollwidget = bui.scrollwidget( 201 parent=self.root_widget, 202 position=(20, 20), 203 highlight=False, 204 color=(0.35, 0.55, 0.15), 205 size=(self._width - 40, self._height - 40), 206 ) 207 self._columnwidget = bui.columnwidget( 208 parent=self._scrollwidget, border=2, margin=0 209 ) 210 else: 211 self._offset_widget = bui.containerwidget( 212 parent=self.root_widget, 213 position=(12, 12), 214 size=(self._width - 40, self._height), 215 background=False, 216 ) 217 self._columnwidget = bui.columnwidget( 218 parent=self._offset_widget, border=2, margin=0 219 ) 220 for index, choice in enumerate(choices): 221 if len(choices_display_fin) == len(choices): 222 choice_display_name = choices_display_fin[index] 223 else: 224 choice_display_name = choice 225 inactive = choice in self._choices_disabled 226 wdg = bui.textwidget( 227 parent=self._columnwidget, 228 size=(self._width - 40, 28), 229 on_select_call=bui.Call(self._select, index), 230 click_activate=True, 231 color=( 232 (0.5, 0.5, 0.5, 0.5) 233 if inactive 234 else ( 235 (0.5, 1, 0.5, 1) 236 if choice == self._current_choice 237 else (0.8, 0.8, 0.8, 1.0) 238 ) 239 ), 240 padding=0, 241 maxwidth=maxwidth, 242 text=choice_display_name, 243 on_activate_call=self._activate, 244 v_align='center', 245 selectable=(not inactive), 246 glow_type='uniform', 247 ) 248 if choice == self._current_choice: 249 bui.containerwidget( 250 edit=self._columnwidget, 251 selected_child=wdg, 252 visible_child=wdg, 253 ) 254 255 # ok from now on our delegate can be called 256 self._delegate = weakref.ref(delegate) 257 self._done_building = True 258 259 def _select(self, index: int) -> None: 260 if self._done_building: 261 self._current_choice = self._choices[index] 262 263 def _activate(self) -> None: 264 bui.getsound('swish').play() 265 bui.apptimer(0.05, self._transition_out) 266 delegate = self._getdelegate() 267 if delegate is not None: 268 # Call this in a timer so it doesn't interfere with us killing 269 # our widgets and whatnot. 270 call = bui.Call( 271 delegate.popup_menu_selected_choice, self, self._current_choice 272 ) 273 bui.apptimer(0, call) 274 275 def _getdelegate(self) -> Any: 276 return None if self._delegate is None else self._delegate() 277 278 def _transition_out(self) -> None: 279 if not self.root_widget: 280 return 281 if not self._transitioning_out: 282 self._transitioning_out = True 283 delegate = self._getdelegate() 284 if delegate is not None: 285 delegate.popup_menu_closing(self) 286 bui.containerwidget(edit=self.root_widget, transition='out_scale') 287 288 @override 289 def on_popup_cancel(self) -> None: 290 if not self._transitioning_out: 291 bui.getsound('swish').play() 292 self._transition_out() 293 294 295class PopupMenu: 296 """A complete popup-menu control. 297 298 This creates a button and wrangles its pop-up menu. 299 """ 300 301 def __init__( 302 self, 303 parent: bui.Widget, 304 position: tuple[float, float], 305 choices: Sequence[str], 306 *, 307 current_choice: str | None = None, 308 on_value_change_call: Callable[[str], Any] | None = None, 309 opening_call: Callable[[], Any] | None = None, 310 closing_call: Callable[[], Any] | None = None, 311 width: float = 230.0, 312 maxwidth: float | None = None, 313 scale: float | None = None, 314 choices_disabled: Sequence[str] | None = None, 315 choices_display: Sequence[bui.Lstr] | None = None, 316 button_size: tuple[float, float] = (160.0, 50.0), 317 autoselect: bool = True, 318 ): 319 # pylint: disable=too-many-locals 320 if choices_disabled is None: 321 choices_disabled = [] 322 if choices_display is None: 323 choices_display = [] 324 assert bui.app.classic is not None 325 uiscale = bui.app.ui_v1.uiscale 326 if scale is None: 327 scale = ( 328 2.3 329 if uiscale is bui.UIScale.SMALL 330 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 331 ) 332 if current_choice not in choices: 333 current_choice = None 334 self._choices = list(choices) 335 if not choices: 336 raise TypeError('no choices given') 337 self._choices_display = list(choices_display) 338 self._choices_disabled = list(choices_disabled) 339 self._width = width 340 self._maxwidth = maxwidth 341 self._scale = scale 342 self._current_choice = ( 343 current_choice if current_choice is not None else self._choices[0] 344 ) 345 self._position = position 346 self._parent = parent 347 if not choices: 348 raise TypeError('Must pass at least one choice') 349 self._parent = parent 350 self._button_size = button_size 351 352 self._button = bui.buttonwidget( 353 parent=self._parent, 354 position=(self._position[0], self._position[1]), 355 autoselect=autoselect, 356 size=self._button_size, 357 scale=1.0, 358 label='', 359 on_activate_call=lambda: bui.apptimer(0, self._make_popup), 360 ) 361 self._on_value_change_call = None # Don't wanna call for initial set. 362 self._opening_call = opening_call 363 self._autoselect = autoselect 364 self._closing_call = closing_call 365 self.set_choice(self._current_choice) 366 self._on_value_change_call = on_value_change_call 367 self._window_widget: bui.Widget | None = None 368 369 # Complain if we outlive our button. 370 bui.uicleanupcheck(self, self._button) 371 372 def _make_popup(self) -> None: 373 if not self._button: 374 return 375 if self._opening_call: 376 self._opening_call() 377 self._window_widget = PopupMenuWindow( 378 position=self._button.get_screen_space_center(), 379 delegate=self, 380 width=self._width, 381 maxwidth=self._maxwidth, 382 scale=self._scale, 383 choices=self._choices, 384 current_choice=self._current_choice, 385 choices_disabled=self._choices_disabled, 386 choices_display=self._choices_display, 387 ).root_widget 388 389 def get_button(self) -> bui.Widget: 390 """Return the menu's button widget.""" 391 return self._button 392 393 def get_window_widget(self) -> bui.Widget | None: 394 """Return the menu's window widget (or None if nonexistent).""" 395 return self._window_widget 396 397 def popup_menu_selected_choice( 398 self, popup_window: PopupWindow, choice: str 399 ) -> None: 400 """Called when a choice is selected.""" 401 del popup_window # Unused here. 402 self.set_choice(choice) 403 if self._on_value_change_call: 404 self._on_value_change_call(choice) 405 406 def popup_menu_closing(self, popup_window: PopupWindow) -> None: 407 """Called when the menu is closing.""" 408 del popup_window # Unused here. 409 if self._button: 410 bui.containerwidget(edit=self._parent, selected_child=self._button) 411 self._window_widget = None 412 if self._closing_call: 413 self._closing_call() 414 415 def set_choice(self, choice: str) -> None: 416 """Set the selected choice.""" 417 self._current_choice = choice 418 displayname: str | bui.Lstr 419 if len(self._choices_display) == len(self._choices): 420 displayname = self._choices_display[self._choices.index(choice)] 421 else: 422 displayname = choice 423 if self._button: 424 bui.buttonwidget(edit=self._button, label=displayname)
class
PopupWindow:
17class PopupWindow: 18 """A transient window that positions and scales itself for visibility. 19 20 Category: UI Classes""" 21 22 def __init__( 23 self, 24 position: tuple[float, float], 25 size: tuple[float, float], 26 scale: float = 1.0, 27 *, 28 offset: tuple[float, float] = (0, 0), 29 bg_color: tuple[float, float, float] = (0.35, 0.55, 0.15), 30 focus_position: tuple[float, float] = (0, 0), 31 focus_size: tuple[float, float] | None = None, 32 toolbar_visibility: Literal[ 33 'inherit', 34 'menu_minimal_no_back', 35 'menu_store_no_back', 36 ] = 'menu_minimal_no_back', 37 edge_buffer_scale: float = 1.0, 38 ): 39 # pylint: disable=too-many-locals 40 if focus_size is None: 41 focus_size = size 42 43 # In vr mode we can't have windows going outside the screen. 44 if bui.app.env.vr: 45 focus_size = size 46 focus_position = (0, 0) 47 48 width = focus_size[0] 49 height = focus_size[1] 50 51 # Ok, we've been given a desired width, height, and scale; 52 # we now need to ensure that we're all onscreen by scaling down if 53 # need be and clamping it to the UI bounds. 54 bounds = bui.uibounds() 55 edge_buffer = 15 * edge_buffer_scale 56 bounds_width = bounds[1] - bounds[0] - edge_buffer * 2 57 bounds_height = bounds[3] - bounds[2] - edge_buffer * 2 58 59 fin_width = width * scale 60 fin_height = height * scale 61 if fin_width > bounds_width: 62 scale /= fin_width / bounds_width 63 fin_width = width * scale 64 fin_height = height * scale 65 if fin_height > bounds_height: 66 scale /= fin_height / bounds_height 67 fin_width = width * scale 68 fin_height = height * scale 69 70 x_min = bounds[0] + edge_buffer + fin_width * 0.5 71 y_min = bounds[2] + edge_buffer + fin_height * 0.5 72 x_max = bounds[1] - edge_buffer - fin_width * 0.5 73 y_max = bounds[3] - edge_buffer - fin_height * 0.5 74 75 x_fin = min(max(x_min, position[0] + offset[0]), x_max) 76 y_fin = min(max(y_min, position[1] + offset[1]), y_max) 77 78 # ok, we've calced a valid x/y position and a scale based on or 79 # focus area. ..now calc the difference between the center of our 80 # focus area and the center of our window to come up with the 81 # offset we'll need to plug in to the window 82 x_offs = ( 83 (focus_position[0] + focus_size[0] * 0.5) - (size[0] * 0.5) 84 ) * scale 85 y_offs = ( 86 (focus_position[1] + focus_size[1] * 0.5) - (size[1] * 0.5) 87 ) * scale 88 89 self.root_widget = bui.containerwidget( 90 transition='in_scale', 91 scale=scale, 92 toolbar_visibility=toolbar_visibility, 93 size=size, 94 parent=bui.get_special_widget('overlay_stack'), 95 stack_offset=(x_fin - x_offs, y_fin - y_offs), 96 scale_origin_stack_offset=(position[0], position[1]), 97 on_outside_click_call=self.on_popup_cancel, 98 claim_outside_clicks=True, 99 color=bg_color, 100 on_cancel_call=self.on_popup_cancel, 101 ) 102 # complain if we outlive our root widget 103 bui.uicleanupcheck(self, self.root_widget) 104 105 def on_popup_cancel(self) -> None: 106 """Called when the popup is canceled. 107 108 Cancels can occur due to clicking outside the window, 109 hitting escape, etc. 110 """
A transient window that positions and scales itself for visibility.
Category: UI Classes
PopupWindow( position: tuple[float, float], size: tuple[float, float], scale: float = 1.0, *, offset: tuple[float, float] = (0, 0), bg_color: tuple[float, float, float] = (0.35, 0.55, 0.15), focus_position: tuple[float, float] = (0, 0), focus_size: tuple[float, float] | None = None, toolbar_visibility: Literal['inherit', 'menu_minimal_no_back', 'menu_store_no_back'] = 'menu_minimal_no_back', edge_buffer_scale: float = 1.0)
22 def __init__( 23 self, 24 position: tuple[float, float], 25 size: tuple[float, float], 26 scale: float = 1.0, 27 *, 28 offset: tuple[float, float] = (0, 0), 29 bg_color: tuple[float, float, float] = (0.35, 0.55, 0.15), 30 focus_position: tuple[float, float] = (0, 0), 31 focus_size: tuple[float, float] | None = None, 32 toolbar_visibility: Literal[ 33 'inherit', 34 'menu_minimal_no_back', 35 'menu_store_no_back', 36 ] = 'menu_minimal_no_back', 37 edge_buffer_scale: float = 1.0, 38 ): 39 # pylint: disable=too-many-locals 40 if focus_size is None: 41 focus_size = size 42 43 # In vr mode we can't have windows going outside the screen. 44 if bui.app.env.vr: 45 focus_size = size 46 focus_position = (0, 0) 47 48 width = focus_size[0] 49 height = focus_size[1] 50 51 # Ok, we've been given a desired width, height, and scale; 52 # we now need to ensure that we're all onscreen by scaling down if 53 # need be and clamping it to the UI bounds. 54 bounds = bui.uibounds() 55 edge_buffer = 15 * edge_buffer_scale 56 bounds_width = bounds[1] - bounds[0] - edge_buffer * 2 57 bounds_height = bounds[3] - bounds[2] - edge_buffer * 2 58 59 fin_width = width * scale 60 fin_height = height * scale 61 if fin_width > bounds_width: 62 scale /= fin_width / bounds_width 63 fin_width = width * scale 64 fin_height = height * scale 65 if fin_height > bounds_height: 66 scale /= fin_height / bounds_height 67 fin_width = width * scale 68 fin_height = height * scale 69 70 x_min = bounds[0] + edge_buffer + fin_width * 0.5 71 y_min = bounds[2] + edge_buffer + fin_height * 0.5 72 x_max = bounds[1] - edge_buffer - fin_width * 0.5 73 y_max = bounds[3] - edge_buffer - fin_height * 0.5 74 75 x_fin = min(max(x_min, position[0] + offset[0]), x_max) 76 y_fin = min(max(y_min, position[1] + offset[1]), y_max) 77 78 # ok, we've calced a valid x/y position and a scale based on or 79 # focus area. ..now calc the difference between the center of our 80 # focus area and the center of our window to come up with the 81 # offset we'll need to plug in to the window 82 x_offs = ( 83 (focus_position[0] + focus_size[0] * 0.5) - (size[0] * 0.5) 84 ) * scale 85 y_offs = ( 86 (focus_position[1] + focus_size[1] * 0.5) - (size[1] * 0.5) 87 ) * scale 88 89 self.root_widget = bui.containerwidget( 90 transition='in_scale', 91 scale=scale, 92 toolbar_visibility=toolbar_visibility, 93 size=size, 94 parent=bui.get_special_widget('overlay_stack'), 95 stack_offset=(x_fin - x_offs, y_fin - y_offs), 96 scale_origin_stack_offset=(position[0], position[1]), 97 on_outside_click_call=self.on_popup_cancel, 98 claim_outside_clicks=True, 99 color=bg_color, 100 on_cancel_call=self.on_popup_cancel, 101 ) 102 # complain if we outlive our root widget 103 bui.uicleanupcheck(self, self.root_widget)
def
on_popup_cancel(self) -> None:
105 def on_popup_cancel(self) -> None: 106 """Called when the popup is canceled. 107 108 Cancels can occur due to clicking outside the window, 109 hitting escape, etc. 110 """
Called when the popup is canceled.
Cancels can occur due to clicking outside the window, hitting escape, etc.
113class PopupMenuWindow(PopupWindow): 114 """A menu built using popup-window functionality.""" 115 116 def __init__( 117 self, 118 position: tuple[float, float], 119 choices: Sequence[str], 120 current_choice: str, 121 *, 122 delegate: Any = None, 123 width: float = 230.0, 124 maxwidth: float | None = None, 125 scale: float = 1.0, 126 choices_disabled: Sequence[str] | None = None, 127 choices_display: Sequence[bui.Lstr] | None = None, 128 ): 129 # FIXME: Clean up a bit. 130 # pylint: disable=too-many-branches 131 # pylint: disable=too-many-locals 132 # pylint: disable=too-many-statements 133 if choices_disabled is None: 134 choices_disabled = [] 135 if choices_display is None: 136 choices_display = [] 137 138 # FIXME: For the moment we base our width on these strings so we 139 # need to flatten them. 140 choices_display_fin: list[str] = [] 141 for choice_display in choices_display: 142 choices_display_fin.append(choice_display.evaluate()) 143 144 if maxwidth is None: 145 maxwidth = width * 1.5 146 147 self._transitioning_out = False 148 self._choices = list(choices) 149 self._choices_display = list(choices_display_fin) 150 self._current_choice = current_choice 151 self._choices_disabled = list(choices_disabled) 152 self._done_building = False 153 if not choices: 154 raise TypeError('Must pass at least one choice') 155 self._width = width 156 self._scale = scale 157 if len(choices) > 8: 158 self._height = 280 159 self._use_scroll = True 160 else: 161 self._height = 20 + len(choices) * 33 162 self._use_scroll = False 163 self._delegate = None # Don't want this stuff called just yet. 164 165 # Extend width to fit our longest string (or our max-width). 166 for index, choice in enumerate(choices): 167 if len(choices_display_fin) == len(choices): 168 choice_display_name = choices_display_fin[index] 169 else: 170 choice_display_name = choice 171 if self._use_scroll: 172 self._width = max( 173 self._width, 174 min( 175 maxwidth, 176 bui.get_string_width( 177 choice_display_name, suppress_warning=True 178 ), 179 ) 180 + 75, 181 ) 182 else: 183 self._width = max( 184 self._width, 185 min( 186 maxwidth, 187 bui.get_string_width( 188 choice_display_name, suppress_warning=True 189 ), 190 ) 191 + 60, 192 ) 193 194 # Init parent class - this will rescale and reposition things as 195 # needed and create our root widget. 196 super().__init__( 197 position, size=(self._width, self._height), scale=self._scale 198 ) 199 200 if self._use_scroll: 201 self._scrollwidget = bui.scrollwidget( 202 parent=self.root_widget, 203 position=(20, 20), 204 highlight=False, 205 color=(0.35, 0.55, 0.15), 206 size=(self._width - 40, self._height - 40), 207 ) 208 self._columnwidget = bui.columnwidget( 209 parent=self._scrollwidget, border=2, margin=0 210 ) 211 else: 212 self._offset_widget = bui.containerwidget( 213 parent=self.root_widget, 214 position=(12, 12), 215 size=(self._width - 40, self._height), 216 background=False, 217 ) 218 self._columnwidget = bui.columnwidget( 219 parent=self._offset_widget, border=2, margin=0 220 ) 221 for index, choice in enumerate(choices): 222 if len(choices_display_fin) == len(choices): 223 choice_display_name = choices_display_fin[index] 224 else: 225 choice_display_name = choice 226 inactive = choice in self._choices_disabled 227 wdg = bui.textwidget( 228 parent=self._columnwidget, 229 size=(self._width - 40, 28), 230 on_select_call=bui.Call(self._select, index), 231 click_activate=True, 232 color=( 233 (0.5, 0.5, 0.5, 0.5) 234 if inactive 235 else ( 236 (0.5, 1, 0.5, 1) 237 if choice == self._current_choice 238 else (0.8, 0.8, 0.8, 1.0) 239 ) 240 ), 241 padding=0, 242 maxwidth=maxwidth, 243 text=choice_display_name, 244 on_activate_call=self._activate, 245 v_align='center', 246 selectable=(not inactive), 247 glow_type='uniform', 248 ) 249 if choice == self._current_choice: 250 bui.containerwidget( 251 edit=self._columnwidget, 252 selected_child=wdg, 253 visible_child=wdg, 254 ) 255 256 # ok from now on our delegate can be called 257 self._delegate = weakref.ref(delegate) 258 self._done_building = True 259 260 def _select(self, index: int) -> None: 261 if self._done_building: 262 self._current_choice = self._choices[index] 263 264 def _activate(self) -> None: 265 bui.getsound('swish').play() 266 bui.apptimer(0.05, self._transition_out) 267 delegate = self._getdelegate() 268 if delegate is not None: 269 # Call this in a timer so it doesn't interfere with us killing 270 # our widgets and whatnot. 271 call = bui.Call( 272 delegate.popup_menu_selected_choice, self, self._current_choice 273 ) 274 bui.apptimer(0, call) 275 276 def _getdelegate(self) -> Any: 277 return None if self._delegate is None else self._delegate() 278 279 def _transition_out(self) -> None: 280 if not self.root_widget: 281 return 282 if not self._transitioning_out: 283 self._transitioning_out = True 284 delegate = self._getdelegate() 285 if delegate is not None: 286 delegate.popup_menu_closing(self) 287 bui.containerwidget(edit=self.root_widget, transition='out_scale') 288 289 @override 290 def on_popup_cancel(self) -> None: 291 if not self._transitioning_out: 292 bui.getsound('swish').play() 293 self._transition_out()
A menu built using popup-window functionality.
PopupMenuWindow( 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: Optional[Sequence[str]] = None, choices_display: Optional[Sequence[babase.Lstr]] = None)
116 def __init__( 117 self, 118 position: tuple[float, float], 119 choices: Sequence[str], 120 current_choice: str, 121 *, 122 delegate: Any = None, 123 width: float = 230.0, 124 maxwidth: float | None = None, 125 scale: float = 1.0, 126 choices_disabled: Sequence[str] | None = None, 127 choices_display: Sequence[bui.Lstr] | None = None, 128 ): 129 # FIXME: Clean up a bit. 130 # pylint: disable=too-many-branches 131 # pylint: disable=too-many-locals 132 # pylint: disable=too-many-statements 133 if choices_disabled is None: 134 choices_disabled = [] 135 if choices_display is None: 136 choices_display = [] 137 138 # FIXME: For the moment we base our width on these strings so we 139 # need to flatten them. 140 choices_display_fin: list[str] = [] 141 for choice_display in choices_display: 142 choices_display_fin.append(choice_display.evaluate()) 143 144 if maxwidth is None: 145 maxwidth = width * 1.5 146 147 self._transitioning_out = False 148 self._choices = list(choices) 149 self._choices_display = list(choices_display_fin) 150 self._current_choice = current_choice 151 self._choices_disabled = list(choices_disabled) 152 self._done_building = False 153 if not choices: 154 raise TypeError('Must pass at least one choice') 155 self._width = width 156 self._scale = scale 157 if len(choices) > 8: 158 self._height = 280 159 self._use_scroll = True 160 else: 161 self._height = 20 + len(choices) * 33 162 self._use_scroll = False 163 self._delegate = None # Don't want this stuff called just yet. 164 165 # Extend width to fit our longest string (or our max-width). 166 for index, choice in enumerate(choices): 167 if len(choices_display_fin) == len(choices): 168 choice_display_name = choices_display_fin[index] 169 else: 170 choice_display_name = choice 171 if self._use_scroll: 172 self._width = max( 173 self._width, 174 min( 175 maxwidth, 176 bui.get_string_width( 177 choice_display_name, suppress_warning=True 178 ), 179 ) 180 + 75, 181 ) 182 else: 183 self._width = max( 184 self._width, 185 min( 186 maxwidth, 187 bui.get_string_width( 188 choice_display_name, suppress_warning=True 189 ), 190 ) 191 + 60, 192 ) 193 194 # Init parent class - this will rescale and reposition things as 195 # needed and create our root widget. 196 super().__init__( 197 position, size=(self._width, self._height), scale=self._scale 198 ) 199 200 if self._use_scroll: 201 self._scrollwidget = bui.scrollwidget( 202 parent=self.root_widget, 203 position=(20, 20), 204 highlight=False, 205 color=(0.35, 0.55, 0.15), 206 size=(self._width - 40, self._height - 40), 207 ) 208 self._columnwidget = bui.columnwidget( 209 parent=self._scrollwidget, border=2, margin=0 210 ) 211 else: 212 self._offset_widget = bui.containerwidget( 213 parent=self.root_widget, 214 position=(12, 12), 215 size=(self._width - 40, self._height), 216 background=False, 217 ) 218 self._columnwidget = bui.columnwidget( 219 parent=self._offset_widget, border=2, margin=0 220 ) 221 for index, choice in enumerate(choices): 222 if len(choices_display_fin) == len(choices): 223 choice_display_name = choices_display_fin[index] 224 else: 225 choice_display_name = choice 226 inactive = choice in self._choices_disabled 227 wdg = bui.textwidget( 228 parent=self._columnwidget, 229 size=(self._width - 40, 28), 230 on_select_call=bui.Call(self._select, index), 231 click_activate=True, 232 color=( 233 (0.5, 0.5, 0.5, 0.5) 234 if inactive 235 else ( 236 (0.5, 1, 0.5, 1) 237 if choice == self._current_choice 238 else (0.8, 0.8, 0.8, 1.0) 239 ) 240 ), 241 padding=0, 242 maxwidth=maxwidth, 243 text=choice_display_name, 244 on_activate_call=self._activate, 245 v_align='center', 246 selectable=(not inactive), 247 glow_type='uniform', 248 ) 249 if choice == self._current_choice: 250 bui.containerwidget( 251 edit=self._columnwidget, 252 selected_child=wdg, 253 visible_child=wdg, 254 ) 255 256 # ok from now on our delegate can be called 257 self._delegate = weakref.ref(delegate) 258 self._done_building = True
@override
def
on_popup_cancel(self) -> None:
289 @override 290 def on_popup_cancel(self) -> None: 291 if not self._transitioning_out: 292 bui.getsound('swish').play() 293 self._transition_out()
Called when the popup is canceled.
Cancels can occur due to clicking outside the window, hitting escape, etc.
Inherited Members
class
PopupMenu:
296class PopupMenu: 297 """A complete popup-menu control. 298 299 This creates a button and wrangles its pop-up menu. 300 """ 301 302 def __init__( 303 self, 304 parent: bui.Widget, 305 position: tuple[float, float], 306 choices: Sequence[str], 307 *, 308 current_choice: str | None = None, 309 on_value_change_call: Callable[[str], Any] | None = None, 310 opening_call: Callable[[], Any] | None = None, 311 closing_call: Callable[[], Any] | None = None, 312 width: float = 230.0, 313 maxwidth: float | None = None, 314 scale: float | None = None, 315 choices_disabled: Sequence[str] | None = None, 316 choices_display: Sequence[bui.Lstr] | None = None, 317 button_size: tuple[float, float] = (160.0, 50.0), 318 autoselect: bool = True, 319 ): 320 # pylint: disable=too-many-locals 321 if choices_disabled is None: 322 choices_disabled = [] 323 if choices_display is None: 324 choices_display = [] 325 assert bui.app.classic is not None 326 uiscale = bui.app.ui_v1.uiscale 327 if scale is None: 328 scale = ( 329 2.3 330 if uiscale is bui.UIScale.SMALL 331 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 332 ) 333 if current_choice not in choices: 334 current_choice = None 335 self._choices = list(choices) 336 if not choices: 337 raise TypeError('no choices given') 338 self._choices_display = list(choices_display) 339 self._choices_disabled = list(choices_disabled) 340 self._width = width 341 self._maxwidth = maxwidth 342 self._scale = scale 343 self._current_choice = ( 344 current_choice if current_choice is not None else self._choices[0] 345 ) 346 self._position = position 347 self._parent = parent 348 if not choices: 349 raise TypeError('Must pass at least one choice') 350 self._parent = parent 351 self._button_size = button_size 352 353 self._button = bui.buttonwidget( 354 parent=self._parent, 355 position=(self._position[0], self._position[1]), 356 autoselect=autoselect, 357 size=self._button_size, 358 scale=1.0, 359 label='', 360 on_activate_call=lambda: bui.apptimer(0, self._make_popup), 361 ) 362 self._on_value_change_call = None # Don't wanna call for initial set. 363 self._opening_call = opening_call 364 self._autoselect = autoselect 365 self._closing_call = closing_call 366 self.set_choice(self._current_choice) 367 self._on_value_change_call = on_value_change_call 368 self._window_widget: bui.Widget | None = None 369 370 # Complain if we outlive our button. 371 bui.uicleanupcheck(self, self._button) 372 373 def _make_popup(self) -> None: 374 if not self._button: 375 return 376 if self._opening_call: 377 self._opening_call() 378 self._window_widget = PopupMenuWindow( 379 position=self._button.get_screen_space_center(), 380 delegate=self, 381 width=self._width, 382 maxwidth=self._maxwidth, 383 scale=self._scale, 384 choices=self._choices, 385 current_choice=self._current_choice, 386 choices_disabled=self._choices_disabled, 387 choices_display=self._choices_display, 388 ).root_widget 389 390 def get_button(self) -> bui.Widget: 391 """Return the menu's button widget.""" 392 return self._button 393 394 def get_window_widget(self) -> bui.Widget | None: 395 """Return the menu's window widget (or None if nonexistent).""" 396 return self._window_widget 397 398 def popup_menu_selected_choice( 399 self, popup_window: PopupWindow, choice: str 400 ) -> None: 401 """Called when a choice is selected.""" 402 del popup_window # Unused here. 403 self.set_choice(choice) 404 if self._on_value_change_call: 405 self._on_value_change_call(choice) 406 407 def popup_menu_closing(self, popup_window: PopupWindow) -> None: 408 """Called when the menu is closing.""" 409 del popup_window # Unused here. 410 if self._button: 411 bui.containerwidget(edit=self._parent, selected_child=self._button) 412 self._window_widget = None 413 if self._closing_call: 414 self._closing_call() 415 416 def set_choice(self, choice: str) -> None: 417 """Set the selected choice.""" 418 self._current_choice = choice 419 displayname: str | bui.Lstr 420 if len(self._choices_display) == len(self._choices): 421 displayname = self._choices_display[self._choices.index(choice)] 422 else: 423 displayname = choice 424 if self._button: 425 bui.buttonwidget(edit=self._button, label=displayname)
A complete popup-menu control.
This creates a button and wrangles its pop-up menu.
PopupMenu( parent: _bauiv1.Widget, position: tuple[float, float], choices: Sequence[str], *, current_choice: str | None = None, on_value_change_call: Optional[Callable[[str], Any]] = None, opening_call: Optional[Callable[[], Any]] = None, closing_call: Optional[Callable[[], Any]] = None, width: float = 230.0, maxwidth: float | None = None, scale: float | None = None, choices_disabled: Optional[Sequence[str]] = None, choices_display: Optional[Sequence[babase.Lstr]] = None, button_size: tuple[float, float] = (160.0, 50.0), autoselect: bool = True)
302 def __init__( 303 self, 304 parent: bui.Widget, 305 position: tuple[float, float], 306 choices: Sequence[str], 307 *, 308 current_choice: str | None = None, 309 on_value_change_call: Callable[[str], Any] | None = None, 310 opening_call: Callable[[], Any] | None = None, 311 closing_call: Callable[[], Any] | None = None, 312 width: float = 230.0, 313 maxwidth: float | None = None, 314 scale: float | None = None, 315 choices_disabled: Sequence[str] | None = None, 316 choices_display: Sequence[bui.Lstr] | None = None, 317 button_size: tuple[float, float] = (160.0, 50.0), 318 autoselect: bool = True, 319 ): 320 # pylint: disable=too-many-locals 321 if choices_disabled is None: 322 choices_disabled = [] 323 if choices_display is None: 324 choices_display = [] 325 assert bui.app.classic is not None 326 uiscale = bui.app.ui_v1.uiscale 327 if scale is None: 328 scale = ( 329 2.3 330 if uiscale is bui.UIScale.SMALL 331 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 332 ) 333 if current_choice not in choices: 334 current_choice = None 335 self._choices = list(choices) 336 if not choices: 337 raise TypeError('no choices given') 338 self._choices_display = list(choices_display) 339 self._choices_disabled = list(choices_disabled) 340 self._width = width 341 self._maxwidth = maxwidth 342 self._scale = scale 343 self._current_choice = ( 344 current_choice if current_choice is not None else self._choices[0] 345 ) 346 self._position = position 347 self._parent = parent 348 if not choices: 349 raise TypeError('Must pass at least one choice') 350 self._parent = parent 351 self._button_size = button_size 352 353 self._button = bui.buttonwidget( 354 parent=self._parent, 355 position=(self._position[0], self._position[1]), 356 autoselect=autoselect, 357 size=self._button_size, 358 scale=1.0, 359 label='', 360 on_activate_call=lambda: bui.apptimer(0, self._make_popup), 361 ) 362 self._on_value_change_call = None # Don't wanna call for initial set. 363 self._opening_call = opening_call 364 self._autoselect = autoselect 365 self._closing_call = closing_call 366 self.set_choice(self._current_choice) 367 self._on_value_change_call = on_value_change_call 368 self._window_widget: bui.Widget | None = None 369 370 # Complain if we outlive our button. 371 bui.uicleanupcheck(self, self._button)
def
get_window_widget(self) -> _bauiv1.Widget | None:
394 def get_window_widget(self) -> bui.Widget | None: 395 """Return the menu's window widget (or None if nonexistent).""" 396 return self._window_widget
Return the menu's window widget (or None if nonexistent).
def
set_choice(self, choice: str) -> None:
416 def set_choice(self, choice: str) -> None: 417 """Set the selected choice.""" 418 self._current_choice = choice 419 displayname: str | bui.Lstr 420 if len(self._choices_display) == len(self._choices): 421 displayname = self._choices_display[self._choices.index(choice)] 422 else: 423 displayname = choice 424 if self._button: 425 bui.buttonwidget(edit=self._button, label=displayname)
Set the selected choice.