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