bauiv1
Ballistica user interface api version 1
1# Released under the MIT License. See LICENSE for details. 2# 3"""Ballistica user interface api version 1""" 4 5# ba_meta require api 9 6 7# The stuff we expose here at the top level is our 'public' api. 8# It should only be imported by code outside of this package or 9# from 'if TYPE_CHECKING' blocks (which will not exec at runtime). 10# Code within our package should import things directly from their 11# submodules. 12 13from __future__ import annotations 14 15# pylint: disable=redefined-builtin 16 17import logging 18 19from efro.util import set_canonical_module_names 20from babase import ( 21 add_clean_frame_callback, 22 allows_ticket_sales, 23 app, 24 AppIntent, 25 AppIntentDefault, 26 AppIntentExec, 27 AppMode, 28 appname, 29 appnameupper, 30 apptime, 31 AppTime, 32 apptimer, 33 AppTimer, 34 Call, 35 fullscreen_control_available, 36 fullscreen_control_get, 37 fullscreen_control_key_shortcut, 38 fullscreen_control_set, 39 charstr, 40 clipboard_is_supported, 41 clipboard_set_text, 42 commit_app_config, 43 ContextRef, 44 displaytime, 45 DisplayTime, 46 displaytimer, 47 DisplayTimer, 48 do_once, 49 fade_screen, 50 get_display_resolution, 51 get_input_idle_time, 52 get_ip_address_type, 53 get_low_level_config_value, 54 get_max_graphics_quality, 55 get_remote_app_name, 56 get_replays_dir, 57 get_string_height, 58 get_string_width, 59 get_type_name, 60 getclass, 61 have_permission, 62 in_logic_thread, 63 in_main_menu, 64 increment_analytics_count, 65 is_browser_likely_available, 66 is_xcode_build, 67 lock_all_input, 68 LoginAdapter, 69 LoginInfo, 70 Lstr, 71 native_review_request, 72 native_review_request_supported, 73 NotFoundError, 74 open_file_externally, 75 open_url, 76 overlay_web_browser_close, 77 overlay_web_browser_is_open, 78 overlay_web_browser_is_supported, 79 overlay_web_browser_open_url, 80 Permission, 81 Plugin, 82 PluginSpec, 83 pushcall, 84 quit, 85 QuitType, 86 request_permission, 87 safecolor, 88 screenmessage, 89 set_analytics_screen, 90 set_low_level_config_value, 91 set_ui_input_device, 92 SpecialChar, 93 supports_max_fps, 94 supports_vsync, 95 timestring, 96 UIScale, 97 unlock_all_input, 98 WeakCall, 99 workspaces_in_use, 100) 101 102from _bauiv1 import ( 103 buttonwidget, 104 checkboxwidget, 105 columnwidget, 106 containerwidget, 107 get_qrcode_texture, 108 get_special_widget, 109 getmesh, 110 getsound, 111 gettexture, 112 hscrollwidget, 113 imagewidget, 114 Mesh, 115 rowwidget, 116 scrollwidget, 117 set_party_window_open, 118 Sound, 119 Texture, 120 textwidget, 121 uibounds, 122 Widget, 123 widget, 124) 125from bauiv1._keyboard import Keyboard 126from bauiv1._uitypes import ( 127 Window, 128 MainWindowState, 129 BasicMainWindowState, 130 uicleanupcheck, 131 MainWindow, 132) 133from bauiv1._appsubsystem import UIV1AppSubsystem 134 135__all__ = [ 136 'add_clean_frame_callback', 137 'allows_ticket_sales', 138 'app', 139 'AppIntent', 140 'AppIntentDefault', 141 'AppIntentExec', 142 'AppMode', 143 'appname', 144 'appnameupper', 145 'appnameupper', 146 'apptime', 147 'AppTime', 148 'apptimer', 149 'AppTimer', 150 'BasicMainWindowState', 151 'buttonwidget', 152 'Call', 153 'fullscreen_control_available', 154 'fullscreen_control_get', 155 'fullscreen_control_key_shortcut', 156 'fullscreen_control_set', 157 'charstr', 158 'checkboxwidget', 159 'clipboard_is_supported', 160 'clipboard_set_text', 161 'columnwidget', 162 'commit_app_config', 163 'containerwidget', 164 'ContextRef', 165 'displaytime', 166 'DisplayTime', 167 'displaytimer', 168 'DisplayTimer', 169 'do_once', 170 'fade_screen', 171 'get_display_resolution', 172 'get_input_idle_time', 173 'get_ip_address_type', 174 'get_low_level_config_value', 175 'get_max_graphics_quality', 176 'get_qrcode_texture', 177 'get_remote_app_name', 178 'get_replays_dir', 179 'get_special_widget', 180 'get_string_height', 181 'get_string_width', 182 'get_type_name', 183 'getclass', 184 'getmesh', 185 'getsound', 186 'gettexture', 187 'have_permission', 188 'hscrollwidget', 189 'imagewidget', 190 'in_logic_thread', 191 'in_main_menu', 192 'increment_analytics_count', 193 'is_browser_likely_available', 194 'is_xcode_build', 195 'Keyboard', 196 'lock_all_input', 197 'LoginAdapter', 198 'LoginInfo', 199 'Lstr', 200 'MainWindow', 201 'MainWindowState', 202 'Mesh', 203 'native_review_request', 204 'native_review_request_supported', 205 'NotFoundError', 206 'open_file_externally', 207 'open_url', 208 'overlay_web_browser_close', 209 'overlay_web_browser_is_open', 210 'overlay_web_browser_is_supported', 211 'overlay_web_browser_open_url', 212 'Permission', 213 'Plugin', 214 'PluginSpec', 215 'pushcall', 216 'quit', 217 'QuitType', 218 'request_permission', 219 'rowwidget', 220 'safecolor', 221 'screenmessage', 222 'scrollwidget', 223 'set_analytics_screen', 224 'set_low_level_config_value', 225 'set_party_window_open', 226 'set_ui_input_device', 227 'Sound', 228 'SpecialChar', 229 'supports_max_fps', 230 'supports_vsync', 231 'Texture', 232 'textwidget', 233 'timestring', 234 'uibounds', 235 'uicleanupcheck', 236 'UIScale', 237 'UIV1AppSubsystem', 238 'unlock_all_input', 239 'WeakCall', 240 'widget', 241 'Widget', 242 'Window', 243 'workspaces_in_use', 244] 245 246# We want stuff to show up as bauiv1.Foo instead of bauiv1._sub.Foo. 247set_canonical_module_names(globals()) 248 249# Sanity check: we want to keep ballistica's dependencies and 250# bootstrapping order clearly defined; let's check a few particular 251# modules to make sure they never directly or indirectly import us 252# before their own execs complete. 253if __debug__: 254 for _mdl in 'babase', '_babase': 255 if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): 256 logging.warning( 257 '%s was imported before %s finished importing;' 258 ' should not happen.', 259 __name__, 260 _mdl, 261 )
13class AppIntent: 14 """A high level directive given to the app. 15 16 Category: **App Classes** 17 """
A high level directive given to the app.
Category: App Classes
Tells the app to simply run in its default mode.
24class AppIntentExec(AppIntent): 25 """Tells the app to exec some Python code.""" 26 27 def __init__(self, code: str): 28 self.code = code
Tells the app to exec some Python code.
14class AppMode: 15 """A high level mode for the app. 16 17 Category: **App Classes** 18 19 """ 20 21 @classmethod 22 def get_app_experience(cls) -> AppExperience: 23 """Return the overall experience provided by this mode.""" 24 raise NotImplementedError('AppMode subclasses must override this.') 25 26 @classmethod 27 def can_handle_intent(cls, intent: AppIntent) -> bool: 28 """Return whether this mode can handle the provided intent. 29 30 For this to return True, the AppMode must claim to support the 31 provided intent (via its _supports_intent() method) AND the 32 AppExperience associated with the AppMode must be supported by 33 the current app and runtime environment. 34 """ 35 # TODO: check AppExperience. 36 return cls._supports_intent(intent) 37 38 @classmethod 39 def _supports_intent(cls, intent: AppIntent) -> bool: 40 """Return whether our mode can handle the provided intent. 41 42 AppModes should override this to define what they can handle. 43 Note that AppExperience does not have to be considered here; that 44 is handled automatically by the can_handle_intent() call.""" 45 raise NotImplementedError('AppMode subclasses must override this.') 46 47 def handle_intent(self, intent: AppIntent) -> None: 48 """Handle an intent.""" 49 raise NotImplementedError('AppMode subclasses must override this.') 50 51 def on_activate(self) -> None: 52 """Called when the mode is being activated.""" 53 54 def on_deactivate(self) -> None: 55 """Called when the mode is being deactivated.""" 56 57 def on_app_active_changed(self) -> None: 58 """Called when ba*.app.active changes while this mode is active. 59 60 The app-mode may want to take action such as pausing a running 61 game in such cases. 62 """
A high level mode for the app.
Category: App Classes
21 @classmethod 22 def get_app_experience(cls) -> AppExperience: 23 """Return the overall experience provided by this mode.""" 24 raise NotImplementedError('AppMode subclasses must override this.')
Return the overall experience provided by this mode.
26 @classmethod 27 def can_handle_intent(cls, intent: AppIntent) -> bool: 28 """Return whether this mode can handle the provided intent. 29 30 For this to return True, the AppMode must claim to support the 31 provided intent (via its _supports_intent() method) AND the 32 AppExperience associated with the AppMode must be supported by 33 the current app and runtime environment. 34 """ 35 # TODO: check AppExperience. 36 return cls._supports_intent(intent)
Return whether this mode can handle the provided intent.
For this to return True, the AppMode must claim to support the provided intent (via its _supports_intent() method) AND the AppExperience associated with the AppMode must be supported by the current app and runtime environment.
47 def handle_intent(self, intent: AppIntent) -> None: 48 """Handle an intent.""" 49 raise NotImplementedError('AppMode subclasses must override this.')
Handle an intent.
57 def on_app_active_changed(self) -> None: 58 """Called when ba*.app.active changes while this mode is active. 59 60 The app-mode may want to take action such as pausing a running 61 game in such cases. 62 """
Called when ba*.app.active changes while this mode is active.
The app-mode may want to take action such as pausing a running game in such cases.
552def apptime() -> babase.AppTime: 553 """Return the current app-time in seconds. 554 555 Category: **General Utility Functions** 556 557 App-time is a monotonic time value; it starts at 0.0 when the app 558 launches and will never jump by large amounts or go backwards, even if 559 the system time changes. Its progression will pause when the app is in 560 a suspended state. 561 562 Note that the AppTime returned here is simply float; it just has a 563 unique type in the type-checker's eyes to help prevent it from being 564 accidentally used with time functionality expecting other time types. 565 """ 566 import babase # pylint: disable=cyclic-import 567 568 return babase.AppTime(0.0)
Return the current app-time in seconds.
Category: General Utility Functions
App-time is a monotonic time value; it starts at 0.0 when the app launches and will never jump by large amounts or go backwards, even if the system time changes. Its progression will pause when the app is in a suspended state.
Note that the AppTime returned here is simply float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.
571def apptimer(time: float, call: Callable[[], Any]) -> None: 572 """Schedule a callable object to run based on app-time. 573 574 Category: **General Utility Functions** 575 576 This function creates a one-off timer which cannot be canceled or 577 modified once created. If you require the ability to do so, or need 578 a repeating timer, use the babase.AppTimer class instead. 579 580 ##### Arguments 581 ###### time (float) 582 > Length of time in seconds that the timer will wait before firing. 583 584 ###### call (Callable[[], Any]) 585 > A callable Python object. Note that the timer will retain a 586 strong reference to the callable for as long as the timer exists, so you 587 may want to look into concepts such as babase.WeakCall if that is not 588 desired. 589 590 ##### Examples 591 Print some stuff through time: 592 >>> babase.screenmessage('hello from now!') 593 >>> babase.apptimer(1.0, babase.Call(babase.screenmessage, 594 'hello from the future!')) 595 >>> babase.apptimer(2.0, babase.Call(babase.screenmessage, 596 ... 'hello from the future 2!')) 597 """ 598 return None
Schedule a callable object to run based on app-time.
Category: General Utility Functions
This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.AppTimer class instead.
Arguments
time (float)
Length of time in seconds that the timer will wait before firing.
call (Callable[[], Any])
A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
Examples
Print some stuff through time:
>>> babase.screenmessage('hello from now!')
>>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
'hello from the future!'))
>>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
... 'hello from the future 2!'))
53class AppTimer: 54 """Timers are used to run code at later points in time. 55 56 Category: **General Utility Classes** 57 58 This class encapsulates a timer based on app-time. 59 The underlying timer will be destroyed when this object is no longer 60 referenced. If you do not want to worry about keeping a reference to 61 your timer around, use the babase.apptimer() function instead to get a 62 one-off timer. 63 64 ##### Arguments 65 ###### time 66 > Length of time in seconds that the timer will wait before firing. 67 68 ###### call 69 > A callable Python object. Remember that the timer will retain a 70 strong reference to the callable for as long as it exists, so you 71 may want to look into concepts such as babase.WeakCall if that is not 72 desired. 73 74 ###### repeat 75 > If True, the timer will fire repeatedly, with each successive 76 firing having the same delay as the first. 77 78 ##### Example 79 80 Use a Timer object to print repeatedly for a few seconds: 81 ... def say_it(): 82 ... babase.screenmessage('BADGER!') 83 ... def stop_saying_it(): 84 ... global g_timer 85 ... g_timer = None 86 ... babase.screenmessage('MUSHROOM MUSHROOM!') 87 ... # Create our timer; it will run as long as we have the self.t ref. 88 ... g_timer = babase.AppTimer(0.3, say_it, repeat=True) 89 ... # Now fire off a one-shot timer to kill it. 90 ... babase.apptimer(3.89, stop_saying_it) 91 """ 92 93 def __init__( 94 self, time: float, call: Callable[[], Any], repeat: bool = False 95 ) -> None: 96 pass
Timers are used to run code at later points in time.
Category: General Utility Classes
This class encapsulates a timer based on app-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.apptimer() function instead to get a one-off timer.
Arguments
time
Length of time in seconds that the timer will wait before firing.
call
A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
repeat
If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.
Example
Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.AppTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.apptimer(3.89, stop_saying_it)
233class BasicMainWindowState(MainWindowState): 234 """A basic MainWindowState holding a lambda to recreate a MainWindow.""" 235 236 def __init__( 237 self, 238 create_call: Callable[ 239 [ 240 Literal['in_right', 'in_left', 'in_scale'] | None, 241 bauiv1.Widget | None, 242 ], 243 bauiv1.MainWindow, 244 ], 245 ) -> None: 246 super().__init__() 247 self.create_call = create_call 248 249 @override 250 def create_window( 251 self, 252 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 253 origin_widget: bauiv1.Widget | None = None, 254 ) -> bauiv1.MainWindow: 255 return self.create_call(transition, origin_widget)
A basic MainWindowState holding a lambda to recreate a MainWindow.
249 @override 250 def create_window( 251 self, 252 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 253 origin_widget: bauiv1.Widget | None = None, 254 ) -> bauiv1.MainWindow: 255 return self.create_call(transition, origin_widget)
Create a window based on this state.
WindowState child classes should override this to recreate their particular type of window.
Inherited Members
621def charstr(char_id: babase.SpecialChar) -> str: 622 """Get a unicode string representing a special character. 623 624 Category: **General Utility Functions** 625 626 Note that these utilize the private-use block of unicode characters 627 (U+E000-U+F8FF) and are specific to the game; exporting or rendering 628 them elsewhere will be meaningless. 629 630 See babase.SpecialChar for the list of available characters. 631 """ 632 return str()
Get a unicode string representing a special character.
Category: General Utility Functions
Note that these utilize the private-use block of unicode characters (U+E000-U+F8FF) and are specific to the game; exporting or rendering them elsewhere will be meaningless.
See babase.SpecialChar for the list of available characters.
200def checkboxwidget( 201 *, 202 edit: bauiv1.Widget | None = None, 203 parent: bauiv1.Widget | None = None, 204 size: Sequence[float] | None = None, 205 position: Sequence[float] | None = None, 206 text: str | bauiv1.Lstr | None = None, 207 value: bool | None = None, 208 on_value_change_call: Callable[[bool], None] | None = None, 209 on_select_call: Callable[[], None] | None = None, 210 text_scale: float | None = None, 211 textcolor: Sequence[float] | None = None, 212 scale: float | None = None, 213 is_radio_button: bool | None = None, 214 maxwidth: float | None = None, 215 autoselect: bool | None = None, 216 color: Sequence[float] | None = None, 217) -> bauiv1.Widget: 218 """Create or edit a check-box widget. 219 220 Category: **User Interface Functions** 221 222 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 223 a new one is created and returned. Arguments that are not set to None 224 are applied to the Widget. 225 """ 226 import bauiv1 # pylint: disable=cyclic-import 227 228 return bauiv1.Widget()
Create or edit a check-box widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
657def clipboard_is_supported() -> bool: 658 """Return whether this platform supports clipboard operations at all. 659 660 Category: **General Utility Functions** 661 662 If this returns False, UIs should not show 'copy to clipboard' 663 buttons, etc. 664 """ 665 return bool()
Return whether this platform supports clipboard operations at all.
Category: General Utility Functions
If this returns False, UIs should not show 'copy to clipboard' buttons, etc.
668def clipboard_set_text(value: str) -> None: 669 """Copy a string to the system clipboard. 670 671 Category: **General Utility Functions** 672 673 Ensure that babase.clipboard_is_supported() returns True before adding 674 buttons/etc. that make use of this functionality. 675 """ 676 return None
Copy a string to the system clipboard.
Category: General Utility Functions
Ensure that babase.clipboard_is_supported() returns True before adding buttons/etc. that make use of this functionality.
231def columnwidget( 232 *, 233 edit: bauiv1.Widget | None = None, 234 parent: bauiv1.Widget | None = None, 235 size: Sequence[float] | None = None, 236 position: Sequence[float] | None = None, 237 background: bool | None = None, 238 selected_child: bauiv1.Widget | None = None, 239 visible_child: bauiv1.Widget | None = None, 240 single_depth: bool | None = None, 241 print_list_exit_instructions: bool | None = None, 242 left_border: float | None = None, 243 top_border: float | None = None, 244 bottom_border: float | None = None, 245 selection_loops_to_parent: bool | None = None, 246 border: float | None = None, 247 margin: float | None = None, 248 claims_left_right: bool | None = None, 249 claims_tab: bool | None = None, 250) -> bauiv1.Widget: 251 """Create or edit a column widget. 252 253 Category: **User Interface Functions** 254 255 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 256 a new one is created and returned. Arguments that are not set to None 257 are applied to the Widget. 258 """ 259 import bauiv1 # pylint: disable=cyclic-import 260 261 return bauiv1.Widget()
Create or edit a column widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
264def containerwidget( 265 *, 266 edit: bauiv1.Widget | None = None, 267 parent: bauiv1.Widget | None = None, 268 size: Sequence[float] | None = None, 269 position: Sequence[float] | None = None, 270 background: bool | None = None, 271 selected_child: bauiv1.Widget | None = None, 272 transition: str | None = None, 273 cancel_button: bauiv1.Widget | None = None, 274 start_button: bauiv1.Widget | None = None, 275 root_selectable: bool | None = None, 276 on_activate_call: Callable[[], None] | None = None, 277 claims_left_right: bool | None = None, 278 claims_tab: bool | None = None, 279 selection_loops: bool | None = None, 280 selection_loops_to_parent: bool | None = None, 281 scale: float | None = None, 282 on_outside_click_call: Callable[[], None] | None = None, 283 single_depth: bool | None = None, 284 visible_child: bauiv1.Widget | None = None, 285 stack_offset: Sequence[float] | None = None, 286 color: Sequence[float] | None = None, 287 on_cancel_call: Callable[[], None] | None = None, 288 print_list_exit_instructions: bool | None = None, 289 click_activate: bool | None = None, 290 always_highlight: bool | None = None, 291 selectable: bool | None = None, 292 scale_origin_stack_offset: Sequence[float] | None = None, 293 toolbar_visibility: ( 294 Literal[ 295 'menu_minimal', 296 'menu_minimal_no_back', 297 'menu_full', 298 'menu_full_no_back', 299 'menu_store', 300 'menu_store_no_back', 301 'menu_in_game', 302 'menu_tokens', 303 'get_tokens', 304 'inherit', 305 ] 306 | None 307 ) = None, 308 on_select_call: Callable[[], None] | None = None, 309 claim_outside_clicks: bool | None = None, 310 claims_up_down: bool | None = None, 311) -> bauiv1.Widget: 312 """Create or edit a container widget. 313 314 Category: **User Interface Functions** 315 316 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 317 a new one is created and returned. Arguments that are not set to None 318 are applied to the Widget. 319 """ 320 import bauiv1 # pylint: disable=cyclic-import 321 322 return bauiv1.Widget()
Create or edit a container widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
148class ContextRef: 149 """Store or use a ballistica context. 150 151 Category: **General Utility Classes** 152 153 Many operations such as bascenev1.newnode() or bascenev1.gettexture() 154 operate implicitly on a current 'context'. A context is some sort of 155 state that functionality can implicitly use. Context determines, for 156 example, which scene nodes or textures get added to without having to 157 specify it explicitly in the newnode()/gettexture() call. Contexts can 158 also affect object lifecycles; for example a babase.ContextCall will 159 become a no-op when the context it was created in is destroyed. 160 161 In general, if you are a modder, you should not need to worry about 162 contexts; mod code should mostly be getting run in the correct 163 context and timers and other callbacks will take care of saving 164 and restoring contexts automatically. There may be rare cases, 165 however, where you need to deal directly with contexts, and that is 166 where this class comes in. 167 168 Creating a babase.ContextRef() will capture a reference to the current 169 context. Other modules may provide ways to access their contexts; for 170 example a bascenev1.Activity instance has a 'context' attribute. You 171 can also use babase.ContextRef.empty() to create a reference to *no* 172 context. Some code such as UI calls may expect this and may complain 173 if you try to use them within a context. 174 175 ##### Usage 176 ContextRefs are generally used with the Python 'with' statement, which 177 sets the context they point to as current on entry and resets it to 178 the previous value on exit. 179 180 ##### Example 181 Explicitly create a few UI bits with no context set. 182 (UI stuff may complain if called within a context): 183 >>> with bui.ContextRef.empty(): 184 ... my_container = bui.containerwidget() 185 """ 186 187 def __init__( 188 self, 189 ) -> None: 190 pass 191 192 def __enter__(self) -> None: 193 """Support for "with" statement.""" 194 pass 195 196 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: 197 """Support for "with" statement.""" 198 pass 199 200 @classmethod 201 def empty(cls) -> ContextRef: 202 """Return a ContextRef pointing to no context. 203 204 This is useful when code should be run free of a context. 205 For example, UI code generally insists on being run this way. 206 Otherwise, callbacks set on the UI could inadvertently stop working 207 due to a game activity ending, which would be unintuitive behavior. 208 """ 209 return ContextRef() 210 211 def is_empty(self) -> bool: 212 """Whether the context was created as empty.""" 213 return bool() 214 215 def is_expired(self) -> bool: 216 """Whether the context has expired.""" 217 return bool()
Store or use a ballistica context.
Category: General Utility Classes
Many operations such as bascenev1.newnode() or bascenev1.gettexture() operate implicitly on a current 'context'. A context is some sort of state that functionality can implicitly use. Context determines, for example, which scene nodes or textures get added to without having to specify it explicitly in the newnode()/gettexture() call. Contexts can also affect object lifecycles; for example a babase.ContextCall will become a no-op when the context it was created in is destroyed.
In general, if you are a modder, you should not need to worry about contexts; mod code should mostly be getting run in the correct context and timers and other callbacks will take care of saving and restoring contexts automatically. There may be rare cases, however, where you need to deal directly with contexts, and that is where this class comes in.
Creating a babase.ContextRef() will capture a reference to the current context. Other modules may provide ways to access their contexts; for example a bascenev1.Activity instance has a 'context' attribute. You can also use babase.ContextRef.empty() to create a reference to no context. Some code such as UI calls may expect this and may complain if you try to use them within a context.
Usage
ContextRefs are generally used with the Python 'with' statement, which sets the context they point to as current on entry and resets it to the previous value on exit.
Example
Explicitly create a few UI bits with no context set. (UI stuff may complain if called within a context):
>>> with bui.ContextRef.empty():
... my_container = bui.containerwidget()
200 @classmethod 201 def empty(cls) -> ContextRef: 202 """Return a ContextRef pointing to no context. 203 204 This is useful when code should be run free of a context. 205 For example, UI code generally insists on being run this way. 206 Otherwise, callbacks set on the UI could inadvertently stop working 207 due to a game activity ending, which would be unintuitive behavior. 208 """ 209 return ContextRef()
Return a ContextRef pointing to no context.
This is useful when code should be run free of a context. For example, UI code generally insists on being run this way. Otherwise, callbacks set on the UI could inadvertently stop working due to a game activity ending, which would be unintuitive behavior.
761def displaytime() -> babase.DisplayTime: 762 """Return the current display-time in seconds. 763 764 Category: **General Utility Functions** 765 766 Display-time is a time value intended to be used for animation and other 767 visual purposes. It will generally increment by a consistent amount each 768 frame. It will pass at an overall similar rate to AppTime, but trades 769 accuracy for smoothness. 770 771 Note that the value returned here is simply a float; it just has a 772 unique type in the type-checker's eyes to help prevent it from being 773 accidentally used with time functionality expecting other time types. 774 """ 775 import babase # pylint: disable=cyclic-import 776 777 return babase.DisplayTime(0.0)
Return the current display-time in seconds.
Category: General Utility Functions
Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.
Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.
780def displaytimer(time: float, call: Callable[[], Any]) -> None: 781 """Schedule a callable object to run based on display-time. 782 783 Category: **General Utility Functions** 784 785 This function creates a one-off timer which cannot be canceled or 786 modified once created. If you require the ability to do so, or need 787 a repeating timer, use the babase.DisplayTimer class instead. 788 789 Display-time is a time value intended to be used for animation and other 790 visual purposes. It will generally increment by a consistent amount each 791 frame. It will pass at an overall similar rate to AppTime, but trades 792 accuracy for smoothness. 793 794 ##### Arguments 795 ###### time (float) 796 > Length of time in seconds that the timer will wait before firing. 797 798 ###### call (Callable[[], Any]) 799 > A callable Python object. Note that the timer will retain a 800 strong reference to the callable for as long as the timer exists, so you 801 may want to look into concepts such as babase.WeakCall if that is not 802 desired. 803 804 ##### Examples 805 Print some stuff through time: 806 >>> babase.screenmessage('hello from now!') 807 >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage, 808 ... 'hello from the future!')) 809 >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage, 810 ... 'hello from the future 2!')) 811 """ 812 return None
Schedule a callable object to run based on display-time.
Category: General Utility Functions
This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.DisplayTimer class instead.
Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.
Arguments
time (float)
Length of time in seconds that the timer will wait before firing.
call (Callable[[], Any])
A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
Examples
Print some stuff through time:
>>> babase.screenmessage('hello from now!')
>>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
... 'hello from the future!'))
>>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
... 'hello from the future 2!'))
220class DisplayTimer: 221 """Timers are used to run code at later points in time. 222 223 Category: **General Utility Classes** 224 225 This class encapsulates a timer based on display-time. 226 The underlying timer will be destroyed when this object is no longer 227 referenced. If you do not want to worry about keeping a reference to 228 your timer around, use the babase.displaytimer() function instead to get a 229 one-off timer. 230 231 Display-time is a time value intended to be used for animation and 232 other visual purposes. It will generally increment by a consistent 233 amount each frame. It will pass at an overall similar rate to AppTime, 234 but trades accuracy for smoothness. 235 236 ##### Arguments 237 ###### time 238 > Length of time in seconds that the timer will wait before firing. 239 240 ###### call 241 > A callable Python object. Remember that the timer will retain a 242 strong reference to the callable for as long as it exists, so you 243 may want to look into concepts such as babase.WeakCall if that is not 244 desired. 245 246 ###### repeat 247 > If True, the timer will fire repeatedly, with each successive 248 firing having the same delay as the first. 249 250 ##### Example 251 252 Use a Timer object to print repeatedly for a few seconds: 253 ... def say_it(): 254 ... babase.screenmessage('BADGER!') 255 ... def stop_saying_it(): 256 ... global g_timer 257 ... g_timer = None 258 ... babase.screenmessage('MUSHROOM MUSHROOM!') 259 ... # Create our timer; it will run as long as we have the self.t ref. 260 ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) 261 ... # Now fire off a one-shot timer to kill it. 262 ... babase.displaytimer(3.89, stop_saying_it) 263 """ 264 265 def __init__( 266 self, time: float, call: Callable[[], Any], repeat: bool = False 267 ) -> None: 268 pass
Timers are used to run code at later points in time.
Category: General Utility Classes
This class encapsulates a timer based on display-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.displaytimer() function instead to get a one-off timer.
Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.
Arguments
time
Length of time in seconds that the timer will wait before firing.
call
A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
repeat
If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.
Example
Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.displaytimer(3.89, stop_saying_it)
820def do_once() -> bool: 821 """Return whether this is the first time running a line of code. 822 823 Category: **General Utility Functions** 824 825 This is used by 'print_once()' type calls to keep from overflowing 826 logs. The call functions by registering the filename and line where 827 The call is made from. Returns True if this location has not been 828 registered already, and False if it has. 829 830 ##### Example 831 This print will only fire for the first loop iteration: 832 >>> for i in range(10): 833 ... if babase.do_once(): 834 ... print('HelloWorld once from loop!') 835 """ 836 return bool()
Return whether this is the first time running a line of code.
Category: General Utility Functions
This is used by 'print_once()' type calls to keep from overflowing logs. The call functions by registering the filename and line where The call is made from. Returns True if this location has not been registered already, and False if it has.
Example
This print will only fire for the first loop iteration:
>>> for i in range(10):
... if babase.do_once():
... print('HelloWorld once from loop!')
1000def get_input_idle_time() -> float: 1001 """Return seconds since any local input occurred (touch, keypress, etc.).""" 1002 return float()
Return seconds since any local input occurred (touch, keypress, etc.).
45def get_ip_address_type(addr: str) -> socket.AddressFamily: 46 """Return socket.AF_INET6 or socket.AF_INET4 for the provided address.""" 47 48 version = ipaddress.ip_address(addr).version 49 if version == 4: 50 return socket.AF_INET 51 assert version == 6 52 return socket.AF_INET6
Return socket.AF_INET6 or socket.AF_INET4 for the provided address.
325def get_qrcode_texture(url: str) -> bauiv1.Texture: 326 """Return a QR code texture. 327 328 The provided url must be 64 bytes or less. 329 """ 330 import bauiv1 # pylint: disable=cyclic-import 331 332 return bauiv1.Texture()
Return a QR code texture.
The provided url must be 64 bytes or less.
112def get_type_name(cls: type) -> str: 113 """Return a full type name including module for a class.""" 114 return f'{cls.__module__}.{cls.__name__}'
Return a full type name including module for a class.
71def getclass( 72 name: str, subclassof: type[T], check_sdlib_modulename_clash: bool = False 73) -> type[T]: 74 """Given a full class name such as foo.bar.MyClass, return the class. 75 76 Category: **General Utility Functions** 77 78 The class will be checked to make sure it is a subclass of the provided 79 'subclassof' class, and a TypeError will be raised if not. 80 """ 81 import importlib 82 83 splits = name.split('.') 84 modulename = '.'.join(splits[:-1]) 85 classname = splits[-1] 86 if modulename in sys.stdlib_module_names and check_sdlib_modulename_clash: 87 raise Exception(f'{modulename} is an inbuilt module.') 88 module = importlib.import_module(modulename) 89 cls: type = getattr(module, classname) 90 91 if not issubclass(cls, subclassof): 92 raise TypeError(f'{name} is not a subclass of {subclassof}.') 93 return cls
Given a full class name such as foo.bar.MyClass, return the class.
Category: General Utility Functions
The class will be checked to make sure it is a subclass of the provided 'subclassof' class, and a TypeError will be raised if not.
359def getmesh(name: str) -> bauiv1.Mesh: 360 """Load a mesh for use solely in the local user interface.""" 361 import bauiv1 # pylint: disable=cyclic-import 362 363 return bauiv1.Mesh()
Load a mesh for use solely in the local user interface.
366def getsound(name: str) -> bauiv1.Sound: 367 """Load a sound for use in the ui.""" 368 import bauiv1 # pylint: disable=cyclic-import 369 370 return bauiv1.Sound()
Load a sound for use in the ui.
373def gettexture(name: str) -> bauiv1.Texture: 374 """Load a texture for use in the ui.""" 375 import bauiv1 # pylint: disable=cyclic-import 376 377 return bauiv1.Texture()
Load a texture for use in the ui.
380def hscrollwidget( 381 *, 382 edit: bauiv1.Widget | None = None, 383 parent: bauiv1.Widget | None = None, 384 size: Sequence[float] | None = None, 385 position: Sequence[float] | None = None, 386 background: bool | None = None, 387 selected_child: bauiv1.Widget | None = None, 388 capture_arrows: bool | None = None, 389 on_select_call: Callable[[], None] | None = None, 390 center_small_content: bool | None = None, 391 color: Sequence[float] | None = None, 392 highlight: bool | None = None, 393 border_opacity: float | None = None, 394 simple_culling_h: float | None = None, 395 claims_left_right: bool | None = None, 396 claims_up_down: bool | None = None, 397 claims_tab: bool | None = None, 398) -> bauiv1.Widget: 399 """Create or edit a horizontal scroll widget. 400 401 Category: **User Interface Functions** 402 403 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 404 a new one is created and returned. Arguments that are not set to None 405 are applied to the Widget. 406 """ 407 import bauiv1 # pylint: disable=cyclic-import 408 409 return bauiv1.Widget()
Create or edit a horizontal scroll widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
412def imagewidget( 413 *, 414 edit: bauiv1.Widget | None = None, 415 parent: bauiv1.Widget | None = None, 416 size: Sequence[float] | None = None, 417 position: Sequence[float] | None = None, 418 color: Sequence[float] | None = None, 419 texture: bauiv1.Texture | None = None, 420 opacity: float | None = None, 421 mesh_transparent: bauiv1.Mesh | None = None, 422 mesh_opaque: bauiv1.Mesh | None = None, 423 has_alpha_channel: bool = True, 424 tint_texture: bauiv1.Texture | None = None, 425 tint_color: Sequence[float] | None = None, 426 transition_delay: float | None = None, 427 draw_controller: bauiv1.Widget | None = None, 428 tint2_color: Sequence[float] | None = None, 429 tilt_scale: float | None = None, 430 mask_texture: bauiv1.Texture | None = None, 431 radial_amount: float | None = None, 432 draw_controller_mult: float | None = None, 433) -> bauiv1.Widget: 434 """Create or edit an image widget. 435 436 Category: **User Interface Functions** 437 438 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 439 a new one is created and returned. Arguments that are not set to None 440 are applied to the Widget. 441 """ 442 import bauiv1 # pylint: disable=cyclic-import 443 444 return bauiv1.Widget()
Create or edit an image widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
27def is_browser_likely_available() -> bool: 28 """Return whether a browser likely exists on the current device. 29 30 category: General Utility Functions 31 32 If this returns False you may want to avoid calling babase.show_url() 33 with any lengthy addresses. (ba.show_url() will display an address 34 as a string in a window if unable to bring up a browser, but that 35 is only useful for simple URLs.) 36 """ 37 app = _babase.app 38 39 if app.classic is None: 40 logging.warning( 41 'is_browser_likely_available() needs to be updated' 42 ' to work without classic.' 43 ) 44 return True 45 46 platform = app.classic.platform 47 hastouchscreen = _babase.hastouchscreen() 48 49 # If we're on a vr device or an android device with no touchscreen, 50 # assume no browser. 51 # FIXME: Might not be the case anymore; should make this definable 52 # at the platform level. 53 if app.env.vr or (platform == 'android' and not hastouchscreen): 54 return False 55 56 # Anywhere else assume we've got one. 57 return True
Return whether a browser likely exists on the current device.
category: General Utility Functions
If this returns False you may want to avoid calling babase.show_url() with any lengthy addresses. (ba.show_url() will display an address as a string in a window if unable to bring up a browser, but that is only useful for simple URLs.)
14class Keyboard: 15 """Chars definitions for on-screen keyboard. 16 17 Category: **App Classes** 18 19 Keyboards are discoverable by the meta-tag system 20 and the user can select which one they want to use. 21 On-screen keyboard uses chars from active babase.Keyboard. 22 """ 23 24 name: str 25 """Displays when user selecting this keyboard.""" 26 27 chars: list[tuple[str, ...]] 28 """Used for row/column lengths.""" 29 30 pages: dict[str, tuple[str, ...]] 31 """Extra chars like emojis.""" 32 33 nums: tuple[str, ...] 34 """The 'num' page."""
Chars definitions for on-screen keyboard.
Category: App Classes
Keyboards are discoverable by the meta-tag system and the user can select which one they want to use. On-screen keyboard uses chars from active babase.Keyboard.
32class LoginAdapter: 33 """Allows using implicit login types in an explicit way. 34 35 Some login types such as Google Play Game Services or Game Center are 36 basically always present and often do not provide a way to log out 37 from within a running app, so this adapter exists to use them in a 38 flexible manner by 'attaching' and 'detaching' from an always-present 39 login, allowing for its use alongside other login types. It also 40 provides common functionality for server-side account verification and 41 other handy bits. 42 """ 43 44 @dataclass 45 class SignInResult: 46 """Describes the final result of a sign-in attempt.""" 47 48 credentials: str 49 50 @dataclass 51 class ImplicitLoginState: 52 """Describes the current state of an implicit login.""" 53 54 login_id: str 55 display_name: str 56 57 def __init__(self, login_type: LoginType): 58 assert _babase.in_logic_thread() 59 self.login_type = login_type 60 self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = ( 61 None 62 ) 63 self._on_app_loading_called = False 64 self._implicit_login_state_dirty = False 65 self._back_end_active = False 66 67 # Which login of our type (if any) is associated with the 68 # current active primary account. 69 self._active_login_id: str | None = None 70 71 self._last_sign_in_time: float | None = None 72 self._last_sign_in_desc: str | None = None 73 74 def on_app_loading(self) -> None: 75 """Should be called for each adapter in on_app_loading.""" 76 77 assert not self._on_app_loading_called 78 self._on_app_loading_called = True 79 80 # Any implicit state we received up until now needs to be pushed 81 # to the app account subsystem. 82 self._update_implicit_login_state() 83 84 def set_implicit_login_state( 85 self, state: ImplicitLoginState | None 86 ) -> None: 87 """Keep the adapter informed of implicit login states. 88 89 This should be called by the adapter back-end when an account 90 of their associated type gets logged in or out. 91 """ 92 assert _babase.in_logic_thread() 93 94 # Ignore redundant sets. 95 if state == self._implicit_login_state: 96 return 97 98 if DEBUG_LOG: 99 if state is None: 100 logging.debug( 101 'LoginAdapter: %s implicit state changed;' 102 ' now signed out.', 103 self.login_type.name, 104 ) 105 else: 106 logging.debug( 107 'LoginAdapter: %s implicit state changed;' 108 ' now signed in as %s.', 109 self.login_type.name, 110 state.display_name, 111 ) 112 113 self._implicit_login_state = state 114 self._implicit_login_state_dirty = True 115 116 # (possibly) push it to the app for handling. 117 self._update_implicit_login_state() 118 119 # This might affect whether we consider that back-end as 'active'. 120 self._update_back_end_active() 121 122 def set_active_logins(self, logins: dict[LoginType, str]) -> None: 123 """Keep the adapter informed of actively used logins. 124 125 This should be called by the app's account subsystem to 126 keep adapters up to date on the full set of logins attached 127 to the currently-in-use account. 128 Note that the logins dict passed in should be immutable as 129 only a reference to it is stored, not a copy. 130 """ 131 assert _babase.in_logic_thread() 132 if DEBUG_LOG: 133 logging.debug( 134 'LoginAdapter: %s adapter got active logins %s.', 135 self.login_type.name, 136 {k: v[:4] + '...' + v[-4:] for k, v in logins.items()}, 137 ) 138 139 self._active_login_id = logins.get(self.login_type) 140 self._update_back_end_active() 141 142 def on_back_end_active_change(self, active: bool) -> None: 143 """Called when active state for the back-end is (possibly) changing. 144 145 Meant to be overridden by subclasses. 146 Being active means that the implicit login provided by the back-end 147 is actually being used by the app. It should therefore register 148 unlocked achievements, leaderboard scores, allow viewing native 149 UIs, etc. When not active it should ignore everything and behave 150 as if signed out, even if it technically is still signed in. 151 """ 152 assert _babase.in_logic_thread() 153 del active # Unused. 154 155 @final 156 def sign_in( 157 self, 158 result_cb: Callable[[LoginAdapter, SignInResult | Exception], None], 159 description: str, 160 ) -> None: 161 """Attempt to sign in via this adapter. 162 163 This can be called even if the back-end is not implicitly signed in; 164 the adapter will attempt to sign in if possible. An exception will 165 be returned if the sign-in attempt fails. 166 """ 167 168 assert _babase.in_logic_thread() 169 170 # Have been seeing multiple sign-in attempts come through 171 # nearly simultaneously which can be problematic server-side. 172 # Let's error if a sign-in attempt is made within a few seconds 173 # of the last one to try and address this. 174 now = time.monotonic() 175 appnow = _babase.apptime() 176 if self._last_sign_in_time is not None: 177 since_last = now - self._last_sign_in_time 178 if since_last < 1.0: 179 logging.warning( 180 'LoginAdapter: %s adapter sign_in() called too soon' 181 ' (%.2fs) after last; this-desc="%s", last-desc="%s",' 182 ' ba-app-time=%.2f.', 183 self.login_type.name, 184 since_last, 185 description, 186 self._last_sign_in_desc, 187 appnow, 188 ) 189 _babase.pushcall( 190 partial( 191 result_cb, 192 self, 193 RuntimeError('sign_in called too soon after last.'), 194 ) 195 ) 196 return 197 198 self._last_sign_in_desc = description 199 self._last_sign_in_time = now 200 201 if DEBUG_LOG: 202 logging.debug( 203 'LoginAdapter: %s adapter sign_in() called;' 204 ' fetching sign-in-token...', 205 self.login_type.name, 206 ) 207 208 def _got_sign_in_token_result(result: str | None) -> None: 209 import bacommon.cloud 210 211 # Failed to get a sign-in-token. 212 if result is None: 213 if DEBUG_LOG: 214 logging.debug( 215 'LoginAdapter: %s adapter sign-in-token fetch failed;' 216 ' aborting sign-in.', 217 self.login_type.name, 218 ) 219 _babase.pushcall( 220 partial( 221 result_cb, 222 self, 223 RuntimeError('fetch-sign-in-token failed.'), 224 ) 225 ) 226 return 227 228 # Got a sign-in token! Now pass it to the cloud which will use 229 # it to verify our identity and give us app credentials on 230 # success. 231 if DEBUG_LOG: 232 logging.debug( 233 'LoginAdapter: %s adapter sign-in-token fetch succeeded;' 234 ' passing to cloud for verification...', 235 self.login_type.name, 236 ) 237 238 def _got_sign_in_response( 239 response: bacommon.cloud.SignInResponse | Exception, 240 ) -> None: 241 # This likely means we couldn't communicate with the server. 242 if isinstance(response, Exception): 243 if DEBUG_LOG: 244 logging.debug( 245 'LoginAdapter: %s adapter got error' 246 ' sign-in response: %s', 247 self.login_type.name, 248 response, 249 ) 250 _babase.pushcall(partial(result_cb, self, response)) 251 else: 252 # This means our credentials were explicitly rejected. 253 if response.credentials is None: 254 result2: LoginAdapter.SignInResult | Exception = ( 255 RuntimeError('Sign-in-token was rejected.') 256 ) 257 else: 258 if DEBUG_LOG: 259 logging.debug( 260 'LoginAdapter: %s adapter got successful' 261 ' sign-in response', 262 self.login_type.name, 263 ) 264 result2 = self.SignInResult( 265 credentials=response.credentials 266 ) 267 _babase.pushcall(partial(result_cb, self, result2)) 268 269 assert _babase.app.plus is not None 270 _babase.app.plus.cloud.send_message_cb( 271 bacommon.cloud.SignInMessage( 272 self.login_type, 273 result, 274 description=description, 275 apptime=appnow, 276 ), 277 on_response=_got_sign_in_response, 278 ) 279 280 # Kick off the sign-in process by fetching a sign-in token. 281 self.get_sign_in_token(completion_cb=_got_sign_in_token_result) 282 283 def is_back_end_active(self) -> bool: 284 """Is this adapter's back-end currently active?""" 285 return self._back_end_active 286 287 def get_sign_in_token( 288 self, completion_cb: Callable[[str | None], None] 289 ) -> None: 290 """Get a sign-in token from the adapter back end. 291 292 This token is then passed to the master-server to complete the 293 sign-in process. The adapter can use this opportunity to bring 294 up account creation UI, call its internal sign_in function, etc. 295 as needed. The provided completion_cb should then be called with 296 either a token or None if sign in failed or was cancelled. 297 """ 298 299 # Default implementation simply fails immediately. 300 _babase.pushcall(partial(completion_cb, None)) 301 302 def _update_implicit_login_state(self) -> None: 303 # If we've received an implicit login state, schedule it to be 304 # sent along to the app. We wait until on-app-loading has been 305 # called so that account-client-v2 has had a chance to load 306 # any existing state so it can properly respond to this. 307 if self._implicit_login_state_dirty and self._on_app_loading_called: 308 309 if DEBUG_LOG: 310 logging.debug( 311 'LoginAdapter: %s adapter sending' 312 ' implicit-state-changed to app.', 313 self.login_type.name, 314 ) 315 316 assert _babase.app.plus is not None 317 _babase.pushcall( 318 partial( 319 _babase.app.plus.accounts.on_implicit_login_state_changed, 320 self.login_type, 321 self._implicit_login_state, 322 ) 323 ) 324 self._implicit_login_state_dirty = False 325 326 def _update_back_end_active(self) -> None: 327 was_active = self._back_end_active 328 if self._implicit_login_state is None: 329 is_active = False 330 else: 331 is_active = ( 332 self._implicit_login_state.login_id == self._active_login_id 333 ) 334 if was_active != is_active: 335 if DEBUG_LOG: 336 logging.debug( 337 'LoginAdapter: %s adapter back-end-active is now %s.', 338 self.login_type.name, 339 is_active, 340 ) 341 self.on_back_end_active_change(is_active) 342 self._back_end_active = is_active
Allows using implicit login types in an explicit way.
Some login types such as Google Play Game Services or Game Center are basically always present and often do not provide a way to log out from within a running app, so this adapter exists to use them in a flexible manner by 'attaching' and 'detaching' from an always-present login, allowing for its use alongside other login types. It also provides common functionality for server-side account verification and other handy bits.
57 def __init__(self, login_type: LoginType): 58 assert _babase.in_logic_thread() 59 self.login_type = login_type 60 self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = ( 61 None 62 ) 63 self._on_app_loading_called = False 64 self._implicit_login_state_dirty = False 65 self._back_end_active = False 66 67 # Which login of our type (if any) is associated with the 68 # current active primary account. 69 self._active_login_id: str | None = None 70 71 self._last_sign_in_time: float | None = None 72 self._last_sign_in_desc: str | None = None
74 def on_app_loading(self) -> None: 75 """Should be called for each adapter in on_app_loading.""" 76 77 assert not self._on_app_loading_called 78 self._on_app_loading_called = True 79 80 # Any implicit state we received up until now needs to be pushed 81 # to the app account subsystem. 82 self._update_implicit_login_state()
Should be called for each adapter in on_app_loading.
84 def set_implicit_login_state( 85 self, state: ImplicitLoginState | None 86 ) -> None: 87 """Keep the adapter informed of implicit login states. 88 89 This should be called by the adapter back-end when an account 90 of their associated type gets logged in or out. 91 """ 92 assert _babase.in_logic_thread() 93 94 # Ignore redundant sets. 95 if state == self._implicit_login_state: 96 return 97 98 if DEBUG_LOG: 99 if state is None: 100 logging.debug( 101 'LoginAdapter: %s implicit state changed;' 102 ' now signed out.', 103 self.login_type.name, 104 ) 105 else: 106 logging.debug( 107 'LoginAdapter: %s implicit state changed;' 108 ' now signed in as %s.', 109 self.login_type.name, 110 state.display_name, 111 ) 112 113 self._implicit_login_state = state 114 self._implicit_login_state_dirty = True 115 116 # (possibly) push it to the app for handling. 117 self._update_implicit_login_state() 118 119 # This might affect whether we consider that back-end as 'active'. 120 self._update_back_end_active()
Keep the adapter informed of implicit login states.
This should be called by the adapter back-end when an account of their associated type gets logged in or out.
122 def set_active_logins(self, logins: dict[LoginType, str]) -> None: 123 """Keep the adapter informed of actively used logins. 124 125 This should be called by the app's account subsystem to 126 keep adapters up to date on the full set of logins attached 127 to the currently-in-use account. 128 Note that the logins dict passed in should be immutable as 129 only a reference to it is stored, not a copy. 130 """ 131 assert _babase.in_logic_thread() 132 if DEBUG_LOG: 133 logging.debug( 134 'LoginAdapter: %s adapter got active logins %s.', 135 self.login_type.name, 136 {k: v[:4] + '...' + v[-4:] for k, v in logins.items()}, 137 ) 138 139 self._active_login_id = logins.get(self.login_type) 140 self._update_back_end_active()
Keep the adapter informed of actively used logins.
This should be called by the app's account subsystem to keep adapters up to date on the full set of logins attached to the currently-in-use account. Note that the logins dict passed in should be immutable as only a reference to it is stored, not a copy.
142 def on_back_end_active_change(self, active: bool) -> None: 143 """Called when active state for the back-end is (possibly) changing. 144 145 Meant to be overridden by subclasses. 146 Being active means that the implicit login provided by the back-end 147 is actually being used by the app. It should therefore register 148 unlocked achievements, leaderboard scores, allow viewing native 149 UIs, etc. When not active it should ignore everything and behave 150 as if signed out, even if it technically is still signed in. 151 """ 152 assert _babase.in_logic_thread() 153 del active # Unused.
Called when active state for the back-end is (possibly) changing.
Meant to be overridden by subclasses. Being active means that the implicit login provided by the back-end is actually being used by the app. It should therefore register unlocked achievements, leaderboard scores, allow viewing native UIs, etc. When not active it should ignore everything and behave as if signed out, even if it technically is still signed in.
155 @final 156 def sign_in( 157 self, 158 result_cb: Callable[[LoginAdapter, SignInResult | Exception], None], 159 description: str, 160 ) -> None: 161 """Attempt to sign in via this adapter. 162 163 This can be called even if the back-end is not implicitly signed in; 164 the adapter will attempt to sign in if possible. An exception will 165 be returned if the sign-in attempt fails. 166 """ 167 168 assert _babase.in_logic_thread() 169 170 # Have been seeing multiple sign-in attempts come through 171 # nearly simultaneously which can be problematic server-side. 172 # Let's error if a sign-in attempt is made within a few seconds 173 # of the last one to try and address this. 174 now = time.monotonic() 175 appnow = _babase.apptime() 176 if self._last_sign_in_time is not None: 177 since_last = now - self._last_sign_in_time 178 if since_last < 1.0: 179 logging.warning( 180 'LoginAdapter: %s adapter sign_in() called too soon' 181 ' (%.2fs) after last; this-desc="%s", last-desc="%s",' 182 ' ba-app-time=%.2f.', 183 self.login_type.name, 184 since_last, 185 description, 186 self._last_sign_in_desc, 187 appnow, 188 ) 189 _babase.pushcall( 190 partial( 191 result_cb, 192 self, 193 RuntimeError('sign_in called too soon after last.'), 194 ) 195 ) 196 return 197 198 self._last_sign_in_desc = description 199 self._last_sign_in_time = now 200 201 if DEBUG_LOG: 202 logging.debug( 203 'LoginAdapter: %s adapter sign_in() called;' 204 ' fetching sign-in-token...', 205 self.login_type.name, 206 ) 207 208 def _got_sign_in_token_result(result: str | None) -> None: 209 import bacommon.cloud 210 211 # Failed to get a sign-in-token. 212 if result is None: 213 if DEBUG_LOG: 214 logging.debug( 215 'LoginAdapter: %s adapter sign-in-token fetch failed;' 216 ' aborting sign-in.', 217 self.login_type.name, 218 ) 219 _babase.pushcall( 220 partial( 221 result_cb, 222 self, 223 RuntimeError('fetch-sign-in-token failed.'), 224 ) 225 ) 226 return 227 228 # Got a sign-in token! Now pass it to the cloud which will use 229 # it to verify our identity and give us app credentials on 230 # success. 231 if DEBUG_LOG: 232 logging.debug( 233 'LoginAdapter: %s adapter sign-in-token fetch succeeded;' 234 ' passing to cloud for verification...', 235 self.login_type.name, 236 ) 237 238 def _got_sign_in_response( 239 response: bacommon.cloud.SignInResponse | Exception, 240 ) -> None: 241 # This likely means we couldn't communicate with the server. 242 if isinstance(response, Exception): 243 if DEBUG_LOG: 244 logging.debug( 245 'LoginAdapter: %s adapter got error' 246 ' sign-in response: %s', 247 self.login_type.name, 248 response, 249 ) 250 _babase.pushcall(partial(result_cb, self, response)) 251 else: 252 # This means our credentials were explicitly rejected. 253 if response.credentials is None: 254 result2: LoginAdapter.SignInResult | Exception = ( 255 RuntimeError('Sign-in-token was rejected.') 256 ) 257 else: 258 if DEBUG_LOG: 259 logging.debug( 260 'LoginAdapter: %s adapter got successful' 261 ' sign-in response', 262 self.login_type.name, 263 ) 264 result2 = self.SignInResult( 265 credentials=response.credentials 266 ) 267 _babase.pushcall(partial(result_cb, self, result2)) 268 269 assert _babase.app.plus is not None 270 _babase.app.plus.cloud.send_message_cb( 271 bacommon.cloud.SignInMessage( 272 self.login_type, 273 result, 274 description=description, 275 apptime=appnow, 276 ), 277 on_response=_got_sign_in_response, 278 ) 279 280 # Kick off the sign-in process by fetching a sign-in token. 281 self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
Attempt to sign in via this adapter.
This can be called even if the back-end is not implicitly signed in; the adapter will attempt to sign in if possible. An exception will be returned if the sign-in attempt fails.
283 def is_back_end_active(self) -> bool: 284 """Is this adapter's back-end currently active?""" 285 return self._back_end_active
Is this adapter's back-end currently active?
287 def get_sign_in_token( 288 self, completion_cb: Callable[[str | None], None] 289 ) -> None: 290 """Get a sign-in token from the adapter back end. 291 292 This token is then passed to the master-server to complete the 293 sign-in process. The adapter can use this opportunity to bring 294 up account creation UI, call its internal sign_in function, etc. 295 as needed. The provided completion_cb should then be called with 296 either a token or None if sign in failed or was cancelled. 297 """ 298 299 # Default implementation simply fails immediately. 300 _babase.pushcall(partial(completion_cb, None))
Get a sign-in token from the adapter back end.
This token is then passed to the master-server to complete the sign-in process. The adapter can use this opportunity to bring up account creation UI, call its internal sign_in function, etc. as needed. The provided completion_cb should then be called with either a token or None if sign in failed or was cancelled.
44 @dataclass 45 class SignInResult: 46 """Describes the final result of a sign-in attempt.""" 47 48 credentials: str
Describes the final result of a sign-in attempt.
50 @dataclass 51 class ImplicitLoginState: 52 """Describes the current state of an implicit login.""" 53 54 login_id: str 55 display_name: str
Describes the current state of an implicit login.
25@dataclass 26class LoginInfo: 27 """Basic info about a login available in the app.plus.accounts section.""" 28 29 name: str
Basic info about a login available in the app.plus.accounts section.
489class Lstr: 490 """Used to define strings in a language-independent way. 491 492 Category: **General Utility Classes** 493 494 These should be used whenever possible in place of hard-coded 495 strings so that in-game or UI elements show up correctly on all 496 clients in their currently-active language. 497 498 To see available resource keys, look at any of the bs_language_*.py 499 files in the game or the translations pages at 500 legacy.ballistica.net/translate. 501 502 ##### Examples 503 EXAMPLE 1: specify a string from a resource path 504 >>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText') 505 506 EXAMPLE 2: specify a translated string via a category and english 507 value; if a translated value is available, it will be used; otherwise 508 the english value will be. To see available translation categories, 509 look under the 'translations' resource section. 510 >>> mynode.text = babase.Lstr(translate=('gameDescriptions', 511 ... 'Defeat all enemies')) 512 513 EXAMPLE 3: specify a raw value and some substitutions. Substitutions 514 can be used with resource and translate modes as well. 515 >>> mynode.text = babase.Lstr(value='${A} / ${B}', 516 ... subs=[('${A}', str(score)), ('${B}', str(total))]) 517 518 EXAMPLE 4: babase.Lstr's can be nested. This example would display the 519 resource at res_a but replace ${NAME} with the value of the 520 resource at res_b 521 >>> mytextnode.text = babase.Lstr( 522 ... resource='res_a', 523 ... subs=[('${NAME}', babase.Lstr(resource='res_b'))]) 524 """ 525 526 # pylint: disable=dangerous-default-value 527 # noinspection PyDefaultArgument 528 @overload 529 def __init__( 530 self, 531 *, 532 resource: str, 533 fallback_resource: str = '', 534 fallback_value: str = '', 535 subs: Sequence[tuple[str, str | Lstr]] = [], 536 ) -> None: 537 """Create an Lstr from a string resource.""" 538 539 # noinspection PyShadowingNames,PyDefaultArgument 540 @overload 541 def __init__( 542 self, 543 *, 544 translate: tuple[str, str], 545 subs: Sequence[tuple[str, str | Lstr]] = [], 546 ) -> None: 547 """Create an Lstr by translating a string in a category.""" 548 549 # noinspection PyDefaultArgument 550 @overload 551 def __init__( 552 self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = [] 553 ) -> None: 554 """Create an Lstr from a raw string value.""" 555 556 # pylint: enable=redefined-outer-name, dangerous-default-value 557 558 def __init__(self, *args: Any, **keywds: Any) -> None: 559 """Instantiate a Lstr. 560 561 Pass a value for either 'resource', 'translate', 562 or 'value'. (see Lstr help for examples). 563 'subs' can be a sequence of 2-member sequences consisting of values 564 and replacements. 565 'fallback_resource' can be a resource key that will be used if the 566 main one is not present for 567 the current language in place of falling back to the english value 568 ('resource' mode only). 569 'fallback_value' can be a literal string that will be used if neither 570 the resource nor the fallback resource is found ('resource' mode only). 571 """ 572 # pylint: disable=too-many-branches 573 if args: 574 raise TypeError('Lstr accepts only keyword arguments') 575 576 # Basically just store the exact args they passed. 577 # However if they passed any Lstr values for subs, 578 # replace them with that Lstr's dict. 579 self.args = keywds 580 our_type = type(self) 581 582 if isinstance(self.args.get('value'), our_type): 583 raise TypeError("'value' must be a regular string; not an Lstr") 584 585 if 'subs' in self.args: 586 subs_new = [] 587 for key, value in keywds['subs']: 588 if isinstance(value, our_type): 589 subs_new.append((key, value.args)) 590 else: 591 subs_new.append((key, value)) 592 self.args['subs'] = subs_new 593 594 # As of protocol 31 we support compact key names 595 # ('t' instead of 'translate', etc). Convert as needed. 596 if 'translate' in keywds: 597 keywds['t'] = keywds['translate'] 598 del keywds['translate'] 599 if 'resource' in keywds: 600 keywds['r'] = keywds['resource'] 601 del keywds['resource'] 602 if 'value' in keywds: 603 keywds['v'] = keywds['value'] 604 del keywds['value'] 605 if 'fallback' in keywds: 606 from babase import _error 607 608 _error.print_error( 609 'deprecated "fallback" arg passed to Lstr(); use ' 610 'either "fallback_resource" or "fallback_value"', 611 once=True, 612 ) 613 keywds['f'] = keywds['fallback'] 614 del keywds['fallback'] 615 if 'fallback_resource' in keywds: 616 keywds['f'] = keywds['fallback_resource'] 617 del keywds['fallback_resource'] 618 if 'subs' in keywds: 619 keywds['s'] = keywds['subs'] 620 del keywds['subs'] 621 if 'fallback_value' in keywds: 622 keywds['fv'] = keywds['fallback_value'] 623 del keywds['fallback_value'] 624 625 def evaluate(self) -> str: 626 """Evaluate the Lstr and returns a flat string in the current language. 627 628 You should avoid doing this as much as possible and instead pass 629 and store Lstr values. 630 """ 631 return _babase.evaluate_lstr(self._get_json()) 632 633 def is_flat_value(self) -> bool: 634 """Return whether the Lstr is a 'flat' value. 635 636 This is defined as a simple string value incorporating no 637 translations, resources, or substitutions. In this case it may 638 be reasonable to replace it with a raw string value, perform 639 string manipulation on it, etc. 640 """ 641 return bool('v' in self.args and not self.args.get('s', [])) 642 643 def _get_json(self) -> str: 644 try: 645 return json.dumps(self.args, separators=(',', ':')) 646 except Exception: 647 from babase import _error 648 649 _error.print_exception('_get_json failed for', self.args) 650 return 'JSON_ERR' 651 652 @override 653 def __str__(self) -> str: 654 return '<ba.Lstr: ' + self._get_json() + '>' 655 656 @override 657 def __repr__(self) -> str: 658 return '<ba.Lstr: ' + self._get_json() + '>' 659 660 @staticmethod 661 def from_json(json_string: str) -> babase.Lstr: 662 """Given a json string, returns a babase.Lstr. Does no validation.""" 663 lstr = Lstr(value='') 664 lstr.args = json.loads(json_string) 665 return lstr
Used to define strings in a language-independent way.
Category: General Utility Classes
These should be used whenever possible in place of hard-coded strings so that in-game or UI elements show up correctly on all clients in their currently-active language.
To see available resource keys, look at any of the bs_language_*.py files in the game or the translations pages at legacy.ballistica.net/translate.
Examples
EXAMPLE 1: specify a string from a resource path
>>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')
EXAMPLE 2: specify a translated string via a category and english value; if a translated value is available, it will be used; otherwise the english value will be. To see available translation categories, look under the 'translations' resource section.
>>> mynode.text = babase.Lstr(translate=('gameDescriptions',
... 'Defeat all enemies'))
EXAMPLE 3: specify a raw value and some substitutions. Substitutions can be used with resource and translate modes as well.
>>> mynode.text = babase.Lstr(value='${A} / ${B}',
... subs=[('${A}', str(score)), ('${B}', str(total))])
EXAMPLE 4: babase.Lstr's can be nested. This example would display the resource at res_a but replace ${NAME} with the value of the resource at res_b
>>> mytextnode.text = babase.Lstr(
... resource='res_a',
... subs=[('${NAME}', babase.Lstr(resource='res_b'))])
558 def __init__(self, *args: Any, **keywds: Any) -> None: 559 """Instantiate a Lstr. 560 561 Pass a value for either 'resource', 'translate', 562 or 'value'. (see Lstr help for examples). 563 'subs' can be a sequence of 2-member sequences consisting of values 564 and replacements. 565 'fallback_resource' can be a resource key that will be used if the 566 main one is not present for 567 the current language in place of falling back to the english value 568 ('resource' mode only). 569 'fallback_value' can be a literal string that will be used if neither 570 the resource nor the fallback resource is found ('resource' mode only). 571 """ 572 # pylint: disable=too-many-branches 573 if args: 574 raise TypeError('Lstr accepts only keyword arguments') 575 576 # Basically just store the exact args they passed. 577 # However if they passed any Lstr values for subs, 578 # replace them with that Lstr's dict. 579 self.args = keywds 580 our_type = type(self) 581 582 if isinstance(self.args.get('value'), our_type): 583 raise TypeError("'value' must be a regular string; not an Lstr") 584 585 if 'subs' in self.args: 586 subs_new = [] 587 for key, value in keywds['subs']: 588 if isinstance(value, our_type): 589 subs_new.append((key, value.args)) 590 else: 591 subs_new.append((key, value)) 592 self.args['subs'] = subs_new 593 594 # As of protocol 31 we support compact key names 595 # ('t' instead of 'translate', etc). Convert as needed. 596 if 'translate' in keywds: 597 keywds['t'] = keywds['translate'] 598 del keywds['translate'] 599 if 'resource' in keywds: 600 keywds['r'] = keywds['resource'] 601 del keywds['resource'] 602 if 'value' in keywds: 603 keywds['v'] = keywds['value'] 604 del keywds['value'] 605 if 'fallback' in keywds: 606 from babase import _error 607 608 _error.print_error( 609 'deprecated "fallback" arg passed to Lstr(); use ' 610 'either "fallback_resource" or "fallback_value"', 611 once=True, 612 ) 613 keywds['f'] = keywds['fallback'] 614 del keywds['fallback'] 615 if 'fallback_resource' in keywds: 616 keywds['f'] = keywds['fallback_resource'] 617 del keywds['fallback_resource'] 618 if 'subs' in keywds: 619 keywds['s'] = keywds['subs'] 620 del keywds['subs'] 621 if 'fallback_value' in keywds: 622 keywds['fv'] = keywds['fallback_value'] 623 del keywds['fallback_value']
Instantiate a Lstr.
Pass a value for either 'resource', 'translate', or 'value'. (see Lstr help for examples). 'subs' can be a sequence of 2-member sequences consisting of values and replacements. 'fallback_resource' can be a resource key that will be used if the main one is not present for the current language in place of falling back to the english value ('resource' mode only). 'fallback_value' can be a literal string that will be used if neither the resource nor the fallback resource is found ('resource' mode only).
625 def evaluate(self) -> str: 626 """Evaluate the Lstr and returns a flat string in the current language. 627 628 You should avoid doing this as much as possible and instead pass 629 and store Lstr values. 630 """ 631 return _babase.evaluate_lstr(self._get_json())
Evaluate the Lstr and returns a flat string in the current language.
You should avoid doing this as much as possible and instead pass and store Lstr values.
633 def is_flat_value(self) -> bool: 634 """Return whether the Lstr is a 'flat' value. 635 636 This is defined as a simple string value incorporating no 637 translations, resources, or substitutions. In this case it may 638 be reasonable to replace it with a raw string value, perform 639 string manipulation on it, etc. 640 """ 641 return bool('v' in self.args and not self.args.get('s', []))
Return whether the Lstr is a 'flat' value.
This is defined as a simple string value incorporating no translations, resources, or substitutions. In this case it may be reasonable to replace it with a raw string value, perform string manipulation on it, etc.
49class MainWindow(Window): 50 """A special window that can be used as a main window.""" 51 52 def __init__( 53 self, 54 root_widget: bauiv1.Widget, 55 transition: str | None, 56 origin_widget: bauiv1.Widget | None, 57 cleanupcheck: bool = True, 58 ): 59 """Create a MainWindow given a root widget and transition info. 60 61 Automatically handles in and out transitions on the provided widget, 62 so there is no need to set transitions when creating it. 63 """ 64 # A back-state supplied by the ui system. 65 self.main_window_back_state: MainWindowState | None = None 66 67 self.main_window_is_top_level: bool = False 68 69 # Windows can be flagged as auxiliary when not related to the 70 # main UI task at hand. UI code may choose to handle auxiliary 71 # windows in special ways, such as by implicitly replacing 72 # existing auxiliary windows with new ones instead of keeping 73 # old ones as back targets. 74 self.main_window_is_auxiliary: bool = False 75 76 self._main_window_transition = transition 77 self._main_window_origin_widget = origin_widget 78 super().__init__(root_widget, cleanupcheck) 79 80 scale_origin: tuple[float, float] | None 81 if origin_widget is not None: 82 self._main_window_transition_out = 'out_scale' 83 scale_origin = origin_widget.get_screen_space_center() 84 transition = 'in_scale' 85 else: 86 self._main_window_transition_out = 'out_right' 87 scale_origin = None 88 _bauiv1.containerwidget( 89 edit=root_widget, 90 transition=transition, 91 scale_origin_stack_offset=scale_origin, 92 ) 93 94 def main_window_close(self, transition: str | None = None) -> None: 95 """Get window transitioning out if still alive.""" 96 97 # no-op if our underlying widget is dead or on its way out. 98 if not self._root_widget or self._root_widget.transitioning_out: 99 return 100 101 # Transition ourself out. 102 try: 103 self.on_main_window_close() 104 except Exception: 105 logging.exception('Error in on_main_window_close() for %s.', self) 106 107 # Note: normally transition of None means instant, but we use 108 # that to mean 'do the default' so we support a special 109 # 'instant' string.. 110 if transition == 'instant': 111 self._root_widget.delete() 112 else: 113 _bauiv1.containerwidget( 114 edit=self._root_widget, 115 transition=( 116 self._main_window_transition_out 117 if transition is None 118 else transition 119 ), 120 ) 121 122 def main_window_has_control(self) -> bool: 123 """Is this MainWindow allowed to change the global main window? 124 125 It is a good idea to make sure this is True before calling 126 main_window_replace(). This prevents fluke UI breakage such as 127 multiple simultaneous events causing a MainWindow to spawn 128 multiple replacements for itself. 129 """ 130 # We are allowed to change main windows if we are the current one 131 # AND our underlying widget is still alive and not transitioning out. 132 return ( 133 babase.app.ui_v1.get_main_window() is self 134 and bool(self._root_widget) 135 and not self._root_widget.transitioning_out 136 ) 137 138 def main_window_back(self) -> None: 139 """Move back in the main window stack. 140 141 Is a no-op if the main window does not have control; 142 no need to check main_window_has_control() first. 143 """ 144 145 # Users should always check main_window_has_control() before 146 # calling us. Error if it seems they did not. 147 if not self.main_window_has_control(): 148 return 149 150 if not self.main_window_is_top_level: 151 152 # Get the 'back' window coming in. 153 babase.app.ui_v1.auto_set_back_window(self) 154 155 self.main_window_close() 156 157 def main_window_replace( 158 self, 159 new_window: MainWindow, 160 back_state: MainWindowState | None = None, 161 is_auxiliary: bool = False, 162 ) -> None: 163 """Replace ourself with a new MainWindow.""" 164 165 # Users should always check main_window_has_control() *before* 166 # creating new MainWindows and passing them in here. Kill the 167 # passed window and Error if it seems they did not. 168 if not self.main_window_has_control(): 169 new_window.get_root_widget().delete() 170 raise RuntimeError( 171 f'main_window_replace() called on a not-in-control window' 172 f' ({self}); always check main_window_has_control() before' 173 f' calling main_window_replace().' 174 ) 175 176 # Just shove the old out the left to give the feel that we're 177 # adding to the nav stack. 178 transition = 'out_left' 179 180 # Transition ourself out. 181 try: 182 self.on_main_window_close() 183 except Exception: 184 logging.exception('Error in on_main_window_close() for %s.', self) 185 186 _bauiv1.containerwidget(edit=self._root_widget, transition=transition) 187 babase.app.ui_v1.set_main_window( 188 new_window, 189 from_window=self, 190 back_state=back_state, 191 is_auxiliary=is_auxiliary, 192 suppress_warning=True, 193 ) 194 195 def on_main_window_close(self) -> None: 196 """Called before transitioning out a main window. 197 198 A good opportunity to save window state/etc. 199 """ 200 201 def get_main_window_state(self) -> MainWindowState: 202 """Return a WindowState to recreate this window, if supported.""" 203 raise NotImplementedError()
A special window that can be used as a main window.
52 def __init__( 53 self, 54 root_widget: bauiv1.Widget, 55 transition: str | None, 56 origin_widget: bauiv1.Widget | None, 57 cleanupcheck: bool = True, 58 ): 59 """Create a MainWindow given a root widget and transition info. 60 61 Automatically handles in and out transitions on the provided widget, 62 so there is no need to set transitions when creating it. 63 """ 64 # A back-state supplied by the ui system. 65 self.main_window_back_state: MainWindowState | None = None 66 67 self.main_window_is_top_level: bool = False 68 69 # Windows can be flagged as auxiliary when not related to the 70 # main UI task at hand. UI code may choose to handle auxiliary 71 # windows in special ways, such as by implicitly replacing 72 # existing auxiliary windows with new ones instead of keeping 73 # old ones as back targets. 74 self.main_window_is_auxiliary: bool = False 75 76 self._main_window_transition = transition 77 self._main_window_origin_widget = origin_widget 78 super().__init__(root_widget, cleanupcheck) 79 80 scale_origin: tuple[float, float] | None 81 if origin_widget is not None: 82 self._main_window_transition_out = 'out_scale' 83 scale_origin = origin_widget.get_screen_space_center() 84 transition = 'in_scale' 85 else: 86 self._main_window_transition_out = 'out_right' 87 scale_origin = None 88 _bauiv1.containerwidget( 89 edit=root_widget, 90 transition=transition, 91 scale_origin_stack_offset=scale_origin, 92 )
Create a MainWindow given a root widget and transition info.
Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.
94 def main_window_close(self, transition: str | None = None) -> None: 95 """Get window transitioning out if still alive.""" 96 97 # no-op if our underlying widget is dead or on its way out. 98 if not self._root_widget or self._root_widget.transitioning_out: 99 return 100 101 # Transition ourself out. 102 try: 103 self.on_main_window_close() 104 except Exception: 105 logging.exception('Error in on_main_window_close() for %s.', self) 106 107 # Note: normally transition of None means instant, but we use 108 # that to mean 'do the default' so we support a special 109 # 'instant' string.. 110 if transition == 'instant': 111 self._root_widget.delete() 112 else: 113 _bauiv1.containerwidget( 114 edit=self._root_widget, 115 transition=( 116 self._main_window_transition_out 117 if transition is None 118 else transition 119 ), 120 )
Get window transitioning out if still alive.
122 def main_window_has_control(self) -> bool: 123 """Is this MainWindow allowed to change the global main window? 124 125 It is a good idea to make sure this is True before calling 126 main_window_replace(). This prevents fluke UI breakage such as 127 multiple simultaneous events causing a MainWindow to spawn 128 multiple replacements for itself. 129 """ 130 # We are allowed to change main windows if we are the current one 131 # AND our underlying widget is still alive and not transitioning out. 132 return ( 133 babase.app.ui_v1.get_main_window() is self 134 and bool(self._root_widget) 135 and not self._root_widget.transitioning_out 136 )
Is this MainWindow allowed to change the global main window?
It is a good idea to make sure this is True before calling main_window_replace(). This prevents fluke UI breakage such as multiple simultaneous events causing a MainWindow to spawn multiple replacements for itself.
138 def main_window_back(self) -> None: 139 """Move back in the main window stack. 140 141 Is a no-op if the main window does not have control; 142 no need to check main_window_has_control() first. 143 """ 144 145 # Users should always check main_window_has_control() before 146 # calling us. Error if it seems they did not. 147 if not self.main_window_has_control(): 148 return 149 150 if not self.main_window_is_top_level: 151 152 # Get the 'back' window coming in. 153 babase.app.ui_v1.auto_set_back_window(self) 154 155 self.main_window_close()
Move back in the main window stack.
Is a no-op if the main window does not have control; no need to check main_window_has_control() first.
157 def main_window_replace( 158 self, 159 new_window: MainWindow, 160 back_state: MainWindowState | None = None, 161 is_auxiliary: bool = False, 162 ) -> None: 163 """Replace ourself with a new MainWindow.""" 164 165 # Users should always check main_window_has_control() *before* 166 # creating new MainWindows and passing them in here. Kill the 167 # passed window and Error if it seems they did not. 168 if not self.main_window_has_control(): 169 new_window.get_root_widget().delete() 170 raise RuntimeError( 171 f'main_window_replace() called on a not-in-control window' 172 f' ({self}); always check main_window_has_control() before' 173 f' calling main_window_replace().' 174 ) 175 176 # Just shove the old out the left to give the feel that we're 177 # adding to the nav stack. 178 transition = 'out_left' 179 180 # Transition ourself out. 181 try: 182 self.on_main_window_close() 183 except Exception: 184 logging.exception('Error in on_main_window_close() for %s.', self) 185 186 _bauiv1.containerwidget(edit=self._root_widget, transition=transition) 187 babase.app.ui_v1.set_main_window( 188 new_window, 189 from_window=self, 190 back_state=back_state, 191 is_auxiliary=is_auxiliary, 192 suppress_warning=True, 193 )
Replace ourself with a new MainWindow.
195 def on_main_window_close(self) -> None: 196 """Called before transitioning out a main window. 197 198 A good opportunity to save window state/etc. 199 """
Called before transitioning out a main window.
A good opportunity to save window state/etc.
201 def get_main_window_state(self) -> MainWindowState: 202 """Return a WindowState to recreate this window, if supported.""" 203 raise NotImplementedError()
Return a WindowState to recreate this window, if supported.
Inherited Members
206class MainWindowState: 207 """Persistent state for a specific main-window and its ancestors. 208 209 This allows MainWindows to be automatically recreated for back-button 210 purposes, when switching app-modes, etc. 211 """ 212 213 def __init__(self) -> None: 214 # The window that back/cancel navigation should take us to. 215 self.parent: MainWindowState | None = None 216 self.is_top_level: bool | None = None 217 self.is_auxiliary: bool | None = None 218 self.window_type: type[MainWindow] | None = None 219 220 def create_window( 221 self, 222 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 223 origin_widget: bauiv1.Widget | None = None, 224 ) -> MainWindow: 225 """Create a window based on this state. 226 227 WindowState child classes should override this to recreate their 228 particular type of window. 229 """ 230 raise NotImplementedError()
Persistent state for a specific main-window and its ancestors.
This allows MainWindows to be automatically recreated for back-button purposes, when switching app-modes, etc.
220 def create_window( 221 self, 222 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 223 origin_widget: bauiv1.Widget | None = None, 224 ) -> MainWindow: 225 """Create a window based on this state. 226 227 WindowState child classes should override this to recreate their 228 particular type of window. 229 """ 230 raise NotImplementedError()
Create a window based on this state.
WindowState child classes should override this to recreate their particular type of window.
Category: User Interface Classes
26class NotFoundError(Exception): 27 """Exception raised when a referenced object does not exist. 28 29 Category: **Exception Classes** 30 """
Exception raised when a referenced object does not exist.
Category: Exception Classes
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- add_note
- args
1323def open_url(address: str, force_fallback: bool = False) -> None: 1324 """Open the provided URL. 1325 1326 Category: **General Utility Functions** 1327 1328 Attempts to open the provided url in a web-browser. If that is not 1329 possible (or force_fallback is True), instead displays the url as 1330 a string and/or qrcode. 1331 """ 1332 return None
Open the provided URL.
Category: General Utility Functions
Attempts to open the provided url in a web-browser. If that is not possible (or force_fallback is True), instead displays the url as a string and/or qrcode.
1335def overlay_web_browser_close() -> bool: 1336 """Close any open overlay web browser. 1337 1338 Category: **General Utility Functions** 1339 """ 1340 return bool()
Close any open overlay web browser.
Category: General Utility Functions
1343def overlay_web_browser_is_open() -> bool: 1344 """Return whether an overlay web browser is open currently. 1345 1346 Category: **General Utility Functions** 1347 """ 1348 return bool()
Return whether an overlay web browser is open currently.
Category: General Utility Functions
1351def overlay_web_browser_is_supported() -> bool: 1352 """Return whether an overlay web browser is supported here. 1353 1354 Category: **General Utility Functions** 1355 1356 An overlay web browser is a small dialog that pops up over the top 1357 of the main engine window. It can be used for performing simple 1358 tasks such as sign-ins. 1359 """ 1360 return bool()
Return whether an overlay web browser is supported here.
Category: General Utility Functions
An overlay web browser is a small dialog that pops up over the top of the main engine window. It can be used for performing simple tasks such as sign-ins.
1363def overlay_web_browser_open_url(address: str) -> None: 1364 """Open the provided URL in an overlayw web browser. 1365 1366 Category: **General Utility Functions** 1367 1368 An overlay web browser is a small dialog that pops up over the top 1369 of the main engine window. It can be used for performing simple 1370 tasks such as sign-ins. 1371 """ 1372 return None
Open the provided URL in an overlayw web browser.
Category: General Utility Functions
An overlay web browser is a small dialog that pops up over the top of the main engine window. It can be used for performing simple tasks such as sign-ins.
89class Permission(Enum): 90 """Permissions that can be requested from the OS. 91 92 Category: Enums 93 """ 94 95 STORAGE = 0
Permissions that can be requested from the OS.
Category: Enums
Inherited Members
- enum.Enum
- name
- value
322class Plugin: 323 """A plugin to alter app behavior in some way. 324 325 Category: **App Classes** 326 327 Plugins are discoverable by the meta-tag system 328 and the user can select which ones they want to enable. 329 Enabled plugins are then called at specific times as the 330 app is running in order to modify its behavior in some way. 331 """ 332 333 def on_app_running(self) -> None: 334 """Called when the app reaches the running state.""" 335 336 def on_app_suspend(self) -> None: 337 """Called when the app enters the suspended state.""" 338 339 def on_app_unsuspend(self) -> None: 340 """Called when the app exits the suspended state.""" 341 342 def on_app_shutdown(self) -> None: 343 """Called when the app is beginning the shutdown process.""" 344 345 def on_app_shutdown_complete(self) -> None: 346 """Called when the app has completed the shutdown process.""" 347 348 def has_settings_ui(self) -> bool: 349 """Called to ask if we have settings UI we can show.""" 350 return False 351 352 def show_settings_ui(self, source_widget: Any | None) -> None: 353 """Called to show our settings UI."""
A plugin to alter app behavior in some way.
Category: App Classes
Plugins are discoverable by the meta-tag system and the user can select which ones they want to enable. Enabled plugins are then called at specific times as the app is running in order to modify its behavior in some way.
342 def on_app_shutdown(self) -> None: 343 """Called when the app is beginning the shutdown process."""
Called when the app is beginning the shutdown process.
345 def on_app_shutdown_complete(self) -> None: 346 """Called when the app has completed the shutdown process."""
Called when the app has completed the shutdown process.
227class PluginSpec: 228 """Represents a plugin the engine knows about. 229 230 Category: **App Classes** 231 232 The 'enabled' attr represents whether this plugin is set to load. 233 Getting or setting that attr affects the corresponding app-config 234 key. Remember to commit the app-config after making any changes. 235 236 The 'attempted_load' attr will be True if the engine has attempted 237 to load the plugin. If 'attempted_load' is True for a PluginSpec 238 but the 'plugin' attr is None, it means there was an error loading 239 the plugin. If a plugin's api-version does not match the running 240 app, if a new plugin is detected with auto-enable-plugins disabled, 241 or if the user has explicitly disabled a plugin, the engine will not 242 even attempt to load it. 243 """ 244 245 def __init__(self, class_path: str, loadable: bool): 246 self.class_path = class_path 247 self.loadable = loadable 248 self.attempted_load = False 249 self.plugin: Plugin | None = None 250 251 @property 252 def enabled(self) -> bool: 253 """Whether the user wants this plugin to load.""" 254 plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {}) 255 assert isinstance(plugstates, dict) 256 val = plugstates.get(self.class_path, {}).get('enabled', False) is True 257 return val 258 259 @enabled.setter 260 def enabled(self, val: bool) -> None: 261 plugstates: dict[str, dict] = _babase.app.config.setdefault( 262 'Plugins', {} 263 ) 264 assert isinstance(plugstates, dict) 265 plugstate = plugstates.setdefault(self.class_path, {}) 266 plugstate['enabled'] = val 267 268 def attempt_load_if_enabled(self) -> Plugin | None: 269 """Possibly load the plugin and log any errors.""" 270 from babase._general import getclass 271 from babase._language import Lstr 272 273 assert not self.attempted_load 274 assert self.plugin is None 275 276 if not self.enabled: 277 return None 278 self.attempted_load = True 279 if not self.loadable: 280 return None 281 try: 282 cls = getclass(self.class_path, Plugin, True) 283 except Exception as exc: 284 _babase.getsimplesound('error').play() 285 _babase.screenmessage( 286 Lstr( 287 resource='pluginClassLoadErrorText', 288 subs=[ 289 ('${PLUGIN}', self.class_path), 290 ('${ERROR}', str(exc)), 291 ], 292 ), 293 color=(1, 0, 0), 294 ) 295 logging.exception( 296 "Error loading plugin class '%s'.", self.class_path 297 ) 298 return None 299 try: 300 self.plugin = cls() 301 return self.plugin 302 except Exception as exc: 303 from babase import _error 304 305 _babase.getsimplesound('error').play() 306 _babase.screenmessage( 307 Lstr( 308 resource='pluginInitErrorText', 309 subs=[ 310 ('${PLUGIN}', self.class_path), 311 ('${ERROR}', str(exc)), 312 ], 313 ), 314 color=(1, 0, 0), 315 ) 316 logging.exception( 317 "Error initing plugin class: '%s'.", self.class_path 318 ) 319 return None
Represents a plugin the engine knows about.
Category: App Classes
The 'enabled' attr represents whether this plugin is set to load. Getting or setting that attr affects the corresponding app-config key. Remember to commit the app-config after making any changes.
The 'attempted_load' attr will be True if the engine has attempted to load the plugin. If 'attempted_load' is True for a PluginSpec but the 'plugin' attr is None, it means there was an error loading the plugin. If a plugin's api-version does not match the running app, if a new plugin is detected with auto-enable-plugins disabled, or if the user has explicitly disabled a plugin, the engine will not even attempt to load it.
251 @property 252 def enabled(self) -> bool: 253 """Whether the user wants this plugin to load.""" 254 plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {}) 255 assert isinstance(plugstates, dict) 256 val = plugstates.get(self.class_path, {}).get('enabled', False) is True 257 return val
Whether the user wants this plugin to load.
268 def attempt_load_if_enabled(self) -> Plugin | None: 269 """Possibly load the plugin and log any errors.""" 270 from babase._general import getclass 271 from babase._language import Lstr 272 273 assert not self.attempted_load 274 assert self.plugin is None 275 276 if not self.enabled: 277 return None 278 self.attempted_load = True 279 if not self.loadable: 280 return None 281 try: 282 cls = getclass(self.class_path, Plugin, True) 283 except Exception as exc: 284 _babase.getsimplesound('error').play() 285 _babase.screenmessage( 286 Lstr( 287 resource='pluginClassLoadErrorText', 288 subs=[ 289 ('${PLUGIN}', self.class_path), 290 ('${ERROR}', str(exc)), 291 ], 292 ), 293 color=(1, 0, 0), 294 ) 295 logging.exception( 296 "Error loading plugin class '%s'.", self.class_path 297 ) 298 return None 299 try: 300 self.plugin = cls() 301 return self.plugin 302 except Exception as exc: 303 from babase import _error 304 305 _babase.getsimplesound('error').play() 306 _babase.screenmessage( 307 Lstr( 308 resource='pluginInitErrorText', 309 subs=[ 310 ('${PLUGIN}', self.class_path), 311 ('${ERROR}', str(exc)), 312 ], 313 ), 314 color=(1, 0, 0), 315 ) 316 logging.exception( 317 "Error initing plugin class: '%s'.", self.class_path 318 ) 319 return None
Possibly load the plugin and log any errors.
1407def pushcall( 1408 call: Callable, 1409 from_other_thread: bool = False, 1410 suppress_other_thread_warning: bool = False, 1411 other_thread_use_fg_context: bool = False, 1412 raw: bool = False, 1413) -> None: 1414 """Push a call to the logic event-loop. 1415 Category: **General Utility Functions** 1416 1417 This call expects to be used in the logic thread, and will automatically 1418 save and restore the babase.Context to behave seamlessly. 1419 1420 If you want to push a call from outside of the logic thread, 1421 however, you can pass 'from_other_thread' as True. In this case 1422 the call will always run in the UI context_ref on the logic thread 1423 or whichever context_ref is in the foreground if 1424 other_thread_use_fg_context is True. 1425 Passing raw=True will disable thread checks and context_ref sets/restores. 1426 """ 1427 return None
Push a call to the logic event-loop. Category: General Utility Functions
This call expects to be used in the logic thread, and will automatically save and restore the babase.Context to behave seamlessly.
If you want to push a call from outside of the logic thread, however, you can pass 'from_other_thread' as True. In this case the call will always run in the UI context_ref on the logic thread or whichever context_ref is in the foreground if other_thread_use_fg_context is True. Passing raw=True will disable thread checks and context_ref sets/restores.
1431def quit( 1432 confirm: bool = False, quit_type: babase.QuitType | None = None 1433) -> None: 1434 """Quit the app. 1435 1436 Category: **General Utility Functions** 1437 1438 If 'confirm' is True, a confirm dialog will be presented if conditions 1439 allow; otherwise the quit will still be immediate. 1440 See docs for babase.QuitType for explanations of the optional 1441 'quit_type' arg. 1442 """ 1443 return None
Quit the app.
Category: General Utility Functions
If 'confirm' is True, a confirm dialog will be presented if conditions allow; otherwise the quit will still be immediate. See docs for babase.QuitType for explanations of the optional 'quit_type' arg.
42class QuitType(Enum): 43 """Types of input a controller can send to the game. 44 45 Category: Enums 46 47 'soft' may hide/reset the app but keep the process running, depending 48 on the platform. 49 50 'back' is a variant of 'soft' which may give 'back-button-pressed' 51 behavior depending on the platform. (returning to some previous 52 activity instead of dumping to the home screen, etc.) 53 54 'hard' leads to the process exiting. This generally should be avoided 55 on platforms such as mobile. 56 """ 57 58 SOFT = 0 59 BACK = 1 60 HARD = 2
Types of input a controller can send to the game.
Category: Enums
'soft' may hide/reset the app but keep the process running, depending on the platform.
'back' is a variant of 'soft' which may give 'back-button-pressed' behavior depending on the platform. (returning to some previous activity instead of dumping to the home screen, etc.)
'hard' leads to the process exiting. This generally should be avoided on platforms such as mobile.
Inherited Members
- enum.Enum
- name
- value
462def rowwidget( 463 edit: bauiv1.Widget | None = None, 464 parent: bauiv1.Widget | None = None, 465 size: Sequence[float] | None = None, 466 position: Sequence[float] | None = None, 467 background: bool | None = None, 468 selected_child: bauiv1.Widget | None = None, 469 visible_child: bauiv1.Widget | None = None, 470 claims_left_right: bool | None = None, 471 claims_tab: bool | None = None, 472 selection_loops_to_parent: bool | None = None, 473) -> bauiv1.Widget: 474 """Create or edit a row widget. 475 476 Category: **User Interface Functions** 477 478 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 479 a new one is created and returned. Arguments that are not set to None 480 are applied to the Widget. 481 """ 482 import bauiv1 # pylint: disable=cyclic-import 483 484 return bauiv1.Widget()
Create or edit a row widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
1481def safecolor( 1482 color: Sequence[float], target_intensity: float = 0.6 1483) -> tuple[float, ...]: 1484 """Given a color tuple, return a color safe to display as text. 1485 1486 Category: **General Utility Functions** 1487 1488 Accepts tuples of length 3 or 4. This will slightly brighten very 1489 dark colors, etc. 1490 """ 1491 return (0.0, 0.0, 0.0)
Given a color tuple, return a color safe to display as text.
Category: General Utility Functions
Accepts tuples of length 3 or 4. This will slightly brighten very dark colors, etc.
1494def screenmessage( 1495 message: str | babase.Lstr, 1496 color: Sequence[float] | None = None, 1497 log: bool = False, 1498) -> None: 1499 """Print a message to the local client's screen, in a given color. 1500 1501 Category: **General Utility Functions** 1502 1503 Note that this version of the function is purely for local display. 1504 To broadcast screen messages in network play, look for methods such as 1505 broadcastmessage() provided by the scene-version packages. 1506 """ 1507 return None
Print a message to the local client's screen, in a given color.
Category: General Utility Functions
Note that this version of the function is purely for local display. To broadcast screen messages in network play, look for methods such as broadcastmessage() provided by the scene-version packages.
487def scrollwidget( 488 *, 489 edit: bauiv1.Widget | None = None, 490 parent: bauiv1.Widget | None = None, 491 size: Sequence[float] | None = None, 492 position: Sequence[float] | None = None, 493 background: bool | None = None, 494 selected_child: bauiv1.Widget | None = None, 495 capture_arrows: bool = False, 496 on_select_call: Callable | None = None, 497 center_small_content: bool | None = None, 498 color: Sequence[float] | None = None, 499 highlight: bool | None = None, 500 border_opacity: float | None = None, 501 simple_culling_v: float | None = None, 502 selection_loops_to_parent: bool | None = None, 503 claims_left_right: bool | None = None, 504 claims_up_down: bool | None = None, 505 claims_tab: bool | None = None, 506 autoselect: bool | None = None, 507) -> bauiv1.Widget: 508 """Create or edit a scroll widget. 509 510 Category: **User Interface Functions** 511 512 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 513 a new one is created and returned. Arguments that are not set to None 514 are applied to the Widget. 515 """ 516 import bauiv1 # pylint: disable=cyclic-import 517 518 return bauiv1.Widget()
Create or edit a scroll widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
1510def set_analytics_screen(screen: str) -> None: 1511 """Used for analytics to see where in the app players spend their time. 1512 1513 Category: **General Utility Functions** 1514 1515 Generally called when opening a new window or entering some UI. 1516 'screen' should be a string description of an app location 1517 ('Main Menu', etc.) 1518 """ 1519 return None
Used for analytics to see where in the app players spend their time.
Category: General Utility Functions
Generally called when opening a new window or entering some UI. 'screen' should be a string description of an app location ('Main Menu', etc.)
57class Sound: 58 """Category: **User Interface Classes**""" 59 60 def play(self) -> None: 61 """Play the sound locally.""" 62 return None 63 64 def stop(self) -> None: 65 """Stop the sound if it is playing.""" 66 return None
Category: User Interface Classes
98class SpecialChar(Enum): 99 """Special characters the game can print. 100 101 Category: Enums 102 """ 103 104 DOWN_ARROW = 0 105 UP_ARROW = 1 106 LEFT_ARROW = 2 107 RIGHT_ARROW = 3 108 TOP_BUTTON = 4 109 LEFT_BUTTON = 5 110 RIGHT_BUTTON = 6 111 BOTTOM_BUTTON = 7 112 DELETE = 8 113 SHIFT = 9 114 BACK = 10 115 LOGO_FLAT = 11 116 REWIND_BUTTON = 12 117 PLAY_PAUSE_BUTTON = 13 118 FAST_FORWARD_BUTTON = 14 119 DPAD_CENTER_BUTTON = 15 120 PLAY_STATION_CROSS_BUTTON = 16 121 PLAY_STATION_CIRCLE_BUTTON = 17 122 PLAY_STATION_TRIANGLE_BUTTON = 18 123 PLAY_STATION_SQUARE_BUTTON = 19 124 PLAY_BUTTON = 20 125 PAUSE_BUTTON = 21 126 OUYA_BUTTON_O = 22 127 OUYA_BUTTON_U = 23 128 OUYA_BUTTON_Y = 24 129 OUYA_BUTTON_A = 25 130 TOKEN = 26 131 LOGO = 27 132 TICKET = 28 133 GOOGLE_PLAY_GAMES_LOGO = 29 134 GAME_CENTER_LOGO = 30 135 DICE_BUTTON1 = 31 136 DICE_BUTTON2 = 32 137 DICE_BUTTON3 = 33 138 DICE_BUTTON4 = 34 139 GAME_CIRCLE_LOGO = 35 140 PARTY_ICON = 36 141 TEST_ACCOUNT = 37 142 TICKET_BACKING = 38 143 TROPHY1 = 39 144 TROPHY2 = 40 145 TROPHY3 = 41 146 TROPHY0A = 42 147 TROPHY0B = 43 148 TROPHY4 = 44 149 LOCAL_ACCOUNT = 45 150 EXPLODINARY_LOGO = 46 151 FLAG_UNITED_STATES = 47 152 FLAG_MEXICO = 48 153 FLAG_GERMANY = 49 154 FLAG_BRAZIL = 50 155 FLAG_RUSSIA = 51 156 FLAG_CHINA = 52 157 FLAG_UNITED_KINGDOM = 53 158 FLAG_CANADA = 54 159 FLAG_INDIA = 55 160 FLAG_JAPAN = 56 161 FLAG_FRANCE = 57 162 FLAG_INDONESIA = 58 163 FLAG_ITALY = 59 164 FLAG_SOUTH_KOREA = 60 165 FLAG_NETHERLANDS = 61 166 FEDORA = 62 167 HAL = 63 168 CROWN = 64 169 YIN_YANG = 65 170 EYE_BALL = 66 171 SKULL = 67 172 HEART = 68 173 DRAGON = 69 174 HELMET = 70 175 MUSHROOM = 71 176 NINJA_STAR = 72 177 VIKING_HELMET = 73 178 MOON = 74 179 SPIDER = 75 180 FIREBALL = 76 181 FLAG_UNITED_ARAB_EMIRATES = 77 182 FLAG_QATAR = 78 183 FLAG_EGYPT = 79 184 FLAG_KUWAIT = 80 185 FLAG_ALGERIA = 81 186 FLAG_SAUDI_ARABIA = 82 187 FLAG_MALAYSIA = 83 188 FLAG_CZECH_REPUBLIC = 84 189 FLAG_AUSTRALIA = 85 190 FLAG_SINGAPORE = 86 191 OCULUS_LOGO = 87 192 STEAM_LOGO = 88 193 NVIDIA_LOGO = 89 194 FLAG_IRAN = 90 195 FLAG_POLAND = 91 196 FLAG_ARGENTINA = 92 197 FLAG_PHILIPPINES = 93 198 FLAG_CHILE = 94 199 MIKIROG = 95 200 V2_LOGO = 96
Special characters the game can print.
Category: Enums
Inherited Members
- enum.Enum
- name
- value
Category: User Interface Classes
526def textwidget( 527 *, 528 edit: bauiv1.Widget | None = None, 529 parent: bauiv1.Widget | None = None, 530 size: Sequence[float] | None = None, 531 position: Sequence[float] | None = None, 532 text: str | bauiv1.Lstr | None = None, 533 v_align: str | None = None, 534 h_align: str | None = None, 535 editable: bool | None = None, 536 padding: float | None = None, 537 on_return_press_call: Callable[[], None] | None = None, 538 on_activate_call: Callable[[], None] | None = None, 539 selectable: bool | None = None, 540 query: bauiv1.Widget | None = None, 541 max_chars: int | None = None, 542 color: Sequence[float] | None = None, 543 click_activate: bool | None = None, 544 on_select_call: Callable[[], None] | None = None, 545 always_highlight: bool | None = None, 546 draw_controller: bauiv1.Widget | None = None, 547 scale: float | None = None, 548 corner_scale: float | None = None, 549 description: str | bauiv1.Lstr | None = None, 550 transition_delay: float | None = None, 551 maxwidth: float | None = None, 552 max_height: float | None = None, 553 flatness: float | None = None, 554 shadow: float | None = None, 555 autoselect: bool | None = None, 556 rotate: float | None = None, 557 enabled: bool | None = None, 558 force_internal_editing: bool | None = None, 559 always_show_carat: bool | None = None, 560 big: bool | None = None, 561 extra_touch_border_scale: float | None = None, 562 res_scale: float | None = None, 563 query_max_chars: bauiv1.Widget | None = None, 564 query_description: bauiv1.Widget | None = None, 565 adapter_finished: bool | None = None, 566 glow_type: str | None = None, 567 allow_clear_button: bool | None = None, 568) -> bauiv1.Widget: 569 """Create or edit a text widget. 570 571 Category: **User Interface Functions** 572 573 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 574 a new one is created and returned. Arguments that are not set to None 575 are applied to the Widget. 576 """ 577 import bauiv1 # pylint: disable=cyclic-import 578 579 return bauiv1.Widget()
Create or edit a text widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
15def timestring( 16 timeval: float | int, 17 centi: bool = True, 18) -> babase.Lstr: 19 """Generate a babase.Lstr for displaying a time value. 20 21 Category: **General Utility Functions** 22 23 Given a time value, returns a babase.Lstr with: 24 (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True). 25 26 WARNING: the underlying Lstr value is somewhat large so don't use this 27 to rapidly update Node text values for an onscreen timer or you may 28 consume significant network bandwidth. For that purpose you should 29 use a 'timedisplay' Node and attribute connections. 30 31 """ 32 from babase._language import Lstr 33 34 # We take float seconds but operate on int milliseconds internally. 35 timeval = int(1000 * timeval) 36 bits = [] 37 subs = [] 38 hval = (timeval // 1000) // (60 * 60) 39 if hval != 0: 40 bits.append('${H}') 41 subs.append( 42 ( 43 '${H}', 44 Lstr( 45 resource='timeSuffixHoursText', 46 subs=[('${COUNT}', str(hval))], 47 ), 48 ) 49 ) 50 mval = ((timeval // 1000) // 60) % 60 51 if mval != 0: 52 bits.append('${M}') 53 subs.append( 54 ( 55 '${M}', 56 Lstr( 57 resource='timeSuffixMinutesText', 58 subs=[('${COUNT}', str(mval))], 59 ), 60 ) 61 ) 62 63 # We add seconds if its non-zero *or* we haven't added anything else. 64 if centi: 65 # pylint: disable=consider-using-f-string 66 sval = timeval / 1000.0 % 60.0 67 if sval >= 0.005 or not bits: 68 bits.append('${S}') 69 subs.append( 70 ( 71 '${S}', 72 Lstr( 73 resource='timeSuffixSecondsText', 74 subs=[('${COUNT}', ('%.2f' % sval))], 75 ), 76 ) 77 ) 78 else: 79 sval = timeval // 1000 % 60 80 if sval != 0 or not bits: 81 bits.append('${S}') 82 subs.append( 83 ( 84 '${S}', 85 Lstr( 86 resource='timeSuffixSecondsText', 87 subs=[('${COUNT}', str(sval))], 88 ), 89 ) 90 ) 91 return Lstr(value=' '.join(bits), subs=subs)
Generate a babase.Lstr for displaying a time value.
Category: General Utility Functions
Given a time value, returns a babase.Lstr with: (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).
WARNING: the underlying Lstr value is somewhat large so don't use this to rapidly update Node text values for an onscreen timer or you may consume significant network bandwidth. For that purpose you should use a 'timedisplay' Node and attribute connections.
267def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None: 268 """Checks to ensure a widget-owning object gets cleaned up properly. 269 270 Category: User Interface Functions 271 272 This adds a check which will print an error message if the provided 273 object still exists ~5 seconds after the provided bauiv1.Widget dies. 274 275 This is a good sanity check for any sort of object that wraps or 276 controls a bauiv1.Widget. For instance, a 'Window' class instance has 277 no reason to still exist once its root container bauiv1.Widget has fully 278 transitioned out and been destroyed. Circular references or careless 279 strong referencing can lead to such objects never getting destroyed, 280 however, and this helps detect such cases to avoid memory leaks. 281 """ 282 if DEBUG_UI_CLEANUP_CHECKS: 283 print(f'adding uicleanup to {obj}') 284 if not isinstance(widget, _bauiv1.Widget): 285 raise TypeError('widget arg is not a bauiv1.Widget') 286 287 if bool(False): 288 289 def foobar() -> None: 290 """Just testing.""" 291 if DEBUG_UI_CLEANUP_CHECKS: 292 print('uicleanupcheck widget dying...') 293 294 widget.add_delete_callback(foobar) 295 296 assert babase.app.classic is not None 297 babase.app.ui_v1.cleanupchecks.append( 298 UICleanupCheck( 299 obj=weakref.ref(obj), widget=widget, widget_death_time=None 300 ) 301 )
Checks to ensure a widget-owning object gets cleaned up properly.
Category: User Interface Functions
This adds a check which will print an error message if the provided object still exists ~5 seconds after the provided bauiv1.Widget dies.
This is a good sanity check for any sort of object that wraps or controls a bauiv1.Widget. For instance, a 'Window' class instance has no reason to still exist once its root container bauiv1.Widget has fully transitioned out and been destroyed. Circular references or careless strong referencing can lead to such objects never getting destroyed, however, and this helps detect such cases to avoid memory leaks.
63class UIScale(Enum): 64 """The overall scale the UI is being rendered for. Note that this is 65 independent of pixel resolution. For example, a phone and a desktop PC 66 might render the game at similar pixel resolutions but the size they 67 display content at will vary significantly. 68 69 Category: Enums 70 71 'large' is used for devices such as desktop PCs where fine details can 72 be clearly seen. UI elements are generally smaller on the screen 73 and more content can be seen at once. 74 75 'medium' is used for devices such as tablets, TVs, or VR headsets. 76 This mode strikes a balance between clean readability and amount of 77 content visible. 78 79 'small' is used primarily for phones or other small devices where 80 content needs to be presented as large and clear in order to remain 81 readable from an average distance. 82 """ 83 84 SMALL = 0 85 MEDIUM = 1 86 LARGE = 2
The overall scale the UI is being rendered for. Note that this is independent of pixel resolution. For example, a phone and a desktop PC might render the game at similar pixel resolutions but the size they display content at will vary significantly.
Category: Enums
'large' is used for devices such as desktop PCs where fine details can be clearly seen. UI elements are generally smaller on the screen and more content can be seen at once.
'medium' is used for devices such as tablets, TVs, or VR headsets. This mode strikes a balance between clean readability and amount of content visible.
'small' is used primarily for phones or other small devices where content needs to be presented as large and clear in order to remain readable from an average distance.
Inherited Members
- enum.Enum
- name
- value
32class UIV1AppSubsystem(babase.AppSubsystem): 33 """Consolidated UI functionality for the app. 34 35 Category: **App Classes** 36 37 To use this class, access the single instance of it at 'ba.app.ui'. 38 """ 39 40 class RootUIElement(Enum): 41 """Stuff provided by the root ui.""" 42 43 MENU_BUTTON = 'menu_button' 44 SQUAD_BUTTON = 'squad_button' 45 ACCOUNT_BUTTON = 'account_button' 46 SETTINGS_BUTTON = 'settings_button' 47 INBOX_BUTTON = 'inbox_button' 48 STORE_BUTTON = 'store_button' 49 INVENTORY_BUTTON = 'inventory_button' 50 ACHIEVEMENTS_BUTTON = 'achievements_button' 51 GET_TOKENS_BUTTON = 'get_tokens_button' 52 TICKETS_METER = 'tickets_meter' 53 TOKENS_METER = 'tokens_meter' 54 TROPHY_METER = 'trophy_meter' 55 LEVEL_METER = 'level_meter' 56 CHEST_SLOT_1 = 'chest_slot_1' 57 CHEST_SLOT_2 = 'chest_slot_2' 58 CHEST_SLOT_3 = 'chest_slot_3' 59 CHEST_SLOT_4 = 'chest_slot_4' 60 61 def __init__(self) -> None: 62 from bauiv1._uitypes import MainWindow 63 64 super().__init__() 65 66 # We hold only a weak ref to the current main Window; we want it 67 # to be able to disappear on its own. That being said, we do 68 # expect MainWindows to keep themselves alive until replaced by 69 # another MainWindow and we complain if they don't. 70 self._main_window = empty_weakref(MainWindow) 71 self._main_window_widget: bauiv1.Widget | None = None 72 73 self.quit_window: bauiv1.Widget | None = None 74 75 # For storing arbitrary class-level state data for Windows or 76 # other UI related classes. 77 self.window_states: dict[type, Any] = {} 78 79 self._uiscale: babase.UIScale 80 self._update_ui_scale() 81 82 self.cleanupchecks: list[UICleanupCheck] = [] 83 self.upkeeptimer: babase.AppTimer | None = None 84 85 self.title_color = (0.72, 0.7, 0.75) 86 self.heading_color = (0.72, 0.7, 0.75) 87 self.infotextcolor = (0.7, 0.9, 0.7) 88 89 # Elements in our root UI will call anything here when 90 # activated. 91 self.root_ui_calls: dict[ 92 UIV1AppSubsystem.RootUIElement, Callable[[], None] 93 ] = {} 94 95 def _update_ui_scale(self) -> None: 96 uiscalestr = babase.get_ui_scale() 97 if uiscalestr == 'large': 98 self._uiscale = babase.UIScale.LARGE 99 elif uiscalestr == 'medium': 100 self._uiscale = babase.UIScale.MEDIUM 101 elif uiscalestr == 'small': 102 self._uiscale = babase.UIScale.SMALL 103 else: 104 logging.error("Invalid UIScale '%s'.", uiscalestr) 105 self._uiscale = babase.UIScale.MEDIUM 106 107 @property 108 def available(self) -> bool: 109 """Can uiv1 currently be used? 110 111 Code that may run in headless mode, before the UI has been spun up, 112 while other ui systems are active, etc. can check this to avoid 113 likely erroring. 114 """ 115 return _bauiv1.is_available() 116 117 @override 118 def reset(self) -> None: 119 from bauiv1._uitypes import MainWindow 120 121 self.root_ui_calls.clear() 122 self._main_window = empty_weakref(MainWindow) 123 self._main_window_widget = None 124 125 @property 126 def uiscale(self) -> babase.UIScale: 127 """Current ui scale for the app.""" 128 return self._uiscale 129 130 @override 131 def on_app_loading(self) -> None: 132 from bauiv1._uitypes import ui_upkeep 133 134 # IMPORTANT: If tweaking UI stuff, make sure it behaves for 135 # small, medium, and large UI modes. (doesn't run off screen, 136 # etc). The overrides below can be used to test with different 137 # sizes. Generally small is used on phones, medium is used on 138 # tablets/tvs, and large is on desktop computers or perhaps 139 # large tablets. When possible, run in windowed mode and resize 140 # the window to assure this holds true at all aspect ratios. 141 142 # UPDATE: A better way to test this is now by setting the 143 # environment variable BA_UI_SCALE to "small", "medium", or 144 # "large". This will affect system UIs not covered by the values 145 # below such as screen-messages. The below values remain 146 # functional, however, for cases such as Android where 147 # environment variables can't be set easily. 148 149 if bool(False): # force-test ui scale 150 self._uiscale = babase.UIScale.SMALL 151 with babase.ContextRef.empty(): 152 babase.pushcall( 153 lambda: babase.screenmessage( 154 f'FORCING UISCALE {self._uiscale.name} FOR TESTING', 155 color=(1, 0, 1), 156 log=True, 157 ) 158 ) 159 160 # Kick off our periodic UI upkeep. 161 162 # FIXME: Can probably kill this if we do immediate UI death 163 # checks. 164 self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True) 165 166 def auto_set_back_window(self, from_window: MainWindow) -> None: 167 """Sets the main menu window automatically from a parent WindowState.""" 168 169 main_window = self._main_window() 170 171 # This should never get called for top-level main-windows. 172 assert ( 173 main_window is None or main_window.main_window_is_top_level is False 174 ) 175 176 back_state = ( 177 None if main_window is None else main_window.main_window_back_state 178 ) 179 if back_state is None: 180 raise RuntimeError( 181 f'Main window {main_window} provides no back-state;' 182 f' cannot use auto-back.' 183 ) 184 185 # Valid states should have values here. 186 assert back_state.is_top_level is not None 187 assert back_state.is_auxiliary is not None 188 assert back_state.window_type is not None 189 190 backwin = back_state.create_window(transition='in_left') 191 192 self.set_main_window( 193 backwin, 194 from_window=from_window, 195 is_back=True, 196 back_state=back_state, 197 suppress_warning=True, 198 ) 199 200 def get_main_window(self) -> bauiv1.MainWindow | None: 201 """Return main window, if any.""" 202 return self._main_window() 203 204 def set_main_window( 205 self, 206 window: bauiv1.MainWindow, 207 *, 208 from_window: bauiv1.MainWindow | None | bool = True, 209 is_back: bool = False, 210 is_top_level: bool = False, 211 is_auxiliary: bool = False, 212 back_state: MainWindowState | None = None, 213 suppress_warning: bool = False, 214 ) -> None: 215 """Set the current 'main' window, replacing any existing. 216 217 Generally this should not be called directly; The high level 218 MainWindow methods main_window_replace() and main_window_back() 219 should be used when possible for navigation. 220 """ 221 # pylint: disable=too-many-locals 222 # pylint: disable=too-many-branches 223 # pylint: disable=too-many-statements 224 from bauiv1._uitypes import MainWindow 225 226 # Encourage migration to the new higher level nav calls. 227 if not suppress_warning: 228 warnings.warn( 229 'set_main_window() should usually not be called directly;' 230 ' use the main_window_replace() or main_window_back()' 231 ' methods on MainWindow objects for navigation instead.' 232 ' If you truly need to use set_main_window(),' 233 ' pass suppress_warning=True to silence this warning.', 234 DeprecationWarning, 235 stacklevel=2, 236 ) 237 238 # We used to accept Widgets but now want MainWindows. 239 if not isinstance(window, MainWindow): 240 raise RuntimeError( 241 f'set_main_window() now takes a MainWindow as its "window" arg.' 242 f' You passed a {type(window)}.', 243 ) 244 window_weakref = weakref.ref(window) 245 window_widget = window.get_root_widget() 246 247 if isinstance(from_window, MainWindow): 248 # from_window_widget = from_window.get_root_widget() 249 pass 250 else: 251 if from_window is not None and not isinstance(from_window, bool): 252 raise RuntimeError( 253 f'set_main_window() now takes a MainWindow or bool or None' 254 f'as its "from_window" arg.' 255 f' You passed a {type(from_window)}.', 256 ) 257 258 existing = self._main_window() 259 260 # If they passed a back-state, make sure it is fully filled out. 261 if back_state is not None: 262 if ( 263 back_state.is_top_level is None 264 or back_state.is_auxiliary is None 265 or back_state.window_type is None 266 ): 267 raise RuntimeError( 268 'Provided back_state is incomplete.' 269 ' Make sure to only pass fully-filled-out MainWindowStates.' 270 ) 271 # If a top-level main-window is being set, complain if there already 272 # is a main-window. 273 if is_top_level: 274 if existing: 275 logging.warning( 276 'set_main_window() called with top-level window %s' 277 ' but found existing main-window %s.', 278 window, 279 existing, 280 ) 281 else: 282 # In other cases, sanity-check that the window ordering this 283 # switch is the one we're switching away from. 284 try: 285 if isinstance(from_window, bool): 286 # For default val True we warn that the arg wasn't 287 # passed. False can be explicitly passed to disable 288 # this check. 289 if from_window is True: 290 caller_frame = inspect.stack()[1] 291 caller_filename = caller_frame.filename 292 caller_line_number = caller_frame.lineno 293 logging.warning( 294 'set_main_window() should be passed a' 295 " 'from_window' value to help ensure proper" 296 ' UI behavior (%s line %i).', 297 caller_filename, 298 caller_line_number, 299 ) 300 else: 301 # For everything else, warn if what they passed 302 # wasn't the previous main menu widget. 303 if from_window is not existing: 304 caller_frame = inspect.stack()[1] 305 caller_filename = caller_frame.filename 306 caller_line_number = caller_frame.lineno 307 logging.warning( 308 "set_main_window() was passed 'from_window' %s" 309 ' but existing main-menu-window is %s.' 310 ' (%s line %i).', 311 from_window, 312 existing, 313 caller_filename, 314 caller_line_number, 315 ) 316 except Exception: 317 # Prevent any bugs in these checks from causing problems. 318 logging.exception('Error checking from_window') 319 320 if is_back: 321 # These values should only be passed for forward navigation. 322 assert not is_top_level 323 assert not is_auxiliary 324 # Make sure back state is complete. 325 assert back_state is not None 326 assert back_state.is_top_level is not None 327 assert back_state.is_auxiliary is not None 328 assert back_state.window_type is type(window) 329 window.main_window_back_state = back_state.parent 330 window.main_window_is_top_level = back_state.is_top_level 331 window.main_window_is_auxiliary = back_state.is_auxiliary 332 else: 333 # Store if the window is top-level so we won't complain later if 334 # we go back from it and there's nowhere to go to. 335 window.main_window_is_top_level = is_top_level 336 337 window.main_window_is_auxiliary = is_auxiliary 338 339 # When navigating forward, generate a back-window-state from 340 # the outgoing window. 341 if is_top_level: 342 # Top level windows don't have or expect anywhere to 343 # go back to. 344 window.main_window_back_state = None 345 elif back_state is not None: 346 window.main_window_back_state = back_state 347 else: 348 oldwin = self._main_window() 349 if oldwin is None: 350 # We currenty only hold weak refs to windows so that 351 # they are free to die on their own, but we expect 352 # the main menu window to keep itself alive as long 353 # as its the main one. Holler if that seems to not 354 # be happening. 355 logging.warning( 356 'set_main_window: No old MainWindow found' 357 ' and is_top_level is False;' 358 ' this should not happen.' 359 ) 360 window.main_window_back_state = None 361 else: 362 window.main_window_back_state = self.save_main_window_state( 363 oldwin 364 ) 365 366 self._main_window = window_weakref 367 self._main_window_widget = window_widget 368 369 def has_main_window(self) -> bool: 370 """Return whether a main menu window is present.""" 371 return bool(self._main_window_widget) 372 373 def clear_main_window(self, transition: str | None = None) -> None: 374 """Clear any existing main window.""" 375 from bauiv1._uitypes import MainWindow 376 377 main_window = self._main_window()