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