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