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