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