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 8 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 increment_analytics_count, 64 is_browser_likely_available, 65 is_xcode_build, 66 lock_all_input, 67 LoginAdapter, 68 LoginInfo, 69 Lstr, 70 native_review_request, 71 native_review_request_supported, 72 NotFoundError, 73 open_file_externally, 74 open_url, 75 overlay_web_browser_close, 76 overlay_web_browser_is_open, 77 overlay_web_browser_is_supported, 78 overlay_web_browser_open_url, 79 Permission, 80 Plugin, 81 PluginSpec, 82 pushcall, 83 quit, 84 QuitType, 85 request_permission, 86 safecolor, 87 screenmessage, 88 set_analytics_screen, 89 set_low_level_config_value, 90 set_ui_input_device, 91 SpecialChar, 92 supports_max_fps, 93 supports_vsync, 94 timestring, 95 UIScale, 96 unlock_all_input, 97 WeakCall, 98 workspaces_in_use, 99) 100 101from _bauiv1 import ( 102 buttonwidget, 103 checkboxwidget, 104 columnwidget, 105 containerwidget, 106 get_qrcode_texture, 107 get_special_widget, 108 getmesh, 109 getsound, 110 gettexture, 111 hscrollwidget, 112 imagewidget, 113 is_party_icon_visible, 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 'increment_analytics_count', 192 'is_browser_likely_available', 193 'is_party_icon_visible', 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 babase.app.active changes. 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 babase.app.active changes. 59 60 The app-mode may want to take action such as pausing a running 61 game in such cases. 62 """
Called when babase.app.active changes.
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 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 WeakCall if that is not desired.
Examples
Print some stuff through time:
>>> screenmessage('hello from now!')
>>> apptimer(1.0, Call(screenmessage,
'hello from the future!'))
>>> apptimer(2.0, Call(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 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 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(): ... screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = AppTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... apptimer(3.89, stop_saying_it)
210class BasicMainWindowState(MainWindowState): 211 """A basic MainWindowState holding a lambda to recreate a MainWindow.""" 212 213 def __init__( 214 self, 215 create_call: Callable[ 216 [ 217 Literal['in_right', 'in_left', 'in_scale'] | None, 218 bauiv1.Widget | None, 219 ], 220 bauiv1.MainWindow, 221 ], 222 ) -> None: 223 super().__init__() 224 self.create_call = create_call 225 226 @override 227 def create_window( 228 self, 229 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 230 origin_widget: bauiv1.Widget | None = None, 231 ) -> bauiv1.MainWindow: 232 return self.create_call(transition, origin_widget)
A basic MainWindowState holding a lambda to recreate a MainWindow.
226 @override 227 def create_window( 228 self, 229 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 230 origin_widget: bauiv1.Widget | None = None, 231 ) -> bauiv1.MainWindow: 232 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 SpecialChar for the list of available characters.
199def checkboxwidget( 200 edit: bauiv1.Widget | None = None, 201 parent: bauiv1.Widget | None = None, 202 size: Sequence[float] | None = None, 203 position: Sequence[float] | None = None, 204 text: str | bauiv1.Lstr | None = None, 205 value: bool | None = None, 206 on_value_change_call: Callable[[bool], None] | None = None, 207 on_select_call: Callable[[], None] | None = None, 208 text_scale: float | None = None, 209 textcolor: Sequence[float] | None = None, 210 scale: float | None = None, 211 is_radio_button: bool | None = None, 212 maxwidth: float | None = None, 213 autoselect: bool | None = None, 214 color: Sequence[float] | None = None, 215) -> bauiv1.Widget: 216 """Create or edit a check-box widget. 217 218 Category: **User Interface Functions** 219 220 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 221 a new one is created and returned. Arguments that are not set to None 222 are applied to the Widget. 223 """ 224 import bauiv1 # pylint: disable=cyclic-import 225 226 return bauiv1.Widget()
Create or edit a check-box widget.
Category: User Interface Functions
Pass a valid existing 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 clipboard_is_supported() returns True before adding buttons/etc. that make use of this functionality.
229def columnwidget( 230 edit: bauiv1.Widget | None = None, 231 parent: bauiv1.Widget | None = None, 232 size: Sequence[float] | None = None, 233 position: Sequence[float] | None = None, 234 background: bool | None = None, 235 selected_child: bauiv1.Widget | None = None, 236 visible_child: bauiv1.Widget | None = None, 237 single_depth: bool | None = None, 238 print_list_exit_instructions: bool | None = None, 239 left_border: float | None = None, 240 top_border: float | None = None, 241 bottom_border: float | None = None, 242 selection_loops_to_parent: bool | None = None, 243 border: float | None = None, 244 margin: float | None = None, 245 claims_left_right: bool | None = None, 246 claims_tab: bool | None = None, 247) -> bauiv1.Widget: 248 """Create or edit a column widget. 249 250 Category: **User Interface Functions** 251 252 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 253 a new one is created and returned. Arguments that are not set to None 254 are applied to the Widget. 255 """ 256 import bauiv1 # pylint: disable=cyclic-import 257 258 return bauiv1.Widget()
Create or edit a column widget.
Category: User Interface Functions
Pass a valid existing 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.
261def containerwidget( 262 edit: bauiv1.Widget | None = None, 263 parent: bauiv1.Widget | None = None, 264 size: Sequence[float] | None = None, 265 position: Sequence[float] | None = None, 266 background: bool | None = None, 267 selected_child: bauiv1.Widget | None = None, 268 transition: str | None = None, 269 cancel_button: bauiv1.Widget | None = None, 270 start_button: bauiv1.Widget | None = None, 271 root_selectable: bool | None = None, 272 on_activate_call: Callable[[], None] | None = None, 273 claims_left_right: bool | None = None, 274 claims_tab: bool | None = None, 275 selection_loops: bool | None = None, 276 selection_loops_to_parent: bool | None = None, 277 scale: float | None = None, 278 on_outside_click_call: Callable[[], None] | None = None, 279 single_depth: bool | None = None, 280 visible_child: bauiv1.Widget | None = None, 281 stack_offset: Sequence[float] | None = None, 282 color: Sequence[float] | None = None, 283 on_cancel_call: Callable[[], None] | None = None, 284 print_list_exit_instructions: bool | None = None, 285 click_activate: bool | None = None, 286 always_highlight: bool | None = None, 287 selectable: bool | None = None, 288 scale_origin_stack_offset: Sequence[float] | None = None, 289 toolbar_visibility: ( 290 Literal[ 291 'menu_minimal', 292 'menu_minimal_no_back', 293 'menu_full', 294 'menu_full_no_back', 295 'menu_store', 296 'menu_store_no_back', 297 'menu_in_game', 298 'menu_tokens', 299 'get_tokens', 300 'inherit', 301 ] 302 | None 303 ) = None, 304 on_select_call: Callable[[], None] | None = None, 305 claim_outside_clicks: bool | None = None, 306 claims_up_down: bool | None = None, 307) -> bauiv1.Widget: 308 """Create or edit a container widget. 309 310 Category: **User Interface Functions** 311 312 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 313 a new one is created and returned. Arguments that are not set to None 314 are applied to the Widget. 315 """ 316 import bauiv1 # pylint: disable=cyclic-import 317 318 return bauiv1.Widget()
Create or edit a container widget.
Category: User Interface Functions
Pass a valid existing 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 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 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 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 WeakCall if that is not desired.
Examples
Print some stuff through time:
>>> screenmessage('hello from now!')
>>> displaytimer(1.0, Call(screenmessage,
... 'hello from the future!'))
>>> displaytimer(2.0, Call(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 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 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(): ... screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = DisplayTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... 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 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.
321def get_qrcode_texture(url: str) -> bauiv1.Texture: 322 """Return a QR code texture. 323 324 The provided url must be 64 bytes or less. 325 """ 326 import bauiv1 # pylint: disable=cyclic-import 327 328 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.
355def getmesh(name: str) -> bauiv1.Mesh: 356 """Load a mesh for use solely in the local user interface.""" 357 import bauiv1 # pylint: disable=cyclic-import 358 359 return bauiv1.Mesh()
Load a mesh for use solely in the local user interface.
362def getsound(name: str) -> bauiv1.Sound: 363 """Load a sound for use in the ui.""" 364 import bauiv1 # pylint: disable=cyclic-import 365 366 return bauiv1.Sound()
Load a sound for use in the ui.
369def gettexture(name: str) -> bauiv1.Texture: 370 """Load a texture for use in the ui.""" 371 import bauiv1 # pylint: disable=cyclic-import 372 373 return bauiv1.Texture()
Load a texture for use in the ui.
376def hscrollwidget( 377 edit: bauiv1.Widget | None = None, 378 parent: bauiv1.Widget | None = None, 379 size: Sequence[float] | None = None, 380 position: Sequence[float] | None = None, 381 background: bool | None = None, 382 selected_child: bauiv1.Widget | None = None, 383 capture_arrows: bool | None = None, 384 on_select_call: Callable[[], None] | None = None, 385 center_small_content: bool | None = None, 386 color: Sequence[float] | None = None, 387 highlight: bool | None = None, 388 border_opacity: float | None = None, 389 simple_culling_h: float | None = None, 390 claims_left_right: bool | None = None, 391 claims_up_down: bool | None = None, 392 claims_tab: bool | None = None, 393) -> bauiv1.Widget: 394 """Create or edit a horizontal scroll widget. 395 396 Category: **User Interface Functions** 397 398 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 399 a new one is created and returned. Arguments that are not set to None 400 are applied to the Widget. 401 """ 402 import bauiv1 # pylint: disable=cyclic-import 403 404 return bauiv1.Widget()
Create or edit a horizontal scroll widget.
Category: User Interface Functions
Pass a valid existing 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.
407def imagewidget( 408 edit: bauiv1.Widget | None = None, 409 parent: bauiv1.Widget | None = None, 410 size: Sequence[float] | None = None, 411 position: Sequence[float] | None = None, 412 color: Sequence[float] | None = None, 413 texture: bauiv1.Texture | None = None, 414 opacity: float | None = None, 415 mesh_transparent: bauiv1.Mesh | None = None, 416 mesh_opaque: bauiv1.Mesh | None = None, 417 has_alpha_channel: bool = True, 418 tint_texture: bauiv1.Texture | None = None, 419 tint_color: Sequence[float] | None = None, 420 transition_delay: float | None = None, 421 draw_controller: bauiv1.Widget | None = None, 422 tint2_color: Sequence[float] | None = None, 423 tilt_scale: float | None = None, 424 mask_texture: bauiv1.Texture | None = None, 425 radial_amount: float | None = None, 426 draw_controller_mult: float | None = None, 427) -> bauiv1.Widget: 428 """Create or edit an image widget. 429 430 Category: **User Interface Functions** 431 432 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 433 a new one is created and returned. Arguments that are not set to None 434 are applied to the Widget. 435 """ 436 import bauiv1 # pylint: disable=cyclic-import 437 438 return bauiv1.Widget()
Create or edit an image widget.
Category: User Interface Functions
Pass a valid existing 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 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 = 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 = 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 = Lstr(value='${A} / ${B}',
... subs=[('${A}', str(score)), ('${B}', str(total))])
EXAMPLE 4: 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
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 # TODO - move to MainWindow 65 # A back-state supplied by the ui system. 66 self.main_window_back_state: MainWindowState | None = None 67 68 self._main_window_transition = transition 69 self._main_window_origin_widget = origin_widget 70 super().__init__(root_widget, cleanupcheck) 71 72 scale_origin: tuple[float, float] | None 73 if origin_widget is not None: 74 self._main_window_transition_out = 'out_scale' 75 scale_origin = origin_widget.get_screen_space_center() 76 transition = 'in_scale' 77 else: 78 self._main_window_transition_out = 'out_right' 79 scale_origin = None 80 _bauiv1.containerwidget( 81 edit=root_widget, 82 transition=transition, 83 scale_origin_stack_offset=scale_origin, 84 ) 85 86 def main_window_close(self) -> None: 87 """Get window transitioning out if still alive.""" 88 89 # no-op if our underlying widget is dead or on its way out. 90 if not self._root_widget or self._root_widget.transitioning_out: 91 return 92 93 # Transition ourself out. 94 try: 95 self.on_main_window_close() 96 except Exception: 97 logging.exception('Error in on_main_window_close() for %s.', self) 98 99 _bauiv1.containerwidget( 100 edit=self._root_widget, transition=self._main_window_transition_out 101 ) 102 103 def can_change_main_window(self) -> bool: 104 """Is this MainWindow allowed to change the global main window? 105 106 It is a good idea to make sure this is True before calling 107 either main_window_back() or main_window_replace(). This 108 prevents fluke UI breakage such as multiple simultaneous events 109 causing a MainWindow to spawn multiple replacements for itself. 110 """ 111 # We are allowed to change main windows if we are the current one 112 # AND our underlying widget is still alive and not transitioning out. 113 return ( 114 babase.app.ui_v1.get_main_window() is self 115 and bool(self._root_widget) 116 and not self._root_widget.transitioning_out 117 ) 118 119 def main_window_back(self) -> None: 120 """Move back in the main window stack.""" 121 122 # Users should always check can_change_main_window() before 123 # calling us. Error if it seems they did not. 124 if not self.can_change_main_window(): 125 raise RuntimeError( 126 'main_window_back() should only be called' 127 ' if can_change_main_window() returns True' 128 ' (it currently is False).' 129 ) 130 131 # Get the 'back' window coming in. 132 babase.app.ui_v1.do_main_window_back(self) 133 134 self.main_window_close() 135 136 def main_window_replace( 137 self, new_window: MainWindow, group_id: str | None = None 138 ) -> None: 139 """Replace ourself with a new MainWindow.""" 140 141 # Users should always check can_change_main_window() before 142 # creating new MainWindows and passing them in here. Error if it 143 # seems they did not. 144 if not self.can_change_main_window(): 145 raise RuntimeError( 146 'main_window_replace() should only be called' 147 ' if can_change_main_window() returns True' 148 ' (it currently is False).' 149 ) 150 151 # If we're navigating within a group, we want it to look like we're 152 # backing out of the old one and going into the new one. 153 if ( 154 group_id is not None 155 and babase.app.ui_v1.main_window_group_id == group_id 156 ): 157 transition = self._main_window_transition_out 158 else: 159 # Otherwise just shove the old out the left to give the feel 160 # that we're adding to the nav stack. 161 transition = 'out_left' 162 163 # Transition ourself out. 164 try: 165 self.on_main_window_close() 166 except Exception: 167 logging.exception('Error in on_main_window_close() for %s.', self) 168 169 _bauiv1.containerwidget(edit=self._root_widget, transition=transition) 170 babase.app.ui_v1.set_main_window( 171 new_window, from_window=self, group_id=group_id 172 ) 173 174 def on_main_window_close(self) -> None: 175 """Called before transitioning out a main window. 176 177 A good opportunity to save window state/etc. 178 """ 179 180 def get_main_window_state(self) -> MainWindowState: 181 """Return a WindowState to recreate this window, if supported.""" 182 # TODO - change to NotImplementedError when moved to MainWindow. 183 raise RuntimeError('FIXME NOT IMPLEMENTED')
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 # TODO - move to MainWindow 65 # A back-state supplied by the ui system. 66 self.main_window_back_state: MainWindowState | None = None 67 68 self._main_window_transition = transition 69 self._main_window_origin_widget = origin_widget 70 super().__init__(root_widget, cleanupcheck) 71 72 scale_origin: tuple[float, float] | None 73 if origin_widget is not None: 74 self._main_window_transition_out = 'out_scale' 75 scale_origin = origin_widget.get_screen_space_center() 76 transition = 'in_scale' 77 else: 78 self._main_window_transition_out = 'out_right' 79 scale_origin = None 80 _bauiv1.containerwidget( 81 edit=root_widget, 82 transition=transition, 83 scale_origin_stack_offset=scale_origin, 84 )
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.
86 def main_window_close(self) -> None: 87 """Get window transitioning out if still alive.""" 88 89 # no-op if our underlying widget is dead or on its way out. 90 if not self._root_widget or self._root_widget.transitioning_out: 91 return 92 93 # Transition ourself out. 94 try: 95 self.on_main_window_close() 96 except Exception: 97 logging.exception('Error in on_main_window_close() for %s.', self) 98 99 _bauiv1.containerwidget( 100 edit=self._root_widget, transition=self._main_window_transition_out 101 )
Get window transitioning out if still alive.
103 def can_change_main_window(self) -> bool: 104 """Is this MainWindow allowed to change the global main window? 105 106 It is a good idea to make sure this is True before calling 107 either main_window_back() or main_window_replace(). This 108 prevents fluke UI breakage such as multiple simultaneous events 109 causing a MainWindow to spawn multiple replacements for itself. 110 """ 111 # We are allowed to change main windows if we are the current one 112 # AND our underlying widget is still alive and not transitioning out. 113 return ( 114 babase.app.ui_v1.get_main_window() is self 115 and bool(self._root_widget) 116 and not self._root_widget.transitioning_out 117 )
Is this MainWindow allowed to change the global main window?
It is a good idea to make sure this is True before calling either main_window_back() or main_window_replace(). This prevents fluke UI breakage such as multiple simultaneous events causing a MainWindow to spawn multiple replacements for itself.
119 def main_window_back(self) -> None: 120 """Move back in the main window stack.""" 121 122 # Users should always check can_change_main_window() before 123 # calling us. Error if it seems they did not. 124 if not self.can_change_main_window(): 125 raise RuntimeError( 126 'main_window_back() should only be called' 127 ' if can_change_main_window() returns True' 128 ' (it currently is False).' 129 ) 130 131 # Get the 'back' window coming in. 132 babase.app.ui_v1.do_main_window_back(self) 133 134 self.main_window_close()
Move back in the main window stack.
136 def main_window_replace( 137 self, new_window: MainWindow, group_id: str | None = None 138 ) -> None: 139 """Replace ourself with a new MainWindow.""" 140 141 # Users should always check can_change_main_window() before 142 # creating new MainWindows and passing them in here. Error if it 143 # seems they did not. 144 if not self.can_change_main_window(): 145 raise RuntimeError( 146 'main_window_replace() should only be called' 147 ' if can_change_main_window() returns True' 148 ' (it currently is False).' 149 ) 150 151 # If we're navigating within a group, we want it to look like we're 152 # backing out of the old one and going into the new one. 153 if ( 154 group_id is not None 155 and babase.app.ui_v1.main_window_group_id == group_id 156 ): 157 transition = self._main_window_transition_out 158 else: 159 # Otherwise just shove the old out the left to give the feel 160 # that we're adding to the nav stack. 161 transition = 'out_left' 162 163 # Transition ourself out. 164 try: 165 self.on_main_window_close() 166 except Exception: 167 logging.exception('Error in on_main_window_close() for %s.', self) 168 169 _bauiv1.containerwidget(edit=self._root_widget, transition=transition) 170 babase.app.ui_v1.set_main_window( 171 new_window, from_window=self, group_id=group_id 172 )
Replace ourself with a new MainWindow.
174 def on_main_window_close(self) -> None: 175 """Called before transitioning out a main window. 176 177 A good opportunity to save window state/etc. 178 """
Called before transitioning out a main window.
A good opportunity to save window state/etc.
180 def get_main_window_state(self) -> MainWindowState: 181 """Return a WindowState to recreate this window, if supported.""" 182 # TODO - change to NotImplementedError when moved to MainWindow. 183 raise RuntimeError('FIXME NOT IMPLEMENTED')
Return a WindowState to recreate this window, if supported.
Inherited Members
186class MainWindowState: 187 """Persistent state for a specific main-window and its ancestors. 188 189 This allows MainWindows to be automatically recreated for back-button 190 purposes, when switching app-modes, etc. 191 """ 192 193 def __init__(self, parent: MainWindowState | None = None) -> None: 194 # The window that back/cancel navigation should take us to. 195 self.parent = parent 196 197 def create_window( 198 self, 199 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 200 origin_widget: bauiv1.Widget | None = None, 201 ) -> MainWindow: 202 """Create a window based on this state. 203 204 WindowState child classes should override this to recreate their 205 particular type of window. 206 """ 207 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.
197 def create_window( 198 self, 199 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 200 origin_widget: bauiv1.Widget | None = None, 201 ) -> MainWindow: 202 """Create a window based on this state. 203 204 WindowState child classes should override this to recreate their 205 particular type of window. 206 """ 207 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
1309def open_url(address: str, force_fallback: bool = False) -> None: 1310 """Open the provided URL. 1311 1312 Category: **General Utility Functions** 1313 1314 Attempts to open the provided url in a web-browser. If that is not 1315 possible (or force_fallback is True), instead displays the url as 1316 a string and/or qrcode. 1317 """ 1318 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.
1321def overlay_web_browser_close() -> bool: 1322 """Close any open overlay web browser. 1323 1324 Category: **General Utility Functions** 1325 """ 1326 return bool()
Close any open overlay web browser.
Category: General Utility Functions
1329def overlay_web_browser_is_open() -> bool: 1330 """Return whether an overlay web browser is open currently. 1331 1332 Category: **General Utility Functions** 1333 """ 1334 return bool()
Return whether an overlay web browser is open currently.
Category: General Utility Functions
1337def overlay_web_browser_is_supported() -> bool: 1338 """Return whether an overlay web browser is supported here. 1339 1340 Category: **General Utility Functions** 1341 1342 An overlay web browser is a small dialog that pops up over the top 1343 of the main engine window. It can be used for performing simple 1344 tasks such as sign-ins. 1345 """ 1346 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.
1349def overlay_web_browser_open_url(address: str) -> None: 1350 """Open the provided URL in an overlayw web browser. 1351 1352 Category: **General Utility Functions** 1353 1354 An overlay web browser is a small dialog that pops up over the top 1355 of the main engine window. It can be used for performing simple 1356 tasks such as sign-ins. 1357 """ 1358 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.
1393def pushcall( 1394 call: Callable, 1395 from_other_thread: bool = False, 1396 suppress_other_thread_warning: bool = False, 1397 other_thread_use_fg_context: bool = False, 1398 raw: bool = False, 1399) -> None: 1400 """Push a call to the logic event-loop. 1401 Category: **General Utility Functions** 1402 1403 This call expects to be used in the logic thread, and will automatically 1404 save and restore the babase.Context to behave seamlessly. 1405 1406 If you want to push a call from outside of the logic thread, 1407 however, you can pass 'from_other_thread' as True. In this case 1408 the call will always run in the UI context_ref on the logic thread 1409 or whichever context_ref is in the foreground if 1410 other_thread_use_fg_context is True. 1411 Passing raw=True will disable thread checks and context_ref sets/restores. 1412 """ 1413 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.
1417def quit( 1418 confirm: bool = False, quit_type: babase.QuitType | None = None 1419) -> None: 1420 """Quit the app. 1421 1422 Category: **General Utility Functions** 1423 1424 If 'confirm' is True, a confirm dialog will be presented if conditions 1425 allow; otherwise the quit will still be immediate. 1426 See docs for babase.QuitType for explanations of the optional 1427 'quit_type' arg. 1428 """ 1429 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 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
456def rowwidget( 457 edit: bauiv1.Widget | None = None, 458 parent: bauiv1.Widget | None = None, 459 size: Sequence[float] | None = None, 460 position: Sequence[float] | None = None, 461 background: bool | None = None, 462 selected_child: bauiv1.Widget | None = None, 463 visible_child: bauiv1.Widget | None = None, 464 claims_left_right: bool | None = None, 465 claims_tab: bool | None = None, 466 selection_loops_to_parent: bool | None = None, 467) -> bauiv1.Widget: 468 """Create or edit a row widget. 469 470 Category: **User Interface Functions** 471 472 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 473 a new one is created and returned. Arguments that are not set to None 474 are applied to the Widget. 475 """ 476 import bauiv1 # pylint: disable=cyclic-import 477 478 return bauiv1.Widget()
Create or edit a row widget.
Category: User Interface Functions
Pass a valid existing 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.
1467def safecolor( 1468 color: Sequence[float], target_intensity: float = 0.6 1469) -> tuple[float, ...]: 1470 """Given a color tuple, return a color safe to display as text. 1471 1472 Category: **General Utility Functions** 1473 1474 Accepts tuples of length 3 or 4. This will slightly brighten very 1475 dark colors, etc. 1476 """ 1477 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.
1480def screenmessage( 1481 message: str | babase.Lstr, 1482 color: Sequence[float] | None = None, 1483 log: bool = False, 1484) -> None: 1485 """Print a message to the local client's screen, in a given color. 1486 1487 Category: **General Utility Functions** 1488 1489 Note that this version of the function is purely for local display. 1490 To broadcast screen messages in network play, look for methods such as 1491 broadcastmessage() provided by the scene-version packages. 1492 """ 1493 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.
481def scrollwidget( 482 edit: bauiv1.Widget | None = None, 483 parent: bauiv1.Widget | None = None, 484 size: Sequence[float] | None = None, 485 position: Sequence[float] | None = None, 486 background: bool | None = None, 487 selected_child: bauiv1.Widget | None = None, 488 capture_arrows: bool = False, 489 on_select_call: Callable | None = None, 490 center_small_content: bool | None = None, 491 color: Sequence[float] | None = None, 492 highlight: bool | None = None, 493 border_opacity: float | None = None, 494 simple_culling_v: float | None = None, 495 selection_loops_to_parent: bool | None = None, 496 claims_left_right: bool | None = None, 497 claims_up_down: bool | None = None, 498 claims_tab: bool | None = None, 499 autoselect: bool | None = None, 500) -> bauiv1.Widget: 501 """Create or edit a scroll widget. 502 503 Category: **User Interface Functions** 504 505 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 506 a new one is created and returned. Arguments that are not set to None 507 are applied to the Widget. 508 """ 509 import bauiv1 # pylint: disable=cyclic-import 510 511 return bauiv1.Widget()
Create or edit a scroll widget.
Category: User Interface Functions
Pass a valid existing 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.
1496def set_analytics_screen(screen: str) -> None: 1497 """Used for analytics to see where in the app players spend their time. 1498 1499 Category: **General Utility Functions** 1500 1501 Generally called when opening a new window or entering some UI. 1502 'screen' should be a string description of an app location 1503 ('Main Menu', etc.) 1504 """ 1505 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
519def textwidget( 520 edit: bauiv1.Widget | None = None, 521 parent: bauiv1.Widget | None = None, 522 size: Sequence[float] | None = None, 523 position: Sequence[float] | None = None, 524 text: str | bauiv1.Lstr | None = None, 525 v_align: str | None = None, 526 h_align: str | None = None, 527 editable: bool | None = None, 528 padding: float | None = None, 529 on_return_press_call: Callable[[], None] | None = None, 530 on_activate_call: Callable[[], None] | None = None, 531 selectable: bool | None = None, 532 query: bauiv1.Widget | None = None, 533 max_chars: int | None = None, 534 color: Sequence[float] | None = None, 535 click_activate: bool | None = None, 536 on_select_call: Callable[[], None] | None = None, 537 always_highlight: bool | None = None, 538 draw_controller: bauiv1.Widget | None = None, 539 scale: float | None = None, 540 corner_scale: float | None = None, 541 description: str | bauiv1.Lstr | None = None, 542 transition_delay: float | None = None, 543 maxwidth: float | None = None, 544 max_height: float | None = None, 545 flatness: float | None = None, 546 shadow: float | None = None, 547 autoselect: bool | None = None, 548 rotate: float | None = None, 549 enabled: bool | None = None, 550 force_internal_editing: bool | None = None, 551 always_show_carat: bool | None = None, 552 big: bool | None = None, 553 extra_touch_border_scale: float | None = None, 554 res_scale: float | None = None, 555 query_max_chars: bauiv1.Widget | None = None, 556 query_description: bauiv1.Widget | None = None, 557 adapter_finished: bool | None = None, 558 glow_type: str | None = None, 559 allow_clear_button: bool | None = None, 560) -> bauiv1.Widget: 561 """Create or edit a text widget. 562 563 Category: **User Interface Functions** 564 565 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 566 a new one is created and returned. Arguments that are not set to None 567 are applied to the Widget. 568 """ 569 import bauiv1 # pylint: disable=cyclic-import 570 571 return bauiv1.Widget()
Create or edit a text widget.
Category: User Interface Functions
Pass a valid existing 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 Lstr for displaying a time value.
Category: General Utility Functions
Given a time value, returns a 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.
244def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None: 245 """Checks to ensure a widget-owning object gets cleaned up properly. 246 247 Category: User Interface Functions 248 249 This adds a check which will print an error message if the provided 250 object still exists ~5 seconds after the provided bauiv1.Widget dies. 251 252 This is a good sanity check for any sort of object that wraps or 253 controls a bauiv1.Widget. For instance, a 'Window' class instance has 254 no reason to still exist once its root container bauiv1.Widget has fully 255 transitioned out and been destroyed. Circular references or careless 256 strong referencing can lead to such objects never getting destroyed, 257 however, and this helps detect such cases to avoid memory leaks. 258 """ 259 if DEBUG_UI_CLEANUP_CHECKS: 260 print(f'adding uicleanup to {obj}') 261 if not isinstance(widget, _bauiv1.Widget): 262 raise TypeError('widget arg is not a bauiv1.Widget') 263 264 if bool(False): 265 266 def foobar() -> None: 267 """Just testing.""" 268 if DEBUG_UI_CLEANUP_CHECKS: 269 print('uicleanupcheck widget dying...') 270 271 widget.add_delete_callback(foobar) 272 273 assert babase.app.classic is not None 274 babase.app.ui_v1.cleanupchecks.append( 275 UICleanupCheck( 276 obj=weakref.ref(obj), widget=widget, widget_death_time=None 277 ) 278 )
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 Widget dies.
This is a good sanity check for any sort of object that wraps or controls a Widget. For instance, a 'Window' class instance has no reason to still exist once its root container 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 LARGE = 0 85 MEDIUM = 1 86 SMALL = 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
31class UIV1AppSubsystem(babase.AppSubsystem): 32 """Consolidated UI functionality for the app. 33 34 Category: **App Classes** 35 36 To use this class, access the single instance of it at 'ba.app.ui'. 37 """ 38 39 class RootUIElement(Enum): 40 """Stuff provided by the root ui.""" 41 42 MENU_BUTTON = 'menu_button' 43 SQUAD_BUTTON = 'squad_button' 44 ACCOUNT_BUTTON = 'account_button' 45 SETTINGS_BUTTON = 'settings_button' 46 INBOX_BUTTON = 'inbox_button' 47 STORE_BUTTON = 'store_button' 48 INVENTORY_BUTTON = 'inventory_button' 49 ACHIEVEMENTS_BUTTON = 'achievements_button' 50 GET_TOKENS_BUTTON = 'get_tokens_button' 51 TICKETS_METER = 'tickets_meter' 52 TOKENS_METER = 'tokens_meter' 53 TROPHY_METER = 'trophy_meter' 54 LEVEL_METER = 'level_meter' 55 CHEST_SLOT_1 = 'chest_slot_1' 56 CHEST_SLOT_2 = 'chest_slot_2' 57 CHEST_SLOT_3 = 'chest_slot_3' 58 CHEST_SLOT_4 = 'chest_slot_4' 59 60 def __init__(self) -> None: 61 from bauiv1._uitypes import MainWindow 62 63 super().__init__() 64 env = babase.env() 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 self.main_window_group_id: str | None = None 73 74 self.quit_window: bauiv1.Widget | None = None 75 76 # The following should probably go away or move to classic. 77 # self._main_menu_location: str | None = None 78 79 # For storing arbitrary class-level state data for Windows or 80 # other UI related classes. 81 self.window_states: dict[type, Any] = {} 82 83 uiscalestr = babase.app.config.get('UI Scale', env['ui_scale']) 84 if uiscalestr == 'auto': 85 uiscalestr = env['ui_scale'] 86 87 self._uiscale: babase.UIScale 88 if uiscalestr == 'large': 89 self._uiscale = babase.UIScale.LARGE 90 elif uiscalestr == 'medium': 91 self._uiscale = babase.UIScale.MEDIUM 92 elif uiscalestr == 'small': 93 self._uiscale = babase.UIScale.SMALL 94 else: 95 logging.error("Invalid UIScale '%s'.", uiscalestr) 96 self._uiscale = babase.UIScale.MEDIUM 97 98 self.cleanupchecks: list[UICleanupCheck] = [] 99 self.upkeeptimer: babase.AppTimer | None = None 100 101 self.title_color = (0.72, 0.7, 0.75) 102 self.heading_color = (0.72, 0.7, 0.75) 103 self.infotextcolor = (0.7, 0.9, 0.7) 104 105 # Elements in our root UI will call anything here when activated. 106 self.root_ui_calls: dict[ 107 UIV1AppSubsystem.RootUIElement, Callable[[], None] 108 ] = {} 109 110 @property 111 def available(self) -> bool: 112 """Can uiv1 currently be used? 113 114 Code that may run in headless mode, before the UI has been spun up, 115 while other ui systems are active, etc. can check this to avoid 116 likely erroring. 117 """ 118 return _bauiv1.is_available() 119 120 @override 121 def reset(self) -> None: 122 from bauiv1._uitypes import MainWindow 123 124 self.root_ui_calls.clear() 125 self._main_window = empty_weakref(MainWindow) 126 self._main_window_widget = None 127 self.main_window_group_id = None 128 129 @property 130 def uiscale(self) -> babase.UIScale: 131 """Current ui scale for the app.""" 132 return self._uiscale 133 134 @override 135 def on_app_loading(self) -> None: 136 from bauiv1._uitypes import ui_upkeep 137 138 # IMPORTANT: If tweaking UI stuff, make sure it behaves for small, 139 # medium, and large UI modes. (doesn't run off screen, etc). 140 # The overrides below can be used to test with different sizes. 141 # Generally small is used on phones, medium is used on tablets/tvs, 142 # and large is on desktop computers or perhaps large tablets. When 143 # possible, run in windowed mode and resize the window to assure 144 # this holds true at all aspect ratios. 145 146 # UPDATE: A better way to test this is now by setting the environment 147 # variable BA_UI_SCALE to "small", "medium", or "large". 148 # This will affect system UIs not covered by the values below such 149 # as screen-messages. The below values remain functional, however, 150 # for cases such as Android where environment variables can't be set 151 # easily. 152 153 if bool(False): # force-test ui scale 154 self._uiscale = babase.UIScale.SMALL 155 with babase.ContextRef.empty(): 156 babase.pushcall( 157 lambda: babase.screenmessage( 158 f'FORCING UISCALE {self._uiscale.name} FOR TESTING', 159 color=(1, 0, 1), 160 log=True, 161 ) 162 ) 163 164 # Kick off our periodic UI upkeep. 165 # FIXME: Can probably kill this if we do immediate UI death checks. 166 self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True) 167 168 def do_main_window_back(self, window: MainWindow) -> None: 169 """Sets the main menu window automatically from a parent WindowState.""" 170 171 main_window = self._main_window() 172 back_state = ( 173 None if main_window is None else main_window.main_window_back_state 174 ) 175 if back_state is None: 176 raise RuntimeError( 177 f'Main window {main_window} provides no back-state;' 178 f' cannot use auto-back.' 179 ) 180 backwin = back_state.create_window(transition='in_left') 181 backwin.main_window_back_state = back_state.parent 182 self.set_main_window(backwin, from_window=window, is_back=True) 183 184 def get_main_window(self) -> bauiv1.MainWindow | None: 185 """Return main window, if any.""" 186 return self._main_window() 187 188 def set_main_window( 189 self, 190 window: bauiv1.MainWindow, 191 from_window: bauiv1.MainWindow | None | bool = True, 192 is_back: bool = False, 193 group_id: str | None = None, 194 is_top_level: bool = False, 195 back_state: MainWindowState | None = None, 196 ) -> None: 197 """Set the current 'main' window, replacing any existing. 198 199 If 'from_window' is passed as a bauiv1.Widget or bauiv1.Window 200 or None, a warning will be issued if it that value does not 201 match the current main window. This can help identify flawed 202 code that can lead to bad UI states. A value of False will 203 disable the check, which is necessary in some cases when the 204 current main window is not known. 205 206 When navigating somewhere from a cancel or back-button, pass 207 is_back=True; this will prevent the new main window from itself 208 being registered as a new location on the stack that can be 209 returned to. 210 211 If a 'group_id' string is provided and the window being replaced 212 has the same group-id, the WindowState stack is left unchanged, 213 effectively replacing the previous window with the new one in 214 the stack. This can be useful in cases where tab-bar-like UIs 215 allow flipping between sibling windows with the back button 216 always leading to a shared parent. 217 """ 218 # pylint: disable=too-many-locals 219 # pylint: disable=too-many-branches 220 # pylint: disable=too-many-statements 221 from bauiv1._uitypes import MainWindow 222 223 from_window_widget: bauiv1.Widget | None 224 225 # We used to accept Widgets but now want MainWindows. 226 assert isinstance(window, MainWindow) 227 window_weakref = weakref.ref(window) 228 window_widget = window.get_root_widget() 229 230 if isinstance(from_window, MainWindow): 231 from_window_widget = from_window.get_root_widget() 232 else: 233 from_window_widget = None 234 235 existing = self._main_window_widget 236 237 try: 238 if isinstance(from_window, bool): 239 # For default val True we warn that the arg wasn't 240 # passed. False can be explicitly passed to disable this 241 # check. 242 if from_window is True: 243 caller_frame = inspect.stack()[1] 244 caller_filename = caller_frame.filename 245 caller_line_number = caller_frame.lineno 246 logging.warning( 247 'set_main_window() should be passed a' 248 " 'from_window' value to help ensure proper UI behavior" 249 ' (%s line %i).', 250 caller_filename, 251 caller_line_number, 252 ) 253 else: 254 # For everything else, warn if what they passed wasn't 255 # the previous main menu widget. 256 if from_window_widget is not existing: 257 caller_frame = inspect.stack()[1] 258 caller_filename = caller_frame.filename 259 caller_line_number = caller_frame.lineno 260 logging.warning( 261 "set_main_window() was passed 'from_window' %s" 262 ' but existing main-menu-window is %s. (%s line %i).', 263 from_window_widget, 264 existing, 265 caller_filename, 266 caller_line_number, 267 ) 268 except Exception: 269 # Prevent any bugs in these checks from causing problems. 270 logging.exception('Error checking from_window') 271 272 # Once the above code leads to us fixing all leftover window 273 # bugs at the source, we can kill the code below. 274 275 # Let's grab the location where we were called from to report if 276 # we have to force-kill the existing window (which normally 277 # should not happen). 278 frameline = None 279 try: 280 frame = inspect.currentframe() 281 if frame is not None: 282 frame = frame.f_back 283 if frame is not None: 284 frameinfo = inspect.getframeinfo(frame) 285 frameline = f'{frameinfo.filename} {frameinfo.lineno}' 286 except Exception: 287 logging.exception('Error calcing line for set_main_window') 288 289 # NOTE: disabling this for now since hopefully our new system 290 # will be bulletproof enough to avoid this. Can turn it back on 291 # if that's not the case. 292 293 # With our legacy main-menu system, the caller is responsible 294 # for clearing out the old main menu window when assigning the 295 # new. However there are corner cases where that doesn't happen 296 # and we get old windows stuck under the new main one. So let's 297 # guard against that. However, we can't simply delete the 298 # existing main window when a new one is assigned because the 299 # user may transition the old out *after* the assignment. Sigh. 300 # So, as a happy medium, let's check in on the old after a short 301 # bit of time and kill it if its still alive. That will be a bit 302 # ugly on screen but at least should un-break things. 303 def _delay_kill() -> None: 304 import time 305 306 if existing: 307 print( 308 f'Killing old main_menu_window' 309 f' when called at: {frameline} t={time.time():.3f}' 310 ) 311 existing.delete() 312 313 if bool(False): 314 babase.apptimer(1.0, _delay_kill) 315 316 if is_back: 317 pass 318 else: 319 # When navigating forward, generate a back-window-state from 320 # the outgoing window. 321 322 # Exception is when we were passed a group and it matches 323 # the existing group; in that case we just keep the existing 324 # back-state. 325 if group_id is not None and group_id == self.main_window_group_id: 326 assert not is_top_level 327 print(f'GOT GROUP ID MATCH {group_id}; KEEPING BACK STATE.') 328 oldwin = self._main_window() 329 if oldwin is None: 330 # We currenty only hold weak refs to windows so 331 # that they are free to die on their own, but we 332 # expect the main menu window to keep itself 333 # alive as long as its the main one. Holler if 334 # that seems to not be happening. 335 logging.warning( 336 'set_main_window: no existing MainWindow found' 337 ' (and is_top_level is False); should not happen.' 338 ' a MainWindow should keep itself alive as long' 339 ' as it is main.' 340 ) 341 window.main_window_back_state = None 342 else: 343 window.main_window_back_state = ( 344 oldwin.main_window_back_state 345 ) 346 else: 347 if is_top_level: 348 # Top level windows don't have or expect anywhere to go 349 # back to. 350 # self._main_window_back_state = None 351 window.main_window_back_state = None 352 elif back_state is not None: 353 window.main_window_back_state = back_state 354 else: 355 oldwin = self._main_window() 356 if oldwin is None: 357 # We currenty only hold weak refs to windows so 358 # that they are free to die on their own, but we 359 # expect the main menu window to keep itself 360 # alive as long as its the main one. Holler if 361 # that seems to not be happening. 362 logging.warning( 363 'set_main_window: No old MainWindow found' 364 ' and is_top_level is False;' 365 ' this should not happen.' 366 ) 367 window.main_window_back_state = None 368 else: 369 oldwinstate = oldwin.get_main_window_state() 370 371 # Store our previous back state on this new one. 372 oldwinstate.parent = oldwin.main_window_back_state 373 window.main_window_back_state = oldwinstate 374 375 self._main_window = window_weakref 376 self._main_window_widget = window_widget 377 self.main_window_group_id = group_id 378 379 def has_main_window(self) -> bool: 380 """Return whether a main menu window is present.""" 381 return bool(self._main_window_widget) 382 383 def clear_main_window(self) -> None: 384 """Clear any existing main window.""" 385 from bauiv1._uitypes import MainWindow 386 387 main_window = self._main_window() 388 if main_window: 389 main_window.main_window_close() 390 else: 391 # Fallback; if we have a widget but no window, nuke the widget. 392 if self._main_window_widget: 393 logging.error( 394 'Have _main_window_widget but no main_window' 395 ' on clear_main_window; unexpected.' 396 ) 397 self._main_window_widget.delete() 398 399 self._main_window = empty_weakref(MainWindow) 400 self._main_window_widget = None 401 self.main_window_group_id = None
Consolidated UI functionality for the app.
Category: App Classes
To use this class, access the single instance of it at 'ba.app.ui'.
110 @property 111 def available(self) -> bool: 112 """Can uiv1 currently be used? 113 114 Code that may run in headless mode, before the UI has been spun up, 115 while other ui systems are active, etc. can check this to avoid 116 likely erroring. 117 """ 118 return _bauiv1.is_available()
Can uiv1 currently be used?
Code that may run in headless mode, before the UI has been spun up, while other ui systems are active, etc. can check this to avoid likely erroring.