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 supports_unicode_display, 96 timestring, 97 UIScale, 98 unlock_all_input, 99 utc_now_cloud, 100 WeakCall, 101 workspaces_in_use, 102) 103 104from _bauiv1 import ( 105 buttonwidget, 106 checkboxwidget, 107 columnwidget, 108 containerwidget, 109 get_qrcode_texture, 110 get_special_widget, 111 getmesh, 112 getsound, 113 gettexture, 114 hscrollwidget, 115 imagewidget, 116 Mesh, 117 rowwidget, 118 scrollwidget, 119 set_party_window_open, 120 Sound, 121 Texture, 122 textwidget, 123 uibounds, 124 Widget, 125 widget, 126) 127from bauiv1._keyboard import Keyboard 128from bauiv1._uitypes import ( 129 Window, 130 MainWindowState, 131 BasicMainWindowState, 132 uicleanupcheck, 133 MainWindow, 134) 135from bauiv1._appsubsystem import UIV1AppSubsystem 136 137__all__ = [ 138 'add_clean_frame_callback', 139 'allows_ticket_sales', 140 'app', 141 'AppIntent', 142 'AppIntentDefault', 143 'AppIntentExec', 144 'AppMode', 145 'appname', 146 'appnameupper', 147 'appnameupper', 148 'apptime', 149 'AppTime', 150 'apptimer', 151 'AppTimer', 152 'BasicMainWindowState', 153 'buttonwidget', 154 'Call', 155 'fullscreen_control_available', 156 'fullscreen_control_get', 157 'fullscreen_control_key_shortcut', 158 'fullscreen_control_set', 159 'charstr', 160 'checkboxwidget', 161 'clipboard_is_supported', 162 'clipboard_set_text', 163 'columnwidget', 164 'commit_app_config', 165 'containerwidget', 166 'ContextRef', 167 'displaytime', 168 'DisplayTime', 169 'displaytimer', 170 'DisplayTimer', 171 'do_once', 172 'fade_screen', 173 'get_display_resolution', 174 'get_input_idle_time', 175 'get_ip_address_type', 176 'get_low_level_config_value', 177 'get_max_graphics_quality', 178 'get_qrcode_texture', 179 'get_remote_app_name', 180 'get_replays_dir', 181 'get_special_widget', 182 'get_string_height', 183 'get_string_width', 184 'get_type_name', 185 'getclass', 186 'getmesh', 187 'getsound', 188 'gettexture', 189 'have_permission', 190 'hscrollwidget', 191 'imagewidget', 192 'in_logic_thread', 193 'in_main_menu', 194 'increment_analytics_count', 195 'is_browser_likely_available', 196 'is_xcode_build', 197 'Keyboard', 198 'lock_all_input', 199 'LoginAdapter', 200 'LoginInfo', 201 'Lstr', 202 'MainWindow', 203 'MainWindowState', 204 'Mesh', 205 'native_review_request', 206 'native_review_request_supported', 207 'NotFoundError', 208 'open_file_externally', 209 'open_url', 210 'overlay_web_browser_close', 211 'overlay_web_browser_is_open', 212 'overlay_web_browser_is_supported', 213 'overlay_web_browser_open_url', 214 'Permission', 215 'Plugin', 216 'PluginSpec', 217 'pushcall', 218 'quit', 219 'QuitType', 220 'request_permission', 221 'rowwidget', 222 'safecolor', 223 'screenmessage', 224 'scrollwidget', 225 'set_analytics_screen', 226 'set_low_level_config_value', 227 'set_party_window_open', 228 'set_ui_input_device', 229 'Sound', 230 'SpecialChar', 231 'supports_max_fps', 232 'supports_vsync', 233 'supports_unicode_display', 234 'Texture', 235 'textwidget', 236 'timestring', 237 'uibounds', 238 'uicleanupcheck', 239 'UIScale', 240 'UIV1AppSubsystem', 241 'unlock_all_input', 242 'utc_now_cloud', 243 'WeakCall', 244 'widget', 245 'Widget', 246 'Window', 247 'workspaces_in_use', 248] 249 250# We want stuff to show up as bauiv1.Foo instead of bauiv1._sub.Foo. 251set_canonical_module_names(globals()) 252 253# Sanity check: we want to keep ballistica's dependencies and 254# bootstrapping order clearly defined; let's check a few particular 255# modules to make sure they never directly or indirectly import us 256# before their own execs complete. 257if __debug__: 258 for _mdl in 'babase', '_babase': 259 if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): 260 logging.warning( 261 '%s was imported before %s finished importing;' 262 ' should not happen.', 263 __name__, 264 _mdl, 265 )
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 _can_handle_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 against current environment. 36 return cls._can_handle_intent(intent) 37 38 @classmethod 39 def _can_handle_intent(cls, intent: AppIntent) -> bool: 40 """Return whether our mode can handle the provided intent. 41 42 AppModes should override this to communicate what they can 43 handle. Note that AppExperience does not have to be considered 44 here; that is handled automatically by the can_handle_intent() 45 call. 46 """ 47 raise NotImplementedError('AppMode subclasses must override this.') 48 49 def handle_intent(self, intent: AppIntent) -> None: 50 """Handle an intent.""" 51 raise NotImplementedError('AppMode subclasses must override this.') 52 53 def on_activate(self) -> None: 54 """Called when the mode is being activated.""" 55 56 def on_deactivate(self) -> None: 57 """Called when the mode is being deactivated.""" 58 59 def on_app_active_changed(self) -> None: 60 """Called when ba*.app.active changes while this mode is active. 61 62 The app-mode may want to take action such as pausing a running 63 game in such cases. 64 """
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 _can_handle_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 against current environment. 36 return cls._can_handle_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 _can_handle_intent() method) AND the AppExperience associated with the AppMode must be supported by the current app and runtime environment.
49 def handle_intent(self, intent: AppIntent) -> None: 50 """Handle an intent.""" 51 raise NotImplementedError('AppMode subclasses must override this.')
Handle an intent.
59 def on_app_active_changed(self) -> None: 60 """Called when ba*.app.active changes while this mode is active. 61 62 The app-mode may want to take action such as pausing a running 63 game in such cases. 64 """
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.
554def apptime() -> babase.AppTime: 555 """Return the current app-time in seconds. 556 557 Category: **General Utility Functions** 558 559 App-time is a monotonic time value; it starts at 0.0 when the app 560 launches and will never jump by large amounts or go backwards, even if 561 the system time changes. Its progression will pause when the app is in 562 a suspended state. 563 564 Note that the AppTime returned here is simply float; it just has a 565 unique type in the type-checker's eyes to help prevent it from being 566 accidentally used with time functionality expecting other time types. 567 """ 568 import babase # pylint: disable=cyclic-import 569 570 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.
573def apptimer(time: float, call: Callable[[], Any]) -> None: 574 """Schedule a callable object to run based on app-time. 575 576 Category: **General Utility Functions** 577 578 This function creates a one-off timer which cannot be canceled or 579 modified once created. If you require the ability to do so, or need 580 a repeating timer, use the babase.AppTimer class instead. 581 582 ##### Arguments 583 ###### time (float) 584 > Length of time in seconds that the timer will wait before firing. 585 586 ###### call (Callable[[], Any]) 587 > A callable Python object. Note that the timer will retain a 588 strong reference to the callable for as long as the timer exists, so you 589 may want to look into concepts such as babase.WeakCall if that is not 590 desired. 591 592 ##### Examples 593 Print some stuff through time: 594 >>> babase.screenmessage('hello from now!') 595 >>> babase.apptimer(1.0, babase.Call(babase.screenmessage, 596 'hello from the future!')) 597 >>> babase.apptimer(2.0, babase.Call(babase.screenmessage, 598 ... 'hello from the future 2!')) 599 """ 600 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!'))
55class AppTimer: 56 """Timers are used to run code at later points in time. 57 58 Category: **General Utility Classes** 59 60 This class encapsulates a timer based on app-time. 61 The underlying timer will be destroyed when this object is no longer 62 referenced. If you do not want to worry about keeping a reference to 63 your timer around, use the babase.apptimer() function instead to get a 64 one-off timer. 65 66 ##### Arguments 67 ###### time 68 > Length of time in seconds that the timer will wait before firing. 69 70 ###### call 71 > A callable Python object. Remember that the timer will retain a 72 strong reference to the callable for as long as it exists, so you 73 may want to look into concepts such as babase.WeakCall if that is not 74 desired. 75 76 ###### repeat 77 > If True, the timer will fire repeatedly, with each successive 78 firing having the same delay as the first. 79 80 ##### Example 81 82 Use a Timer object to print repeatedly for a few seconds: 83 ... def say_it(): 84 ... babase.screenmessage('BADGER!') 85 ... def stop_saying_it(): 86 ... global g_timer 87 ... g_timer = None 88 ... babase.screenmessage('MUSHROOM MUSHROOM!') 89 ... # Create our timer; it will run as long as we have the self.t ref. 90 ... g_timer = babase.AppTimer(0.3, say_it, repeat=True) 91 ... # Now fire off a one-shot timer to kill it. 92 ... babase.apptimer(3.89, stop_saying_it) 93 """ 94 95 def __init__( 96 self, time: float, call: Callable[[], Any], repeat: bool = False 97 ) -> None: 98 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)
263class BasicMainWindowState(MainWindowState): 264 """A basic MainWindowState holding a lambda to recreate a MainWindow.""" 265 266 def __init__( 267 self, 268 create_call: Callable[ 269 [ 270 Literal['in_right', 'in_left', 'in_scale'] | None, 271 bauiv1.Widget | None, 272 ], 273 bauiv1.MainWindow, 274 ], 275 ) -> None: 276 super().__init__() 277 self.create_call = create_call 278 279 @override 280 def create_window( 281 self, 282 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 283 origin_widget: bauiv1.Widget | None = None, 284 ) -> bauiv1.MainWindow: 285 return self.create_call(transition, origin_widget)
A basic MainWindowState holding a lambda to recreate a MainWindow.
279 @override 280 def create_window( 281 self, 282 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 283 origin_widget: bauiv1.Widget | None = None, 284 ) -> bauiv1.MainWindow: 285 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.
618def charstr(char_id: babase.SpecialChar) -> str: 619 """Get a unicode string representing a special character. 620 621 Category: **General Utility Functions** 622 623 Note that these utilize the private-use block of unicode characters 624 (U+E000-U+F8FF) and are specific to the game; exporting or rendering 625 them elsewhere will be meaningless. 626 627 See babase.SpecialChar for the list of available characters. 628 """ 629 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.
203def checkboxwidget( 204 *, 205 edit: bauiv1.Widget | None = None, 206 parent: bauiv1.Widget | None = None, 207 size: Sequence[float] | None = None, 208 position: Sequence[float] | None = None, 209 text: str | bauiv1.Lstr | None = None, 210 value: bool | None = None, 211 on_value_change_call: Callable[[bool], None] | None = None, 212 on_select_call: Callable[[], None] | None = None, 213 text_scale: float | None = None, 214 textcolor: Sequence[float] | None = None, 215 scale: float | None = None, 216 is_radio_button: bool | None = None, 217 maxwidth: float | None = None, 218 autoselect: bool | None = None, 219 color: Sequence[float] | None = None, 220) -> bauiv1.Widget: 221 """Create or edit a check-box widget. 222 223 Category: **User Interface Functions** 224 225 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 226 a new one is created and returned. Arguments that are not set to None 227 are applied to the Widget. 228 """ 229 import bauiv1 # pylint: disable=cyclic-import 230 231 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.
654def clipboard_is_supported() -> bool: 655 """Return whether this platform supports clipboard operations at all. 656 657 Category: **General Utility Functions** 658 659 If this returns False, UIs should not show 'copy to clipboard' 660 buttons, etc. 661 """ 662 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.
665def clipboard_set_text(value: str) -> None: 666 """Copy a string to the system clipboard. 667 668 Category: **General Utility Functions** 669 670 Ensure that babase.clipboard_is_supported() returns True before adding 671 buttons/etc. that make use of this functionality. 672 """ 673 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.
234def columnwidget( 235 *, 236 edit: bauiv1.Widget | None = None, 237 parent: bauiv1.Widget | None = None, 238 size: Sequence[float] | None = None, 239 position: Sequence[float] | None = None, 240 background: bool | None = None, 241 selected_child: bauiv1.Widget | None = None, 242 visible_child: bauiv1.Widget | None = None, 243 single_depth: bool | None = None, 244 print_list_exit_instructions: bool | None = None, 245 left_border: float | None = None, 246 top_border: float | None = None, 247 bottom_border: float | None = None, 248 selection_loops_to_parent: bool | None = None, 249 border: float | None = None, 250 margin: float | None = None, 251 claims_left_right: bool | None = None, 252) -> bauiv1.Widget: 253 """Create or edit a column widget. 254 255 Category: **User Interface Functions** 256 257 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 258 a new one is created and returned. Arguments that are not set to None 259 are applied to the Widget. 260 """ 261 import bauiv1 # pylint: disable=cyclic-import 262 263 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.
266def containerwidget( 267 *, 268 edit: bauiv1.Widget | None = None, 269 parent: bauiv1.Widget | None = None, 270 id: str | None = None, 271 size: Sequence[float] | None = None, 272 position: Sequence[float] | None = None, 273 background: bool | None = None, 274 selected_child: bauiv1.Widget | None = None, 275 transition: str | None = None, 276 cancel_button: bauiv1.Widget | None = None, 277 start_button: bauiv1.Widget | None = None, 278 root_selectable: bool | None = None, 279 on_activate_call: Callable[[], None] | None = None, 280 claims_left_right: bool | None = None, 281 selection_loops: bool | None = None, 282 selection_loops_to_parent: bool | None = None, 283 scale: float | None = None, 284 on_outside_click_call: Callable[[], None] | None = None, 285 single_depth: bool | None = None, 286 visible_child: bauiv1.Widget | None = None, 287 stack_offset: Sequence[float] | None = None, 288 color: Sequence[float] | None = None, 289 on_cancel_call: Callable[[], None] | None = None, 290 print_list_exit_instructions: bool | None = None, 291 click_activate: bool | None = None, 292 always_highlight: bool | None = None, 293 selectable: bool | None = None, 294 scale_origin_stack_offset: Sequence[float] | None = None, 295 toolbar_visibility: ( 296 Literal[ 297 'menu_minimal', 298 'menu_minimal_no_back', 299 'menu_full', 300 'menu_full_no_back', 301 'menu_store', 302 'menu_store_no_back', 303 'menu_in_game', 304 'menu_tokens', 305 'get_tokens', 306 'no_menu_minimal', 307 'inherit', 308 ] 309 | None 310 ) = None, 311 on_select_call: Callable[[], None] | None = None, 312 claim_outside_clicks: bool | None = None, 313 claims_up_down: bool | None = None, 314) -> bauiv1.Widget: 315 """Create or edit a container widget. 316 317 Category: **User Interface Functions** 318 319 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 320 a new one is created and returned. Arguments that are not set to None 321 are applied to the Widget. 322 """ 323 import bauiv1 # pylint: disable=cyclic-import 324 325 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.
150class ContextRef: 151 """Store or use a ballistica context. 152 153 Category: **General Utility Classes** 154 155 Many operations such as bascenev1.newnode() or bascenev1.gettexture() 156 operate implicitly on a current 'context'. A context is some sort of 157 state that functionality can implicitly use. Context determines, for 158 example, which scene nodes or textures get added to without having to 159 specify it explicitly in the newnode()/gettexture() call. Contexts can 160 also affect object lifecycles; for example a babase.ContextCall will 161 become a no-op when the context it was created in is destroyed. 162 163 In general, if you are a modder, you should not need to worry about 164 contexts; mod code should mostly be getting run in the correct 165 context and timers and other callbacks will take care of saving 166 and restoring contexts automatically. There may be rare cases, 167 however, where you need to deal directly with contexts, and that is 168 where this class comes in. 169 170 Creating a babase.ContextRef() will capture a reference to the current 171 context. Other modules may provide ways to access their contexts; for 172 example a bascenev1.Activity instance has a 'context' attribute. You 173 can also use babase.ContextRef.empty() to create a reference to *no* 174 context. Some code such as UI calls may expect this and may complain 175 if you try to use them within a context. 176 177 ##### Usage 178 ContextRefs are generally used with the Python 'with' statement, which 179 sets the context they point to as current on entry and resets it to 180 the previous value on exit. 181 182 ##### Example 183 Explicitly create a few UI bits with no context set. 184 (UI stuff may complain if called within a context): 185 >>> with bui.ContextRef.empty(): 186 ... my_container = bui.containerwidget() 187 """ 188 189 def __init__( 190 self, 191 ) -> None: 192 pass 193 194 def __enter__(self) -> None: 195 """Support for "with" statement.""" 196 pass 197 198 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: 199 """Support for "with" statement.""" 200 pass 201 202 @classmethod 203 def empty(cls) -> ContextRef: 204 """Return a ContextRef pointing to no context. 205 206 This is useful when code should be run free of a context. 207 For example, UI code generally insists on being run this way. 208 Otherwise, callbacks set on the UI could inadvertently stop working 209 due to a game activity ending, which would be unintuitive behavior. 210 """ 211 return ContextRef() 212 213 def is_empty(self) -> bool: 214 """Whether the context was created as empty.""" 215 return bool() 216 217 def is_expired(self) -> bool: 218 """Whether the context has expired.""" 219 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()
202 @classmethod 203 def empty(cls) -> ContextRef: 204 """Return a ContextRef pointing to no context. 205 206 This is useful when code should be run free of a context. 207 For example, UI code generally insists on being run this way. 208 Otherwise, callbacks set on the UI could inadvertently stop working 209 due to a game activity ending, which would be unintuitive behavior. 210 """ 211 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.
759def displaytime() -> babase.DisplayTime: 760 """Return the current display-time in seconds. 761 762 Category: **General Utility Functions** 763 764 Display-time is a time value intended to be used for animation and other 765 visual purposes. It will generally increment by a consistent amount each 766 frame. It will pass at an overall similar rate to AppTime, but trades 767 accuracy for smoothness. 768 769 Note that the value returned here is simply a float; it just has a 770 unique type in the type-checker's eyes to help prevent it from being 771 accidentally used with time functionality expecting other time types. 772 """ 773 import babase # pylint: disable=cyclic-import 774 775 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.
778def displaytimer(time: float, call: Callable[[], Any]) -> None: 779 """Schedule a callable object to run based on display-time. 780 781 Category: **General Utility Functions** 782 783 This function creates a one-off timer which cannot be canceled or 784 modified once created. If you require the ability to do so, or need 785 a repeating timer, use the babase.DisplayTimer class instead. 786 787 Display-time is a time value intended to be used for animation and other 788 visual purposes. It will generally increment by a consistent amount each 789 frame. It will pass at an overall similar rate to AppTime, but trades 790 accuracy for smoothness. 791 792 ##### Arguments 793 ###### time (float) 794 > Length of time in seconds that the timer will wait before firing. 795 796 ###### call (Callable[[], Any]) 797 > A callable Python object. Note that the timer will retain a 798 strong reference to the callable for as long as the timer exists, so you 799 may want to look into concepts such as babase.WeakCall if that is not 800 desired. 801 802 ##### Examples 803 Print some stuff through time: 804 >>> babase.screenmessage('hello from now!') 805 >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage, 806 ... 'hello from the future!')) 807 >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage, 808 ... 'hello from the future 2!')) 809 """ 810 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!'))
222class DisplayTimer: 223 """Timers are used to run code at later points in time. 224 225 Category: **General Utility Classes** 226 227 This class encapsulates a timer based on display-time. 228 The underlying timer will be destroyed when this object is no longer 229 referenced. If you do not want to worry about keeping a reference to 230 your timer around, use the babase.displaytimer() function instead to get a 231 one-off timer. 232 233 Display-time is a time value intended to be used for animation and 234 other visual purposes. It will generally increment by a consistent 235 amount each frame. It will pass at an overall similar rate to AppTime, 236 but trades accuracy for smoothness. 237 238 ##### Arguments 239 ###### time 240 > Length of time in seconds that the timer will wait before firing. 241 242 ###### call 243 > A callable Python object. Remember that the timer will retain a 244 strong reference to the callable for as long as it exists, so you 245 may want to look into concepts such as babase.WeakCall if that is not 246 desired. 247 248 ###### repeat 249 > If True, the timer will fire repeatedly, with each successive 250 firing having the same delay as the first. 251 252 ##### Example 253 254 Use a Timer object to print repeatedly for a few seconds: 255 ... def say_it(): 256 ... babase.screenmessage('BADGER!') 257 ... def stop_saying_it(): 258 ... global g_timer 259 ... g_timer = None 260 ... babase.screenmessage('MUSHROOM MUSHROOM!') 261 ... # Create our timer; it will run as long as we have the self.t ref. 262 ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) 263 ... # Now fire off a one-shot timer to kill it. 264 ... babase.displaytimer(3.89, stop_saying_it) 265 """ 266 267 def __init__( 268 self, time: float, call: Callable[[], Any], repeat: bool = False 269 ) -> None: 270 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)
818def do_once() -> bool: 819 """Return whether this is the first time running a line of code. 820 821 Category: **General Utility Functions** 822 823 This is used by 'print_once()' type calls to keep from overflowing 824 logs. The call functions by registering the filename and line where 825 The call is made from. Returns True if this location has not been 826 registered already, and False if it has. 827 828 ##### Example 829 This print will only fire for the first loop iteration: 830 >>> for i in range(10): 831 ... if babase.do_once(): 832 ... print('HelloWorld once from loop!') 833 """ 834 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!')
1003def get_input_idle_time() -> float: 1004 """Return seconds since any local input occurred (touch, keypress, etc.).""" 1005 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.
328def get_qrcode_texture(url: str) -> bauiv1.Texture: 329 """Return a QR code texture. 330 331 The provided url must be 64 bytes or less. 332 """ 333 import bauiv1 # pylint: disable=cyclic-import 334 335 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.
366def getmesh(name: str) -> bauiv1.Mesh: 367 """Load a mesh for use solely in the local user interface.""" 368 import bauiv1 # pylint: disable=cyclic-import 369 370 return bauiv1.Mesh()
Load a mesh for use solely in the local user interface.
373def getsound(name: str) -> bauiv1.Sound: 374 """Load a sound for use in the ui.""" 375 import bauiv1 # pylint: disable=cyclic-import 376 377 return bauiv1.Sound()
Load a sound for use in the ui.
380def gettexture(name: str) -> bauiv1.Texture: 381 """Load a texture for use in the ui.""" 382 import bauiv1 # pylint: disable=cyclic-import 383 384 return bauiv1.Texture()
Load a texture for use in the ui.
387def hscrollwidget( 388 *, 389 edit: bauiv1.Widget | None = None, 390 parent: bauiv1.Widget | None = None, 391 size: Sequence[float] | None = None, 392 position: Sequence[float] | None = None, 393 background: bool | None = None, 394 selected_child: bauiv1.Widget | None = None, 395 capture_arrows: bool | None = None, 396 on_select_call: Callable[[], None] | None = None, 397 center_small_content: bool | None = None, 398 color: Sequence[float] | None = None, 399 highlight: bool | None = None, 400 border_opacity: float | None = None, 401 simple_culling_h: float | None = None, 402 claims_left_right: bool | None = None, 403 claims_up_down: bool | None = None, 404) -> bauiv1.Widget: 405 """Create or edit a horizontal scroll widget. 406 407 Category: **User Interface Functions** 408 409 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 410 a new one is created and returned. Arguments that are not set to None 411 are applied to the Widget. 412 """ 413 import bauiv1 # pylint: disable=cyclic-import 414 415 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.
418def imagewidget( 419 *, 420 edit: bauiv1.Widget | None = None, 421 parent: bauiv1.Widget | None = None, 422 size: Sequence[float] | None = None, 423 position: Sequence[float] | None = None, 424 color: Sequence[float] | None = None, 425 texture: bauiv1.Texture | None = None, 426 opacity: float | None = None, 427 mesh_transparent: bauiv1.Mesh | None = None, 428 mesh_opaque: bauiv1.Mesh | None = None, 429 has_alpha_channel: bool = True, 430 tint_texture: bauiv1.Texture | None = None, 431 tint_color: Sequence[float] | None = None, 432 transition_delay: float | None = None, 433 draw_controller: bauiv1.Widget | None = None, 434 tint2_color: Sequence[float] | None = None, 435 tilt_scale: float | None = None, 436 mask_texture: bauiv1.Texture | None = None, 437 radial_amount: float | None = None, 438 draw_controller_mult: float | None = None, 439) -> bauiv1.Widget: 440 """Create or edit an image widget. 441 442 Category: **User Interface Functions** 443 444 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 445 a new one is created and returned. Arguments that are not set to None 446 are applied to the Widget. 447 """ 448 import bauiv1 # pylint: disable=cyclic-import 449 450 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.
38def is_browser_likely_available() -> bool: 39 """Return whether a browser likely exists on the current device. 40 41 category: General Utility Functions 42 43 If this returns False you may want to avoid calling babase.show_url() 44 with any lengthy addresses. (ba.show_url() will display an address 45 as a string in a window if unable to bring up a browser, but that 46 is only useful for simple URLs.) 47 """ 48 app = _babase.app 49 50 if app.classic is None: 51 logging.warning( 52 'is_browser_likely_available() needs to be updated' 53 ' to work without classic.' 54 ) 55 return True 56 57 platform = app.classic.platform 58 hastouchscreen = _babase.hastouchscreen() 59 60 # If we're on a vr device or an android device with no touchscreen, 61 # assume no browser. 62 # FIXME: Might not be the case anymore; should make this definable 63 # at the platform level. 64 if app.env.vr or (platform == 'android' and not hastouchscreen): 65 return False 66 67 # Anywhere else assume we've got one. 68 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.
496class Lstr: 497 """Used to define strings in a language-independent way. 498 499 Category: **General Utility Classes** 500 501 These should be used whenever possible in place of hard-coded 502 strings so that in-game or UI elements show up correctly on all 503 clients in their currently-active language. 504 505 To see available resource keys, look at any of the bs_language_*.py 506 files in the game or the translations pages at 507 legacy.ballistica.net/translate. 508 509 ##### Examples 510 EXAMPLE 1: specify a string from a resource path 511 >>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText') 512 513 EXAMPLE 2: specify a translated string via a category and english 514 value; if a translated value is available, it will be used; otherwise 515 the english value will be. To see available translation categories, 516 look under the 'translations' resource section. 517 >>> mynode.text = babase.Lstr(translate=('gameDescriptions', 518 ... 'Defeat all enemies')) 519 520 EXAMPLE 3: specify a raw value and some substitutions. Substitutions 521 can be used with resource and translate modes as well. 522 >>> mynode.text = babase.Lstr(value='${A} / ${B}', 523 ... subs=[('${A}', str(score)), ('${B}', str(total))]) 524 525 EXAMPLE 4: babase.Lstr's can be nested. This example would display the 526 resource at res_a but replace ${NAME} with the value of the 527 resource at res_b 528 >>> mytextnode.text = babase.Lstr( 529 ... resource='res_a', 530 ... subs=[('${NAME}', babase.Lstr(resource='res_b'))]) 531 """ 532 533 # This class is used a lot in UI stuff and doesn't need to be 534 # flexible, so let's optimize its performance a bit. 535 __slots__ = ['args'] 536 537 @overload 538 def __init__( 539 self, 540 *, 541 resource: str, 542 fallback_resource: str = '', 543 fallback_value: str = '', 544 subs: Sequence[tuple[str, str | Lstr]] | None = None, 545 ) -> None: 546 """Create an Lstr from a string resource.""" 547 548 @overload 549 def __init__( 550 self, 551 *, 552 translate: tuple[str, str], 553 subs: Sequence[tuple[str, str | Lstr]] | None = None, 554 ) -> None: 555 """Create an Lstr by translating a string in a category.""" 556 557 @overload 558 def __init__( 559 self, 560 *, 561 value: str, 562 subs: Sequence[tuple[str, str | Lstr]] | None = None, 563 ) -> None: 564 """Create an Lstr from a raw string value.""" 565 566 def __init__(self, *args: Any, **keywds: Any) -> None: 567 """Instantiate a Lstr. 568 569 Pass a value for either 'resource', 'translate', 570 or 'value'. (see Lstr help for examples). 571 'subs' can be a sequence of 2-member sequences consisting of values 572 and replacements. 573 'fallback_resource' can be a resource key that will be used if the 574 main one is not present for 575 the current language in place of falling back to the english value 576 ('resource' mode only). 577 'fallback_value' can be a literal string that will be used if neither 578 the resource nor the fallback resource is found ('resource' mode only). 579 """ 580 # pylint: disable=too-many-branches 581 if args: 582 raise TypeError('Lstr accepts only keyword arguments') 583 584 # Basically just store the exact args they passed. 585 # However if they passed any Lstr values for subs, 586 # replace them with that Lstr's dict. 587 self.args = keywds 588 our_type = type(self) 589 590 if isinstance(self.args.get('value'), our_type): 591 raise TypeError("'value' must be a regular string; not an Lstr") 592 593 if 'subs' in keywds: 594 subs = keywds.get('subs') 595 subs_filtered = [] 596 if subs is not None: 597 for key, value in keywds['subs']: 598 if isinstance(value, our_type): 599 subs_filtered.append((key, value.args)) 600 else: 601 subs_filtered.append((key, value)) 602 self.args['subs'] = subs_filtered 603 604 # As of protocol 31 we support compact key names 605 # ('t' instead of 'translate', etc). Convert as needed. 606 if 'translate' in keywds: 607 keywds['t'] = keywds['translate'] 608 del keywds['translate'] 609 if 'resource' in keywds: 610 keywds['r'] = keywds['resource'] 611 del keywds['resource'] 612 if 'value' in keywds: 613 keywds['v'] = keywds['value'] 614 del keywds['value'] 615 if 'fallback' in keywds: 616 from babase import _error 617 618 _error.print_error( 619 'deprecated "fallback" arg passed to Lstr(); use ' 620 'either "fallback_resource" or "fallback_value"', 621 once=True, 622 ) 623 keywds['f'] = keywds['fallback'] 624 del keywds['fallback'] 625 if 'fallback_resource' in keywds: 626 keywds['f'] = keywds['fallback_resource'] 627 del keywds['fallback_resource'] 628 if 'subs' in keywds: 629 keywds['s'] = keywds['subs'] 630 del keywds['subs'] 631 if 'fallback_value' in keywds: 632 keywds['fv'] = keywds['fallback_value'] 633 del keywds['fallback_value'] 634 635 def evaluate(self) -> str: 636 """Evaluate the Lstr and returns a flat string in the current language. 637 638 You should avoid doing this as much as possible and instead pass 639 and store Lstr values. 640 """ 641 return _babase.evaluate_lstr(self._get_json()) 642 643 def is_flat_value(self) -> bool: 644 """Return whether the Lstr is a 'flat' value. 645 646 This is defined as a simple string value incorporating no 647 translations, resources, or substitutions. In this case it may 648 be reasonable to replace it with a raw string value, perform 649 string manipulation on it, etc. 650 """ 651 return bool('v' in self.args and not self.args.get('s', [])) 652 653 def _get_json(self) -> str: 654 try: 655 return json.dumps(self.args, separators=(',', ':')) 656 except Exception: 657 from babase import _error 658 659 _error.print_exception('_get_json failed for', self.args) 660 return 'JSON_ERR' 661 662 @override 663 def __str__(self) -> str: 664 return '<ba.Lstr: ' + self._get_json() + '>' 665 666 @override 667 def __repr__(self) -> str: 668 return '<ba.Lstr: ' + self._get_json() + '>' 669 670 @staticmethod 671 def from_json(json_string: str) -> babase.Lstr: 672 """Given a json string, returns a babase.Lstr. Does no validation.""" 673 lstr = Lstr(value='') 674 lstr.args = json.loads(json_string) 675 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'))])
566 def __init__(self, *args: Any, **keywds: Any) -> None: 567 """Instantiate a Lstr. 568 569 Pass a value for either 'resource', 'translate', 570 or 'value'. (see Lstr help for examples). 571 'subs' can be a sequence of 2-member sequences consisting of values 572 and replacements. 573 'fallback_resource' can be a resource key that will be used if the 574 main one is not present for 575 the current language in place of falling back to the english value 576 ('resource' mode only). 577 'fallback_value' can be a literal string that will be used if neither 578 the resource nor the fallback resource is found ('resource' mode only). 579 """ 580 # pylint: disable=too-many-branches 581 if args: 582 raise TypeError('Lstr accepts only keyword arguments') 583 584 # Basically just store the exact args they passed. 585 # However if they passed any Lstr values for subs, 586 # replace them with that Lstr's dict. 587 self.args = keywds 588 our_type = type(self) 589 590 if isinstance(self.args.get('value'), our_type): 591 raise TypeError("'value' must be a regular string; not an Lstr") 592 593 if 'subs' in keywds: 594 subs = keywds.get('subs') 595 subs_filtered = [] 596 if subs is not None: 597 for key, value in keywds['subs']: 598 if isinstance(value, our_type): 599 subs_filtered.append((key, value.args)) 600 else: 601 subs_filtered.append((key, value)) 602 self.args['subs'] = subs_filtered 603 604 # As of protocol 31 we support compact key names 605 # ('t' instead of 'translate', etc). Convert as needed. 606 if 'translate' in keywds: 607 keywds['t'] = keywds['translate'] 608 del keywds['translate'] 609 if 'resource' in keywds: 610 keywds['r'] = keywds['resource'] 611 del keywds['resource'] 612 if 'value' in keywds: 613 keywds['v'] = keywds['value'] 614 del keywds['value'] 615 if 'fallback' in keywds: 616 from babase import _error 617 618 _error.print_error( 619 'deprecated "fallback" arg passed to Lstr(); use ' 620 'either "fallback_resource" or "fallback_value"', 621 once=True, 622 ) 623 keywds['f'] = keywds['fallback'] 624 del keywds['fallback'] 625 if 'fallback_resource' in keywds: 626 keywds['f'] = keywds['fallback_resource'] 627 del keywds['fallback_resource'] 628 if 'subs' in keywds: 629 keywds['s'] = keywds['subs'] 630 del keywds['subs'] 631 if 'fallback_value' in keywds: 632 keywds['fv'] = keywds['fallback_value'] 633 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).
635 def evaluate(self) -> str: 636 """Evaluate the Lstr and returns a flat string in the current language. 637 638 You should avoid doing this as much as possible and instead pass 639 and store Lstr values. 640 """ 641 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.
643 def is_flat_value(self) -> bool: 644 """Return whether the Lstr is a 'flat' value. 645 646 This is defined as a simple string value incorporating no 647 translations, resources, or substitutions. In this case it may 648 be reasonable to replace it with a raw string value, perform 649 string manipulation on it, etc. 650 """ 651 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 type of window that can be set as 'main'. 51 52 The UI system has at most one main window at any given time. 53 MainWindows support high level functionality such as saving and 54 restoring states, allowing them to be automatically recreated when 55 navigating back from other locations or when something like ui-scale 56 changes. 57 """ 58 59 def __init__( 60 self, 61 root_widget: bauiv1.Widget, 62 transition: str | None, 63 origin_widget: bauiv1.Widget | None, 64 cleanupcheck: bool = True, 65 ): 66 """Create a MainWindow given a root widget and transition info. 67 68 Automatically handles in and out transitions on the provided widget, 69 so there is no need to set transitions when creating it. 70 """ 71 # A back-state supplied by the ui system. 72 self.main_window_back_state: MainWindowState | None = None 73 74 self.main_window_is_top_level: bool = False 75 76 # Windows can be flagged as auxiliary when not related to the 77 # main UI task at hand. UI code may choose to handle auxiliary 78 # windows in special ways, such as by implicitly replacing 79 # existing auxiliary windows with new ones instead of keeping 80 # old ones as back targets. 81 self.main_window_is_auxiliary: bool = False 82 83 self._main_window_transition = transition 84 self._main_window_origin_widget = origin_widget 85 super().__init__(root_widget, cleanupcheck) 86 87 scale_origin: tuple[float, float] | None 88 if origin_widget is not None: 89 self._main_window_transition_out = 'out_scale' 90 scale_origin = origin_widget.get_screen_space_center() 91 transition = 'in_scale' 92 else: 93 self._main_window_transition_out = 'out_right' 94 scale_origin = None 95 _bauiv1.containerwidget( 96 edit=root_widget, 97 transition=transition, 98 scale_origin_stack_offset=scale_origin, 99 ) 100 101 def main_window_close(self, transition: str | None = None) -> None: 102 """Get window transitioning out if still alive.""" 103 104 # no-op if our underlying widget is dead or on its way out. 105 if not self._root_widget or self._root_widget.transitioning_out: 106 return 107 108 # Transition ourself out. 109 try: 110 self.on_main_window_close() 111 except Exception: 112 logging.exception('Error in on_main_window_close() for %s.', self) 113 114 # Note: normally transition of None means instant, but we use 115 # that to mean 'do the default' so we support a special 116 # 'instant' string.. 117 if transition == 'instant': 118 self._root_widget.delete() 119 else: 120 _bauiv1.containerwidget( 121 edit=self._root_widget, 122 transition=( 123 self._main_window_transition_out 124 if transition is None 125 else transition 126 ), 127 ) 128 129 def main_window_has_control(self) -> bool: 130 """Is this MainWindow allowed to change the global main window? 131 132 It is a good idea to make sure this is True before calling 133 main_window_replace(). This prevents fluke UI breakage such as 134 multiple simultaneous events causing a MainWindow to spawn 135 multiple replacements for itself. 136 """ 137 # We are allowed to change main windows if we are the current one 138 # AND our underlying widget is still alive and not transitioning out. 139 return ( 140 babase.app.ui_v1.get_main_window() is self 141 and bool(self._root_widget) 142 and not self._root_widget.transitioning_out 143 ) 144 145 def main_window_back(self) -> None: 146 """Move back in the main window stack. 147 148 Is a no-op if the main window does not have control; 149 no need to check main_window_has_control() first. 150 """ 151 152 # Users should always check main_window_has_control() before 153 # calling us. Error if it seems they did not. 154 if not self.main_window_has_control(): 155 return 156 157 uiv1 = babase.app.ui_v1 158 159 # Get the 'back' window coming in. 160 if not self.main_window_is_top_level: 161 162 back_state = self.main_window_back_state 163 if back_state is None: 164 raise RuntimeError( 165 f'Main window {self} provides no back-state.' 166 ) 167 168 # Valid states should have values here. 169 assert back_state.is_top_level is not None 170 assert back_state.is_auxiliary is not None 171 assert back_state.window_type is not None 172 173 backwin = back_state.create_window(transition='in_left') 174 175 uiv1.set_main_window( 176 backwin, 177 from_window=self, 178 is_back=True, 179 back_state=back_state, 180 suppress_warning=True, 181 ) 182 183 # Transition ourself out. 184 self.main_window_close() 185 186 def main_window_replace( 187 self, 188 new_window: MainWindow, 189 back_state: MainWindowState | None = None, 190 is_auxiliary: bool = False, 191 ) -> None: 192 """Replace ourself with a new MainWindow.""" 193 194 # Users should always check main_window_has_control() *before* 195 # creating new MainWindows and passing them in here. Kill the 196 # passed window and Error if it seems they did not. 197 if not self.main_window_has_control(): 198 new_window.get_root_widget().delete() 199 raise RuntimeError( 200 f'main_window_replace() called on a not-in-control window' 201 f' ({self}); always check main_window_has_control() before' 202 f' calling main_window_replace().' 203 ) 204 205 # Just shove the old out the left to give the feel that we're 206 # adding to the nav stack. 207 transition = 'out_left' 208 209 # Transition ourself out. 210 try: 211 self.on_main_window_close() 212 except Exception: 213 logging.exception('Error in on_main_window_close() for %s.', self) 214 215 _bauiv1.containerwidget(edit=self._root_widget, transition=transition) 216 babase.app.ui_v1.set_main_window( 217 new_window, 218 from_window=self, 219 back_state=back_state, 220 is_auxiliary=is_auxiliary, 221 suppress_warning=True, 222 ) 223 224 def on_main_window_close(self) -> None: 225 """Called before transitioning out a main window. 226 227 A good opportunity to save window state/etc. 228 """ 229 230 def get_main_window_state(self) -> MainWindowState: 231 """Return a WindowState to recreate this window, if supported.""" 232 raise NotImplementedError()
A special type of window that can be set as 'main'.
The UI system has at most one main window at any given time. MainWindows support high level functionality such as saving and restoring states, allowing them to be automatically recreated when navigating back from other locations or when something like ui-scale changes.
59 def __init__( 60 self, 61 root_widget: bauiv1.Widget, 62 transition: str | None, 63 origin_widget: bauiv1.Widget | None, 64 cleanupcheck: bool = True, 65 ): 66 """Create a MainWindow given a root widget and transition info. 67 68 Automatically handles in and out transitions on the provided widget, 69 so there is no need to set transitions when creating it. 70 """ 71 # A back-state supplied by the ui system. 72 self.main_window_back_state: MainWindowState | None = None 73 74 self.main_window_is_top_level: bool = False 75 76 # Windows can be flagged as auxiliary when not related to the 77 # main UI task at hand. UI code may choose to handle auxiliary 78 # windows in special ways, such as by implicitly replacing 79 # existing auxiliary windows with new ones instead of keeping 80 # old ones as back targets. 81 self.main_window_is_auxiliary: bool = False 82 83 self._main_window_transition = transition 84 self._main_window_origin_widget = origin_widget 85 super().__init__(root_widget, cleanupcheck) 86 87 scale_origin: tuple[float, float] | None 88 if origin_widget is not None: 89 self._main_window_transition_out = 'out_scale' 90 scale_origin = origin_widget.get_screen_space_center() 91 transition = 'in_scale' 92 else: 93 self._main_window_transition_out = 'out_right' 94 scale_origin = None 95 _bauiv1.containerwidget( 96 edit=root_widget, 97 transition=transition, 98 scale_origin_stack_offset=scale_origin, 99 )
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.
101 def main_window_close(self, transition: str | None = None) -> None: 102 """Get window transitioning out if still alive.""" 103 104 # no-op if our underlying widget is dead or on its way out. 105 if not self._root_widget or self._root_widget.transitioning_out: 106 return 107 108 # Transition ourself out. 109 try: 110 self.on_main_window_close() 111 except Exception: 112 logging.exception('Error in on_main_window_close() for %s.', self) 113 114 # Note: normally transition of None means instant, but we use 115 # that to mean 'do the default' so we support a special 116 # 'instant' string.. 117 if transition == 'instant': 118 self._root_widget.delete() 119 else: 120 _bauiv1.containerwidget( 121 edit=self._root_widget, 122 transition=( 123 self._main_window_transition_out 124 if transition is None 125 else transition 126 ), 127 )
Get window transitioning out if still alive.
129 def main_window_has_control(self) -> bool: 130 """Is this MainWindow allowed to change the global main window? 131 132 It is a good idea to make sure this is True before calling 133 main_window_replace(). This prevents fluke UI breakage such as 134 multiple simultaneous events causing a MainWindow to spawn 135 multiple replacements for itself. 136 """ 137 # We are allowed to change main windows if we are the current one 138 # AND our underlying widget is still alive and not transitioning out. 139 return ( 140 babase.app.ui_v1.get_main_window() is self 141 and bool(self._root_widget) 142 and not self._root_widget.transitioning_out 143 )
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.
145 def main_window_back(self) -> None: 146 """Move back in the main window stack. 147 148 Is a no-op if the main window does not have control; 149 no need to check main_window_has_control() first. 150 """ 151 152 # Users should always check main_window_has_control() before 153 # calling us. Error if it seems they did not. 154 if not self.main_window_has_control(): 155 return 156 157 uiv1 = babase.app.ui_v1 158 159 # Get the 'back' window coming in. 160 if not self.main_window_is_top_level: 161 162 back_state = self.main_window_back_state 163 if back_state is None: 164 raise RuntimeError( 165 f'Main window {self} provides no back-state.' 166 ) 167 168 # Valid states should have values here. 169 assert back_state.is_top_level is not None 170 assert back_state.is_auxiliary is not None 171 assert back_state.window_type is not None 172 173 backwin = back_state.create_window(transition='in_left') 174 175 uiv1.set_main_window( 176 backwin, 177 from_window=self, 178 is_back=True, 179 back_state=back_state, 180 suppress_warning=True, 181 ) 182 183 # Transition ourself out. 184 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.
186 def main_window_replace( 187 self, 188 new_window: MainWindow, 189 back_state: MainWindowState | None = None, 190 is_auxiliary: bool = False, 191 ) -> None: 192 """Replace ourself with a new MainWindow.""" 193 194 # Users should always check main_window_has_control() *before* 195 # creating new MainWindows and passing them in here. Kill the 196 # passed window and Error if it seems they did not. 197 if not self.main_window_has_control(): 198 new_window.get_root_widget().delete() 199 raise RuntimeError( 200 f'main_window_replace() called on a not-in-control window' 201 f' ({self}); always check main_window_has_control() before' 202 f' calling main_window_replace().' 203 ) 204 205 # Just shove the old out the left to give the feel that we're 206 # adding to the nav stack. 207 transition = 'out_left' 208 209 # Transition ourself out. 210 try: 211 self.on_main_window_close() 212 except Exception: 213 logging.exception('Error in on_main_window_close() for %s.', self) 214 215 _bauiv1.containerwidget(edit=self._root_widget, transition=transition) 216 babase.app.ui_v1.set_main_window( 217 new_window, 218 from_window=self, 219 back_state=back_state, 220 is_auxiliary=is_auxiliary, 221 suppress_warning=True, 222 )
Replace ourself with a new MainWindow.
224 def on_main_window_close(self) -> None: 225 """Called before transitioning out a main window. 226 227 A good opportunity to save window state/etc. 228 """
Called before transitioning out a main window.
A good opportunity to save window state/etc.
235class MainWindowState: 236 """Persistent state for a specific MainWindow. 237 238 This allows MainWindows to be automatically recreated for back-button 239 purposes, when switching app-modes, etc. 240 """ 241 242 def __init__(self) -> None: 243 # The window that back/cancel navigation should take us to. 244 self.parent: MainWindowState | None = None 245 self.is_top_level: bool | None = None 246 self.is_auxiliary: bool | None = None 247 self.window_type: type[MainWindow] | None = None 248 self.selection: str | None = None 249 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 ) -> MainWindow: 255 """Create a window based on this state. 256 257 WindowState child classes should override this to recreate their 258 particular type of window. 259 """ 260 raise NotImplementedError()
Persistent state for a specific MainWindow.
This allows MainWindows to be automatically recreated for back-button purposes, when switching app-modes, etc.
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 ) -> MainWindow: 255 """Create a window based on this state. 256 257 WindowState child classes should override this to recreate their 258 particular type of window. 259 """ 260 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
1321def open_url(address: str, force_fallback: bool = False) -> None: 1322 """Open the provided URL. 1323 1324 Category: **General Utility Functions** 1325 1326 Attempts to open the provided url in a web-browser. If that is not 1327 possible (or force_fallback is True), instead displays the url as 1328 a string and/or qrcode. 1329 """ 1330 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.
1333def overlay_web_browser_close() -> bool: 1334 """Close any open overlay web browser. 1335 1336 Category: **General Utility Functions** 1337 """ 1338 return bool()
Close any open overlay web browser.
Category: General Utility Functions
1341def overlay_web_browser_is_open() -> bool: 1342 """Return whether an overlay web browser is open currently. 1343 1344 Category: **General Utility Functions** 1345 """ 1346 return bool()
Return whether an overlay web browser is open currently.
Category: General Utility Functions
1349def overlay_web_browser_is_supported() -> bool: 1350 """Return whether an overlay web browser is supported here. 1351 1352 Category: **General Utility Functions** 1353 1354 An overlay web browser is a small dialog that pops up over the top 1355 of the main engine window. It can be used for performing simple 1356 tasks such as sign-ins. 1357 """ 1358 return 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.
1361def overlay_web_browser_open_url(address: str) -> None: 1362 """Open the provided URL in an overlayw web browser. 1363 1364 Category: **General Utility Functions** 1365 1366 An overlay web browser is a small dialog that pops up over the top 1367 of the main engine window. It can be used for performing simple 1368 tasks such as sign-ins. 1369 """ 1370 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.
1405def pushcall( 1406 call: Callable, 1407 from_other_thread: bool = False, 1408 suppress_other_thread_warning: bool = False, 1409 other_thread_use_fg_context: bool = False, 1410 raw: bool = False, 1411) -> None: 1412 """Push a call to the logic event-loop. 1413 Category: **General Utility Functions** 1414 1415 This call expects to be used in the logic thread, and will automatically 1416 save and restore the babase.Context to behave seamlessly. 1417 1418 If you want to push a call from outside of the logic thread, 1419 however, you can pass 'from_other_thread' as True. In this case 1420 the call will always run in the UI context_ref on the logic thread 1421 or whichever context_ref is in the foreground if 1422 other_thread_use_fg_context is True. 1423 Passing raw=True will disable thread checks and context_ref sets/restores. 1424 """ 1425 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.
1429def quit( 1430 confirm: bool = False, quit_type: babase.QuitType | None = None 1431) -> None: 1432 """Quit the app. 1433 1434 Category: **General Utility Functions** 1435 1436 If 'confirm' is True, a confirm dialog will be presented if conditions 1437 allow; otherwise the quit will still be immediate. 1438 See docs for babase.QuitType for explanations of the optional 1439 'quit_type' arg. 1440 """ 1441 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.
468def rowwidget( 469 edit: bauiv1.Widget | None = None, 470 parent: bauiv1.Widget | None = None, 471 size: Sequence[float] | None = None, 472 position: Sequence[float] | None = None, 473 background: bool | None = None, 474 selected_child: bauiv1.Widget | None = None, 475 visible_child: bauiv1.Widget | None = None, 476 claims_left_right: bool | None = None, 477 selection_loops_to_parent: bool | None = None, 478) -> bauiv1.Widget: 479 """Create or edit a row widget. 480 481 Category: **User Interface Functions** 482 483 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 484 a new one is created and returned. Arguments that are not set to None 485 are applied to the Widget. 486 """ 487 import bauiv1 # pylint: disable=cyclic-import 488 489 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.
1479def safecolor( 1480 color: Sequence[float], target_intensity: float = 0.6 1481) -> tuple[float, ...]: 1482 """Given a color tuple, return a color safe to display as text. 1483 1484 Category: **General Utility Functions** 1485 1486 Accepts tuples of length 3 or 4. This will slightly brighten very 1487 dark colors, etc. 1488 """ 1489 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.
1492def screenmessage( 1493 message: str | babase.Lstr, 1494 color: Sequence[float] | None = None, 1495 log: bool = False, 1496) -> None: 1497 """Print a message to the local client's screen, in a given color. 1498 1499 Category: **General Utility Functions** 1500 1501 Note that this version of the function is purely for local display. 1502 To broadcast screen messages in network play, look for methods such as 1503 broadcastmessage() provided by the scene-version packages. 1504 """ 1505 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.
492def scrollwidget( 493 *, 494 edit: bauiv1.Widget | None = None, 495 parent: bauiv1.Widget | None = None, 496 size: Sequence[float] | None = None, 497 position: Sequence[float] | None = None, 498 background: bool | None = None, 499 selected_child: bauiv1.Widget | None = None, 500 capture_arrows: bool = False, 501 on_select_call: Callable | None = None, 502 center_small_content: bool | None = None, 503 color: Sequence[float] | None = None, 504 highlight: bool | None = None, 505 border_opacity: float | None = None, 506 simple_culling_v: float | None = None, 507 selection_loops_to_parent: bool | None = None, 508 claims_left_right: bool | None = None, 509 claims_up_down: bool | None = None, 510 autoselect: bool | None = None, 511) -> bauiv1.Widget: 512 """Create or edit a scroll widget. 513 514 Category: **User Interface Functions** 515 516 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 517 a new one is created and returned. Arguments that are not set to None 518 are applied to the Widget. 519 """ 520 import bauiv1 # pylint: disable=cyclic-import 521 522 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.
1508def set_analytics_screen(screen: str) -> None: 1509 """Used for analytics to see where in the app players spend their time. 1510 1511 Category: **General Utility Functions** 1512 1513 Generally called when opening a new window or entering some UI. 1514 'screen' should be a string description of an app location 1515 ('Main Menu', etc.) 1516 """ 1517 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.)
59class Sound: 60 """Category: **User Interface Classes**""" 61 62 def play(self) -> None: 63 """Play the sound locally.""" 64 return None 65 66 def stop(self) -> None: 67 """Stop the sound if it is playing.""" 68 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
1657def supports_unicode_display() -> bool: 1658 """Return whether we can display all unicode characters in the gui.""" 1659 return bool()
Return whether we can display all unicode characters in the gui.
Category: User Interface Classes
530def textwidget( 531 *, 532 edit: bauiv1.Widget | None = None, 533 parent: bauiv1.Widget | None = None, 534 size: Sequence[float] | None = None, 535 position: Sequence[float] | None = None, 536 text: str | bauiv1.Lstr | None = None, 537 v_align: str | None = None, 538 h_align: str | None = None, 539 editable: bool | None = None, 540 padding: float | None = None, 541 on_return_press_call: Callable[[], None] | None = None, 542 on_activate_call: Callable[[], None] | None = None, 543 selectable: bool | None = None, 544 query: bauiv1.Widget | None = None, 545 max_chars: int | None = None, 546 color: Sequence[float] | None = None, 547 click_activate: bool | None = None, 548 on_select_call: Callable[[], None] | None = None, 549 always_highlight: bool | None = None, 550 draw_controller: bauiv1.Widget | None = None, 551 scale: float | None = None, 552 corner_scale: float | None = None, 553 description: str | bauiv1.Lstr | None = None, 554 transition_delay: float | None = None, 555 maxwidth: float | None = None, 556 max_height: float | None = None, 557 flatness: float | None = None, 558 shadow: float | None = None, 559 autoselect: bool | None = None, 560 rotate: float | None = None, 561 enabled: bool | None = None, 562 force_internal_editing: bool | None = None, 563 always_show_carat: bool | None = None, 564 big: bool | None = None, 565 extra_touch_border_scale: float | None = None, 566 res_scale: float | None = None, 567 query_max_chars: bauiv1.Widget | None = None, 568 query_description: bauiv1.Widget | None = None, 569 adapter_finished: bool | None = None, 570 glow_type: str | None = None, 571 allow_clear_button: bool | None = None, 572) -> bauiv1.Widget: 573 """Create or edit a text widget. 574 575 Category: **User Interface Functions** 576 577 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 578 a new one is created and returned. Arguments that are not set to None 579 are applied to the Widget. 580 """ 581 import bauiv1 # pylint: disable=cyclic-import 582 583 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.
297def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None: 298 """Checks to ensure a widget-owning object gets cleaned up properly. 299 300 Category: User Interface Functions 301 302 This adds a check which will print an error message if the provided 303 object still exists ~5 seconds after the provided bauiv1.Widget dies. 304 305 This is a good sanity check for any sort of object that wraps or 306 controls a bauiv1.Widget. For instance, a 'Window' class instance has 307 no reason to still exist once its root container bauiv1.Widget has fully 308 transitioned out and been destroyed. Circular references or careless 309 strong referencing can lead to such objects never getting destroyed, 310 however, and this helps detect such cases to avoid memory leaks. 311 """ 312 if DEBUG_UI_CLEANUP_CHECKS: 313 print(f'adding uicleanup to {obj}') 314 if not isinstance(widget, _bauiv1.Widget): 315 raise TypeError('widget arg is not a bauiv1.Widget') 316 317 if bool(False): 318 319 def foobar() -> None: 320 """Just testing.""" 321 if DEBUG_UI_CLEANUP_CHECKS: 322 print('uicleanupcheck widget dying...') 323 324 widget.add_delete_callback(foobar) 325 326 assert babase.app.classic is not None 327 babase.app.ui_v1.cleanupchecks.append( 328 UICleanupCheck( 329 obj=weakref.ref(obj), widget=widget, widget_death_time=None 330 ) 331 )
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_0 = 'chest_slot_0' 57 CHEST_SLOT_1 = 'chest_slot_1' 58 CHEST_SLOT_2 = 'chest_slot_2' 59 CHEST_SLOT_3 = 'chest_slot_3' 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 get_main_window(self) -> bauiv1.MainWindow | None: 167 """Return main window, if any.""" 168 return self._main_window() 169 170 def set_main_window( 171 self, 172 window: bauiv1.MainWindow, 173 *, 174 from_window: bauiv1.MainWindow | None | bool = True, 175 is_back: bool = False, 176 is_top_level: bool = False, 177 is_auxiliary: bool = False, 178 back_state: MainWindowState | None = None, 179 suppress_warning: bool = False, 180 ) -> None: 181 """Set the current 'main' window. 182 183 Generally this should not be called directly; The high level 184 MainWindow methods main_window_replace() and main_window_back() 185 should be used whenever possible to implement navigation. 186 187 The caller is responsible for cleaning up any previous main 188 window. 189 """ 190 # pylint: disable=too-many-locals 191 # pylint: disable=too-many-branches 192 # pylint: disable=too-many-statements 193 from bauiv1._uitypes import MainWindow 194 195 # Encourage migration to the new higher level nav calls. 196 if not suppress_warning: 197 warnings.warn( 198 'set_main_window() should usually not be called directly;' 199 ' use the main_window_replace() or main_window_back()' 200 ' methods on MainWindow objects for navigation instead.' 201 ' If you truly need to use set_main_window(),' 202 ' pass suppress_warning=True to silence this warning.', 203 DeprecationWarning, 204 stacklevel=2, 205 ) 206 207 # We used to accept Widgets but now want MainWindows. 208 if not isinstance(window, MainWindow): 209 raise RuntimeError( 210 f'set_main_window() now takes a MainWindow as its "window" arg.' 211 f' You passed a {type(window)}.', 212 ) 213 window_weakref = weakref.ref(window) 214 window_widget = window.get_root_widget() 215 216 if not isinstance(from_window, MainWindow): 217 if from_window is not None and not isinstance(from_window, bool): 218 raise RuntimeError( 219 f'set_main_window() now takes a MainWindow or bool or None' 220 f'as its "from_window" arg.' 221 f' You passed a {type(from_window)}.', 222 ) 223 224 existing = self._main_window() 225 226 # If they passed a back-state, make sure it is fully filled out. 227 if back_state is not None: 228 if ( 229 back_state.is_top_level is None 230 or back_state.is_auxiliary is None 231 or back_state.window_type is None 232 ): 233 raise RuntimeError( 234 'Provided back_state is incomplete.' 235 ' Make sure to only pass fully-filled-out MainWindowStates.' 236 ) 237 238 # If a top-level main-window is being set, complain if there already 239 # is a main-window. 240 if is_top_level: 241 if existing: 242 logging.warning( 243 'set_main_window() called with top-level window %s' 244 ' but found existing main-window %s.', 245 window, 246 existing, 247 ) 248 else: 249 # In other cases, sanity-check that the window asking for 250 # this switch is the one we're switching away from. 251 try: 252 if isinstance(from_window, bool): 253 # For default val True we warn that the arg wasn't 254 # passed. False can be explicitly passed to disable 255 # this check. 256 if from_window is True: 257 caller_frame = inspect.stack()[1] 258 caller_filename = caller_frame.filename 259 caller_line_number = caller_frame.lineno 260 logging.warning( 261 'set_main_window() should be passed a' 262 " 'from_window' value to help ensure proper" 263 ' UI behavior (%s line %i).', 264 caller_filename, 265 caller_line_number, 266 ) 267 else: 268 # For everything else, warn if what they passed 269 # wasn't the previous main menu widget. 270 if from_window is not existing: 271 caller_frame = inspect.stack()[1] 272 caller_filename = caller_frame.filename 273 caller_line_number = caller_frame.lineno 274 logging.warning( 275 "set_main_window() was passed 'from_window' %s" 276 ' but existing main-menu-window is %s.' 277 ' (%s line %i).', 278 from_window, 279 existing, 280 caller_filename, 281 caller_line_number, 282 ) 283 except Exception: 284 # Prevent any bugs in these checks from causing problems. 285 logging.exception('Error checking from_window') 286 287 if is_back: 288 # These values should only be passed for forward navigation. 289 assert not is_top_level 290 assert not is_auxiliary 291 # Make sure back state is complete. 292 assert back_state is not None 293 assert back_state.is_top_level is not None 294 assert back_state.is_auxiliary is not None 295 assert back_state.window_type is type(window) 296 window.main_window_back_state = back_state.parent 297 window.main_window_is_top_level = back_state.is_top_level 298 window.main_window_is_auxiliary = back_state.is_auxiliary 299 else: 300 # Store if the window is top-level so we won't complain later if 301 # we go back from it and there's nowhere to go to. 302 window.main_window_is_top_level = is_top_level 303 304 window.main_window_is_auxiliary = is_auxiliary 305 306 # When navigating forward, generate a back-window-state from 307 # the outgoing window. 308 if is_top_level: 309 # Top level windows don't have or expect anywhere to 310 # go back to. 311 window.main_window_back_state = None 312 elif back_state is not None: 313 window.main_window_back_state = back_state 314 else: 315 oldwin = self._main_window() 316 if oldwin is None: 317 # We currenty only hold weak refs to windows so that 318 # they are free to die on their own, but we expect 319 # the main menu window to keep itself alive as long 320 # as its the main one. Holler if that seems to not 321 # be happening. 322 logging.warning( 323 'set_main_window: No old MainWindow found' 324 ' and is_top_level is False;' 325 ' this should not happen.' 326 ) 327 window.main_window_back_state = None 328 else: 329 window.main_window_back_state = self.save_main_window_state( 330 oldwin 331 ) 332 333 self._main_window = window_weakref 334 self._main_window_widget = window_widget 335 336 def has_main_window(self) -> bool: 337 """Return whether a main menu window is present.""" 338 return bool(self._main_window_widget) 339 340 def clear_main_window(self, transition: str | None = None) -> None: 341 """Clear any existing main window.""" 342 from bauiv1._uitypes import MainWindow 343 344 main_window = self._main_window() 345 if main_window: 346 main_window.main_window_close(transition=transition) 347 else: 348 # Fallback; if we have a widget but no window, nuke the widget. 349 if self._main_window_widget: 350 logging.error( 351 'Have _main_window_widget but no main_window' 352 ' on clear_main_window; unexpected.' 353 ) 354 self._main_window_widget.delete() 355 356 self._main_window = empty_weakref(MainWindow) 357 self._main_window_widget = None 358 359 def save_main_window_state(self, window: MainWindow) -> MainWindowState: 360 """Fully initialize a window-state from a window. 361 362 Use this to get a complete state for later restoration purposes. 363 Calling the window's get_main_window_state() directly is 364 insufficient. 365 """ 366 winstate = window.get_main_window_state() 367 368 # Store some common window stuff on its state. 369 winstate.parent = window.main_window_back_state 370 winstate.is_top_level = window.main_window_is_top_level 371 winstate.is_auxiliary = window.main_window_is_auxiliary 372 winstate.window_type = type(window) 373 374 return winstate 375 376 def restore_main_window_state(self, state: MainWindowState) -> None: 377 """Restore UI to a saved state.""" 378 existing = self.get_main_window() 379 if existing is not None: 380 raise RuntimeError('There is already a MainWindow.') 381 382 # Valid states should have a value here. 383 assert state.is_top_level is not None 384 assert state.is_auxiliary is not None 385 assert state.window_type is not None 386 387 win = state.create_window(transition=None) 388 self.set_main_window( 389 win, 390 from_window=False, # disable check 391 is_top_level=state.is_top_level, 392 is_auxiliary=state.is_auxiliary, 393 back_state=state.parent, 394 suppress_warning=True, 395 ) 396 397 @override 398 def on_screen_change(self) -> None: 399 # Update our stored UIScale. 400 self._update_ui_scale() 401 402 # Update native bits (allow root widget to rebuild itself/etc.) 403 _bauiv1.on_screen_change() 404 405 # Lastly, if we have a main window, recreate it to pick up the 406 # new UIScale/etc. 407 mainwindow = self.get_main_window() 408 if mainwindow is not None: 409 winstate = self.save_main_window_state(mainwindow) 410 self.clear_main_window(transition='instant') 411 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 get_main_window(self) -> bauiv1.MainWindow | None: 167 """Return main window, if any.""" 168 return self._main_window()
Return main window, if any.
170 def set_main_window( 171 self, 172 window: bauiv1.MainWindow, 173 *, 174 from_window: bauiv1.MainWindow | None | bool = True, 175 is_back: bool = False, 176 is_top_level: bool = False, 177 is_auxiliary: bool = False, 178 back_state: MainWindowState | None = None, 179 suppress_warning: bool = False, 180 ) -> None: 181 """Set the current 'main' window. 182 183 Generally this should not be called directly; The high level 184 MainWindow methods main_window_replace() and main_window_back() 185 should be used whenever possible to implement navigation. 186 187 The caller is responsible for cleaning up any previous main 188 window. 189 """ 190 # pylint: disable=too-many-locals 191 # pylint: disable=too-many-branches 192 # pylint: disable=too-many-statements 193 from bauiv1._uitypes import MainWindow 194 195 # Encourage migration to the new higher level nav calls. 196 if not suppress_warning: 197 warnings.warn( 198 'set_main_window() should usually not be called directly;' 199 ' use the main_window_replace() or main_window_back()' 200 ' methods on MainWindow objects for navigation instead.' 201 ' If you truly need to use set_main_window(),' 202 ' pass suppress_warning=True to silence this warning.', 203 DeprecationWarning, 204 stacklevel=2, 205 ) 206 207 # We used to accept Widgets but now want MainWindows. 208 if not isinstance(window, MainWindow): 209 raise RuntimeError( 210 f'set_main_window() now takes a MainWindow as its "window" arg.' 211 f' You passed a {type(window)}.', 212 ) 213 window_weakref = weakref.ref(window) 214 window_widget = window.get_root_widget() 215 216 if not isinstance(from_window, MainWindow): 217 if from_window is not None and not isinstance(from_window, bool): 218 raise RuntimeError( 219 f'set_main_window() now takes a MainWindow or bool or None' 220 f'as its "from_window" arg.' 221 f' You passed a {type(from_window)}.', 222 ) 223 224 existing = self._main_window() 225 226 # If they passed a back-state, make sure it is fully filled out. 227 if back_state is not None: 228 if ( 229 back_state.is_top_level is None 230 or back_state.is_auxiliary is None 231 or back_state.window_type is None 232 ): 233 raise RuntimeError( 234 'Provided back_state is incomplete.' 235 ' Make sure to only pass fully-filled-out MainWindowStates.' 236 ) 237 238 # If a top-level main-window is being set, complain if there already 239 # is a main-window. 240 if is_top_level: 241 if existing: 242 logging.warning( 243 'set_main_window() called with top-level window %s' 244 ' but found existing main-window %s.', 245 window, 246 existing, 247 ) 248 else: 249 # In other cases, sanity-check that the window asking for 250 # this switch is the one we're switching away from. 251 try: 252 if isinstance(from_window, bool): 253 # For default val True we warn that the arg wasn't 254 # passed. False can be explicitly passed to disable 255 # this check. 256 if from_window is True: 257 caller_frame = inspect.stack()[1] 258 caller_filename = caller_frame.filename 259 caller_line_number = caller_frame.lineno 260 logging.warning( 261 'set_main_window() should be passed a' 262 " 'from_window' value to help ensure proper" 263 ' UI behavior (%s line %i).', 264 caller_filename, 265 caller_line_number, 266 ) 267 else: 268 # For everything else, warn if what they passed 269 # wasn't the previous main menu widget. 270 if from_window is not existing: 271 caller_frame = inspect.stack()[1] 272 caller_filename = caller_frame.filename 273 caller_line_number = caller_frame.lineno 274 logging.warning( 275 "set_main_window() was passed 'from_window' %s" 276 ' but existing main-menu-window is %s.' 277 ' (%s line %i).', 278 from_window, 279 existing, 280 caller_filename, 281 caller_line_number, 282 ) 283 except Exception: 284 # Prevent any bugs in these checks from causing problems. 285 logging.exception('Error checking from_window') 286 287 if is_back: 288 # These values should only be passed for forward navigation. 289 assert not is_top_level 290 assert not is_auxiliary 291 # Make sure back state is complete. 292 assert back_state is not None 293 assert back_state.is_top_level is not None 294 assert back_state.is_auxiliary is not None 295 assert back_state.window_type is type(window) 296 window.main_window_back_state = back_state.parent 297 window.main_window_is_top_level = back_state.is_top_level 298 window.main_window_is_auxiliary = back_state.is_auxiliary 299 else: 300 # Store if the window is top-level so we won't complain later if 301 # we go back from it and there's nowhere to go to. 302 window.main_window_is_top_level = is_top_level 303 304 window.main_window_is_auxiliary = is_auxiliary 305 306 # When navigating forward, generate a back-window-state from 307 # the outgoing window. 308 if is_top_level: 309 # Top level windows don't have or expect anywhere to 310 # go back to. 311 window.main_window_back_state = None 312 elif back_state is not None: 313 window.main_window_back_state = back_state 314 else: 315 oldwin = self._main_window() 316 if oldwin is None: 317 # We currenty only hold weak refs to windows so that 318 # they are free to die on their own, but we expect 319 # the main menu window to keep itself alive as long 320 # as its the main one. Holler if that seems to not 321 # be happening. 322 logging.warning( 323 'set_main_window: No old MainWindow found' 324 ' and is_top_level is False;' 325 ' this should not happen.' 326 ) 327 window.main_window_back_state = None 328 else: 329 window.main_window_back_state = self.save_main_window_state( 330 oldwin 331 ) 332 333 self._main_window = window_weakref 334 self._main_window_widget = window_widget
Set the current 'main' window.
Generally this should not be called directly; The high level MainWindow methods main_window_replace() and main_window_back() should be used whenever possible to implement navigation.
The caller is responsible for cleaning up any previous main window.
336 def has_main_window(self) -> bool: 337 """Return whether a main menu window is present.""" 338 return bool(self._main_window_widget)
Return whether a main menu window is present.
340 def clear_main_window(self, transition: str | None = None) -> None: 341 """Clear any existing main window.""" 342 from bauiv1._uitypes import MainWindow 343 344 main_window = self._main_window() 345 if main_window: 346 main_window.main_window_close(transition=transition) 347 else: 348 # Fallback; if we have a widget but no window, nuke the widget. 349 if self._main_window_widget: 350 logging.error( 351 'Have _main_window_widget but no main_window' 352 ' on clear_main_window; unexpected.' 353 ) 354 self._main_window_widget.delete() 355 356 self._main_window = empty_weakref(MainWindow) 357 self._main_window_widget = None
Clear any existing main window.
359 def save_main_window_state(self, window: MainWindow) -> MainWindowState: 360 """Fully initialize a window-state from a window. 361 362 Use this to get a complete state for later restoration purposes. 363 Calling the window's get_main_window_state() directly is 364 insufficient. 365 """ 366 winstate = window.get_main_window_state() 367 368 # Store some common window stuff on its state. 369 winstate.parent = window.main_window_back_state 370 winstate.is_top_level = window.main_window_is_top_level 371 winstate.is_auxiliary = window.main_window_is_auxiliary 372 winstate.window_type = type(window) 373 374 return winstate
Fully initialize a window-state from a window.
Use this to get a complete state for later restoration purposes. Calling the window's get_main_window_state() directly is insufficient.
376 def restore_main_window_state(self, state: MainWindowState) -> None: 377 """Restore UI to a saved state.""" 378 existing = self.get_main_window() 379 if existing is not None: 380 raise RuntimeError('There is already a MainWindow.') 381 382 # Valid states should have a value here. 383 assert state.is_top_level is not None 384 assert state.is_auxiliary is not None 385 assert state.window_type is not None 386 387 win = state.create_window(transition=None) 388 self.set_main_window( 389 win, 390 from_window=False, # disable check 391 is_top_level=state.is_top_level, 392 is_auxiliary=state.is_auxiliary, 393 back_state=state.parent, 394 suppress_warning=True, 395 )
Restore UI to a saved state.
397 @override 398 def on_screen_change(self) -> None: 399 # Update our stored UIScale. 400 self._update_ui_scale() 401 402 # Update native bits (allow root widget to rebuild itself/etc.) 403 _bauiv1.on_screen_change() 404 405 # Lastly, if we have a main window, recreate it to pick up the 406 # new UIScale/etc. 407 mainwindow = self.get_main_window() 408 if mainwindow is not None: 409 winstate = self.save_main_window_state(mainwindow) 410 self.clear_main_window(transition='instant') 411 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_0 = 'chest_slot_0' 57 CHEST_SLOT_1 = 'chest_slot_1' 58 CHEST_SLOT_2 = 'chest_slot_2' 59 CHEST_SLOT_3 = 'chest_slot_3'
Stuff provided by the root ui.
29def utc_now_cloud() -> datetime.datetime: 30 """Returns estimated utc time regardless of local clock settings. 31 32 Applies offsets pulled from server communication/etc. 33 """ 34 # FIXME - do something smart here. 35 return utc_now()
Returns estimated utc time regardless of local clock settings.
Applies offsets pulled from server communication/etc.
597def widget( 598 *, 599 edit: bauiv1.Widget, 600 up_widget: bauiv1.Widget | None = None, 601 down_widget: bauiv1.Widget | None = None, 602 left_widget: bauiv1.Widget | None = None, 603 right_widget: bauiv1.Widget | None = None, 604 show_buffer_top: float | None = None, 605 show_buffer_bottom: float | None = None, 606 show_buffer_left: float | None = None, 607 show_buffer_right: float | None = None, 608 depth_range: tuple[float, float] | None = None, 609 autoselect: bool | None = None, 610) -> None: 611 """Edit common attributes of any widget. 612 613 Category: **User Interface Functions** 614 615 Unlike other UI calls, this can only be used to edit, not to create. 616 """ 617 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.
77class Widget: 78 """Internal type for low level UI elements; buttons, windows, etc. 79 80 Category: **User Interface Classes** 81 82 This class represents a weak reference to a widget object 83 in the internal C++ layer. Currently, functions such as 84 bauiv1.buttonwidget() must be used to instantiate or edit these. 85 """ 86 87 transitioning_out: bool 88 """Whether this widget is in the process of dying (read only). 89 90 It can be useful to check this on a window's root widget to 91 prevent multiple window actions from firing simultaneously, 92 potentially leaving the UI in a broken state.""" 93 94 def __bool__(self) -> bool: 95 """Support for bool evaluation.""" 96 return bool(True) # Slight obfuscation. 97 98 def activate(self) -> None: 99 """Activates a widget; the same as if it had been clicked.""" 100 return None 101 102 def add_delete_callback(self, call: Callable) -> None: 103 """Add a call to be run immediately after this widget is destroyed.""" 104 return None 105 106 def delete(self, ignore_missing: bool = True) -> None: 107 """Delete the Widget. Ignores already-deleted Widgets if ignore_missing 108 is True; otherwise an Exception is thrown. 109 """ 110 return None 111 112 def exists(self) -> bool: 113 """Returns whether the Widget still exists. 114 Most functionality will fail on a nonexistent widget. 115 116 Note that you can also use the boolean operator for this same 117 functionality, so a statement such as "if mywidget" will do 118 the right thing both for Widget objects and values of None. 119 """ 120 return bool() 121 122 def get_children(self) -> list[bauiv1.Widget]: 123 """Returns any child Widgets of this Widget.""" 124 import bauiv1 125 126 return [bauiv1.Widget()] 127 128 def get_screen_space_center(self) -> tuple[float, float]: 129 """Returns the coords of the bauiv1.Widget center relative to the center 130 of the screen. This can be useful for placing pop-up windows and other 131 special cases. 132 """ 133 return (0.0, 0.0) 134 135 def get_selected_child(self) -> bauiv1.Widget | None: 136 """Returns the selected child Widget or None if nothing is selected.""" 137 import bauiv1 138 139 return bauiv1.Widget() 140 141 def get_widget_type(self) -> str: 142 """Return the internal type of the Widget as a string. Note that this 143 is different from the Python bauiv1.Widget type, which is the same for 144 all widgets. 145 """ 146 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.
98 def activate(self) -> None: 99 """Activates a widget; the same as if it had been clicked.""" 100 return None
Activates a widget; the same as if it had been clicked.
102 def add_delete_callback(self, call: Callable) -> None: 103 """Add a call to be run immediately after this widget is destroyed.""" 104 return None
Add a call to be run immediately after this widget is destroyed.
106 def delete(self, ignore_missing: bool = True) -> None: 107 """Delete the Widget. Ignores already-deleted Widgets if ignore_missing 108 is True; otherwise an Exception is thrown. 109 """ 110 return None
Delete the Widget. Ignores already-deleted Widgets if ignore_missing is True; otherwise an Exception is thrown.
112 def exists(self) -> bool: 113 """Returns whether the Widget still exists. 114 Most functionality will fail on a nonexistent widget. 115 116 Note that you can also use the boolean operator for this same 117 functionality, so a statement such as "if mywidget" will do 118 the right thing both for Widget objects and values of None. 119 """ 120 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.
122 def get_children(self) -> list[bauiv1.Widget]: 123 """Returns any child Widgets of this Widget.""" 124 import bauiv1 125 126 return [bauiv1.Widget()]
Returns any child Widgets of this Widget.
128 def get_screen_space_center(self) -> tuple[float, float]: 129 """Returns the coords of the bauiv1.Widget center relative to the center 130 of the screen. This can be useful for placing pop-up windows and other 131 special cases. 132 """ 133 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.
135 def get_selected_child(self) -> bauiv1.Widget | None: 136 """Returns the selected child Widget or None if nothing is selected.""" 137 import bauiv1 138 139 return bauiv1.Widget()
Returns the selected child Widget or None if nothing is selected.
141 def get_widget_type(self) -> str: 142 """Return the internal type of the Widget as a string. Note that this 143 is different from the Python bauiv1.Widget type, which is the same for 144 all widgets. 145 """ 146 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.