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