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 border_opacity=0.5, 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() 294 295 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)
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 border_opacity=0.5, 208 ) 209 self._columnwidget = bui.columnwidget( 210 parent=self._scrollwidget, border=2, margin=0 211 ) 212 else: 213 self._offset_widget = bui.containerwidget( 214 parent=self.root_widget, 215 position=(12, 12), 216 size=(self._width - 40, self._height), 217 background=False, 218 ) 219 self._columnwidget = bui.columnwidget( 220 parent=self._offset_widget, border=2, margin=0 221 ) 222 for index, choice in enumerate(choices): 223 if len(choices_display_fin) == len(choices): 224 choice_display_name = choices_display_fin[index] 225 else: 226 choice_display_name = choice 227 inactive = choice in self._choices_disabled 228 wdg = bui.textwidget( 229 parent=self._columnwidget, 230 size=(self._width - 40, 28), 231 on_select_call=bui.Call(self._select, index), 232 click_activate=True, 233 color=( 234 (0.5, 0.5, 0.5, 0.5) 235 if inactive 236 else ( 237 (0.5, 1, 0.5, 1) 238 if choice == self._current_choice 239 else (0.8, 0.8, 0.8, 1.0) 240 ) 241 ), 242 padding=0, 243 maxwidth=maxwidth, 244 text=choice_display_name, 245 on_activate_call=self._activate, 246 v_align='center', 247 selectable=(not inactive), 248 glow_type='uniform', 249 ) 250 if choice == self._current_choice: 251 bui.containerwidget( 252 edit=self._columnwidget, 253 selected_child=wdg, 254 visible_child=wdg, 255 ) 256 257 # ok from now on our delegate can be called 258 self._delegate = weakref.ref(delegate) 259 self._done_building = True 260 261 def _select(self, index: int) -> None: 262 if self._done_building: 263 self._current_choice = self._choices[index] 264 265 def _activate(self) -> None: 266 bui.getsound('swish').play() 267 bui.apptimer(0.05, self._transition_out) 268 delegate = self._getdelegate() 269 if delegate is not None: 270 # Call this in a timer so it doesn't interfere with us killing 271 # our widgets and whatnot. 272 call = bui.Call( 273 delegate.popup_menu_selected_choice, self, self._current_choice 274 ) 275 bui.apptimer(0, call) 276 277 def _getdelegate(self) -> Any: 278 return None if self._delegate is None else self._delegate() 279 280 def _transition_out(self) -> None: 281 if not self.root_widget: 282 return 283 if not self._transitioning_out: 284 self._transitioning_out = True 285 delegate = self._getdelegate() 286 if delegate is not None: 287 delegate.popup_menu_closing(self) 288 bui.containerwidget(edit=self.root_widget, transition='out_scale') 289 290 @override 291 def on_popup_cancel(self) -> None: 292 if not self._transitioning_out: 293 bui.getsound('swish').play() 294 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 border_opacity=0.5, 208 ) 209 self._columnwidget = bui.columnwidget( 210 parent=self._scrollwidget, border=2, margin=0 211 ) 212 else: 213 self._offset_widget = bui.containerwidget( 214 parent=self.root_widget, 215 position=(12, 12), 216 size=(self._width - 40, self._height), 217 background=False, 218 ) 219 self._columnwidget = bui.columnwidget( 220 parent=self._offset_widget, border=2, margin=0 221 ) 222 for index, choice in enumerate(choices): 223 if len(choices_display_fin) == len(choices): 224 choice_display_name = choices_display_fin[index] 225 else: 226 choice_display_name = choice 227 inactive = choice in self._choices_disabled 228 wdg = bui.textwidget( 229 parent=self._columnwidget, 230 size=(self._width - 40, 28), 231 on_select_call=bui.Call(self._select, index), 232 click_activate=True, 233 color=( 234 (0.5, 0.5, 0.5, 0.5) 235 if inactive 236 else ( 237 (0.5, 1, 0.5, 1) 238 if choice == self._current_choice 239 else (0.8, 0.8, 0.8, 1.0) 240 ) 241 ), 242 padding=0, 243 maxwidth=maxwidth, 244 text=choice_display_name, 245 on_activate_call=self._activate, 246 v_align='center', 247 selectable=(not inactive), 248 glow_type='uniform', 249 ) 250 if choice == self._current_choice: 251 bui.containerwidget( 252 edit=self._columnwidget, 253 selected_child=wdg, 254 visible_child=wdg, 255 ) 256 257 # ok from now on our delegate can be called 258 self._delegate = weakref.ref(delegate) 259 self._done_building = True
@override
def
on_popup_cancel(self) -> None:
290 @override 291 def on_popup_cancel(self) -> None: 292 if not self._transitioning_out: 293 bui.getsound('swish').play() 294 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:
297class PopupMenu: 298 """A complete popup-menu control. 299 300 This creates a button and wrangles its pop-up menu. 301 """ 302 303 def __init__( 304 self, 305 parent: bui.Widget, 306 position: tuple[float, float], 307 choices: Sequence[str], 308 *, 309 current_choice: str | None = None, 310 on_value_change_call: Callable[[str], Any] | None = None, 311 opening_call: Callable[[], Any] | None = None, 312 closing_call: Callable[[], Any] | None = None, 313 width: float = 230.0, 314 maxwidth: float | None = None, 315 scale: float | None = None, 316 choices_disabled: Sequence[str] | None = None, 317 choices_display: Sequence[bui.Lstr] | None = None, 318 button_size: tuple[float, float] = (160.0, 50.0), 319 autoselect: bool = True, 320 ): 321 # pylint: disable=too-many-locals 322 if choices_disabled is None: 323 choices_disabled = [] 324 if choices_display is None: 325 choices_display = [] 326 assert bui.app.classic is not None 327 uiscale = bui.app.ui_v1.uiscale 328 if scale is None: 329 scale = ( 330 2.3 331 if uiscale is bui.UIScale.SMALL 332 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 333 ) 334 if current_choice not in choices: 335 current_choice = None 336 self._choices = list(choices) 337 if not choices: 338 raise TypeError('no choices given') 339 self._choices_display = list(choices_display) 340 self._choices_disabled = list(choices_disabled) 341 self._width = width 342 self._maxwidth = maxwidth 343 self._scale = scale 344 self._current_choice = ( 345 current_choice if current_choice is not None else self._choices[0] 346 ) 347 self._position = position 348 self._parent = parent 349 if not choices: 350 raise TypeError('Must pass at least one choice') 351 self._parent = parent 352 self._button_size = button_size 353 354 self._button = bui.buttonwidget( 355 parent=self._parent, 356 position=(self._position[0], self._position[1]), 357 autoselect=autoselect, 358 size=self._button_size, 359 scale=1.0, 360 label='', 361 on_activate_call=lambda: bui.apptimer(0, self._make_popup), 362 ) 363 self._on_value_change_call = None # Don't wanna call for initial set. 364 self._opening_call = opening_call 365 self._autoselect = autoselect 366 self._closing_call = closing_call 367 self.set_choice(self._current_choice) 368 self._on_value_change_call = on_value_change_call 369 self._window_widget: bui.Widget | None = None 370 371 # Complain if we outlive our button. 372 bui.uicleanupcheck(self, self._button) 373 374 def _make_popup(self) -> None: 375 if not self._button: 376 return 377 if self._opening_call: 378 self._opening_call() 379 self._window_widget = PopupMenuWindow( 380 position=self._button.get_screen_space_center(), 381 delegate=self, 382 width=self._width, 383 maxwidth=self._maxwidth, 384 scale=self._scale, 385 choices=self._choices, 386 current_choice=self._current_choice, 387 choices_disabled=self._choices_disabled, 388 choices_display=self._choices_display, 389 ).root_widget 390 391 def get_button(self) -> bui.Widget: 392 """Return the menu's button widget.""" 393 return self._button 394 395 def get_window_widget(self) -> bui.Widget | None: 396 """Return the menu's window widget (or None if nonexistent).""" 397 return self._window_widget 398 399 def popup_menu_selected_choice( 400 self, popup_window: PopupWindow, choice: str 401 ) -> None: 402 """Called when a choice is selected.""" 403 del popup_window # Unused here. 404 self.set_choice(choice) 405 if self._on_value_change_call: 406 self._on_value_change_call(choice) 407 408 def popup_menu_closing(self, popup_window: PopupWindow) -> None: 409 """Called when the menu is closing.""" 410 del popup_window # Unused here. 411 if self._button: 412 bui.containerwidget(edit=self._parent, selected_child=self._button) 413 self._window_widget = None 414 if self._closing_call: 415 self._closing_call() 416 417 def set_choice(self, choice: str) -> None: 418 """Set the selected choice.""" 419 self._current_choice = choice 420 displayname: str | bui.Lstr 421 if len(self._choices_display) == len(self._choices): 422 displayname = self._choices_display[self._choices.index(choice)] 423 else: 424 displayname = choice 425 if self._button: 426 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)
303 def __init__( 304 self, 305 parent: bui.Widget, 306 position: tuple[float, float], 307 choices: Sequence[str], 308 *, 309 current_choice: str | None = None, 310 on_value_change_call: Callable[[str], Any] | None = None, 311 opening_call: Callable[[], Any] | None = None, 312 closing_call: Callable[[], Any] | None = None, 313 width: float = 230.0, 314 maxwidth: float | None = None, 315 scale: float | None = None, 316 choices_disabled: Sequence[str] | None = None, 317 choices_display: Sequence[bui.Lstr] | None = None, 318 button_size: tuple[float, float] = (160.0, 50.0), 319 autoselect: bool = True, 320 ): 321 # pylint: disable=too-many-locals 322 if choices_disabled is None: 323 choices_disabled = [] 324 if choices_display is None: 325 choices_display = [] 326 assert bui.app.classic is not None 327 uiscale = bui.app.ui_v1.uiscale 328 if scale is None: 329 scale = ( 330 2.3 331 if uiscale is bui.UIScale.SMALL 332 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 333 ) 334 if current_choice not in choices: 335 current_choice = None 336 self._choices = list(choices) 337 if not choices: 338 raise TypeError('no choices given') 339 self._choices_display = list(choices_display) 340 self._choices_disabled = list(choices_disabled) 341 self._width = width 342 self._maxwidth = maxwidth 343 self._scale = scale 344 self._current_choice = ( 345 current_choice if current_choice is not None else self._choices[0] 346 ) 347 self._position = position 348 self._parent = parent 349 if not choices: 350 raise TypeError('Must pass at least one choice') 351 self._parent = parent 352 self._button_size = button_size 353 354 self._button = bui.buttonwidget( 355 parent=self._parent, 356 position=(self._position[0], self._position[1]), 357 autoselect=autoselect, 358 size=self._button_size, 359 scale=1.0, 360 label='', 361 on_activate_call=lambda: bui.apptimer(0, self._make_popup), 362 ) 363 self._on_value_change_call = None # Don't wanna call for initial set. 364 self._opening_call = opening_call 365 self._autoselect = autoselect 366 self._closing_call = closing_call 367 self.set_choice(self._current_choice) 368 self._on_value_change_call = on_value_change_call 369 self._window_widget: bui.Widget | None = None 370 371 # Complain if we outlive our button. 372 bui.uicleanupcheck(self, self._button)
def
get_window_widget(self) -> _bauiv1.Widget | None:
395 def get_window_widget(self) -> bui.Widget | None: 396 """Return the menu's window widget (or None if nonexistent).""" 397 return self._window_widget
Return the menu's window widget (or None if nonexistent).
def
set_choice(self, choice: str) -> None:
417 def set_choice(self, choice: str) -> None: 418 """Set the selected choice.""" 419 self._current_choice = choice 420 displayname: str | bui.Lstr 421 if len(self._choices_display) == len(self._choices): 422 displayname = self._choices_display[self._choices.index(choice)] 423 else: 424 displayname = choice 425 if self._button: 426 bui.buttonwidget(edit=self._button, label=displayname)
Set the selected choice.