bauiv1
Ballistica user interface api version 1
1# Released under the MIT License. See LICENSE for details. 2# 3"""Ballistica user interface api version 1""" 4 5# ba_meta require api 8 6 7# The stuff we expose here at the top level is our 'public' api. 8# It should only be imported by code outside of this package or 9# from 'if TYPE_CHECKING' blocks (which will not exec at runtime). 10# Code within our package should import things directly from their 11# submodules. 12 13from __future__ import annotations 14 15# pylint: disable=redefined-builtin 16 17import logging 18 19from efro.util import set_canonical_module_names 20from babase import ( 21 add_clean_frame_callback, 22 app, 23 AppIntent, 24 AppIntentDefault, 25 AppIntentExec, 26 AppMode, 27 appname, 28 appnameupper, 29 apptime, 30 AppTime, 31 apptimer, 32 AppTimer, 33 Call, 34 fullscreen_control_available, 35 fullscreen_control_get, 36 fullscreen_control_key_shortcut, 37 fullscreen_control_set, 38 charstr, 39 clipboard_is_supported, 40 clipboard_set_text, 41 commit_app_config, 42 ContextRef, 43 displaytime, 44 DisplayTime, 45 displaytimer, 46 DisplayTimer, 47 do_once, 48 fade_screen, 49 get_display_resolution, 50 get_input_idle_time, 51 get_ip_address_type, 52 get_low_level_config_value, 53 get_max_graphics_quality, 54 get_remote_app_name, 55 get_replays_dir, 56 get_string_height, 57 get_string_width, 58 get_type_name, 59 getclass, 60 have_permission, 61 in_logic_thread, 62 increment_analytics_count, 63 is_browser_likely_available, 64 is_xcode_build, 65 lock_all_input, 66 LoginAdapter, 67 LoginInfo, 68 Lstr, 69 native_review_request, 70 native_review_request_supported, 71 NotFoundError, 72 open_file_externally, 73 open_url, 74 overlay_web_browser_close, 75 overlay_web_browser_is_open, 76 overlay_web_browser_is_supported, 77 overlay_web_browser_open_url, 78 Permission, 79 Plugin, 80 PluginSpec, 81 pushcall, 82 quit, 83 QuitType, 84 request_permission, 85 safecolor, 86 screenmessage, 87 set_analytics_screen, 88 set_low_level_config_value, 89 set_ui_input_device, 90 SpecialChar, 91 supports_max_fps, 92 supports_vsync, 93 timestring, 94 UIScale, 95 unlock_all_input, 96 WeakCall, 97 workspaces_in_use, 98) 99 100from _bauiv1 import ( 101 buttonwidget, 102 checkboxwidget, 103 columnwidget, 104 containerwidget, 105 get_qrcode_texture, 106 get_special_widget, 107 getmesh, 108 getsound, 109 gettexture, 110 hscrollwidget, 111 imagewidget, 112 is_party_icon_visible, 113 Mesh, 114 rowwidget, 115 scrollwidget, 116 set_party_icon_always_visible, 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 Window, uicleanupcheck 127from bauiv1._subsystem import UIV1Subsystem 128 129__all__ = [ 130 'add_clean_frame_callback', 131 'app', 132 'AppIntent', 133 'AppIntentDefault', 134 'AppIntentExec', 135 'AppMode', 136 'appname', 137 'appnameupper', 138 'appnameupper', 139 'apptime', 140 'AppTime', 141 'apptimer', 142 'AppTimer', 143 'buttonwidget', 144 'Call', 145 'fullscreen_control_available', 146 'fullscreen_control_get', 147 'fullscreen_control_key_shortcut', 148 'fullscreen_control_set', 149 'charstr', 150 'checkboxwidget', 151 'clipboard_is_supported', 152 'clipboard_set_text', 153 'columnwidget', 154 'commit_app_config', 155 'containerwidget', 156 'ContextRef', 157 'displaytime', 158 'DisplayTime', 159 'displaytimer', 160 'DisplayTimer', 161 'do_once', 162 'fade_screen', 163 'get_display_resolution', 164 'get_input_idle_time', 165 'get_ip_address_type', 166 'get_low_level_config_value', 167 'get_max_graphics_quality', 168 'get_qrcode_texture', 169 'get_remote_app_name', 170 'get_replays_dir', 171 'get_special_widget', 172 'get_string_height', 173 'get_string_width', 174 'get_type_name', 175 'getclass', 176 'getmesh', 177 'getsound', 178 'gettexture', 179 'have_permission', 180 'hscrollwidget', 181 'imagewidget', 182 'in_logic_thread', 183 'increment_analytics_count', 184 'is_browser_likely_available', 185 'is_party_icon_visible', 186 'is_xcode_build', 187 'Keyboard', 188 'lock_all_input', 189 'LoginAdapter', 190 'LoginInfo', 191 'Lstr', 192 'Mesh', 193 'native_review_request', 194 'native_review_request_supported', 195 'NotFoundError', 196 'open_file_externally', 197 'open_url', 198 'overlay_web_browser_close', 199 'overlay_web_browser_is_open', 200 'overlay_web_browser_is_supported', 201 'overlay_web_browser_open_url', 202 'Permission', 203 'Plugin', 204 'PluginSpec', 205 'pushcall', 206 'quit', 207 'QuitType', 208 'request_permission', 209 'rowwidget', 210 'safecolor', 211 'screenmessage', 212 'scrollwidget', 213 'set_analytics_screen', 214 'set_low_level_config_value', 215 'set_party_icon_always_visible', 216 'set_party_window_open', 217 'set_ui_input_device', 218 'Sound', 219 'SpecialChar', 220 'supports_max_fps', 221 'supports_vsync', 222 'Texture', 223 'textwidget', 224 'timestring', 225 'uibounds', 226 'uicleanupcheck', 227 'UIScale', 228 'UIV1Subsystem', 229 'unlock_all_input', 230 'WeakCall', 231 'widget', 232 'Widget', 233 'Window', 234 'workspaces_in_use', 235] 236 237# We want stuff to show up as bauiv1.Foo instead of bauiv1._sub.Foo. 238set_canonical_module_names(globals()) 239 240# Sanity check: we want to keep ballistica's dependencies and 241# bootstrapping order clearly defined; let's check a few particular 242# modules to make sure they never directly or indirectly import us 243# before their own execs complete. 244if __debug__: 245 for _mdl in 'babase', '_babase': 246 if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): 247 logging.warning( 248 '%s was imported before %s finished importing;' 249 ' should not happen.', 250 __name__, 251 _mdl, 252 )
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 # FIXME: 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 # FIXME: 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.
547def apptime() -> babase.AppTime: 548 """Return the current app-time in seconds. 549 550 Category: **General Utility Functions** 551 552 App-time is a monotonic time value; it starts at 0.0 when the app 553 launches and will never jump by large amounts or go backwards, even if 554 the system time changes. Its progression will pause when the app is in 555 a suspended state. 556 557 Note that the AppTime returned here is simply float; it just has a 558 unique type in the type-checker's eyes to help prevent it from being 559 accidentally used with time functionality expecting other time types. 560 """ 561 import babase # pylint: disable=cyclic-import 562 563 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.
566def apptimer(time: float, call: Callable[[], Any]) -> None: 567 """Schedule a callable object to run based on app-time. 568 569 Category: **General Utility Functions** 570 571 This function creates a one-off timer which cannot be canceled or 572 modified once created. If you require the ability to do so, or need 573 a repeating timer, use the babase.AppTimer class instead. 574 575 ##### Arguments 576 ###### time (float) 577 > Length of time in seconds that the timer will wait before firing. 578 579 ###### call (Callable[[], Any]) 580 > A callable Python object. Note that the timer will retain a 581 strong reference to the callable for as long as the timer exists, so you 582 may want to look into concepts such as babase.WeakCall if that is not 583 desired. 584 585 ##### Examples 586 Print some stuff through time: 587 >>> babase.screenmessage('hello from now!') 588 >>> babase.apptimer(1.0, babase.Call(babase.screenmessage, 589 'hello from the future!')) 590 >>> babase.apptimer(2.0, babase.Call(babase.screenmessage, 591 ... 'hello from the future 2!')) 592 """ 593 return None
Schedule a callable object to run based on app-time.
Category: General Utility Functions
This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.AppTimer class instead.
Arguments
time (float)
Length of time in seconds that the timer will wait before firing.
call (Callable[[], Any])
A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
Examples
Print some stuff through time:
>>> babase.screenmessage('hello from now!')
>>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
'hello from the future!'))
>>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
... 'hello from the future 2!'))
53class AppTimer: 54 """Timers are used to run code at later points in time. 55 56 Category: **General Utility Classes** 57 58 This class encapsulates a timer based on app-time. 59 The underlying timer will be destroyed when this object is no longer 60 referenced. If you do not want to worry about keeping a reference to 61 your timer around, use the babase.apptimer() function instead to get a 62 one-off timer. 63 64 ##### Arguments 65 ###### time 66 > Length of time in seconds that the timer will wait before firing. 67 68 ###### call 69 > A callable Python object. Remember that the timer will retain a 70 strong reference to the callable for as long as it exists, so you 71 may want to look into concepts such as babase.WeakCall if that is not 72 desired. 73 74 ###### repeat 75 > If True, the timer will fire repeatedly, with each successive 76 firing having the same delay as the first. 77 78 ##### Example 79 80 Use a Timer object to print repeatedly for a few seconds: 81 ... def say_it(): 82 ... babase.screenmessage('BADGER!') 83 ... def stop_saying_it(): 84 ... global g_timer 85 ... g_timer = None 86 ... babase.screenmessage('MUSHROOM MUSHROOM!') 87 ... # Create our timer; it will run as long as we have the self.t ref. 88 ... g_timer = babase.AppTimer(0.3, say_it, repeat=True) 89 ... # Now fire off a one-shot timer to kill it. 90 ... babase.apptimer(3.89, stop_saying_it) 91 """ 92 93 def __init__( 94 self, time: float, call: Callable[[], Any], repeat: bool = False 95 ) -> None: 96 pass
Timers are used to run code at later points in time.
Category: General Utility Classes
This class encapsulates a timer based on app-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.apptimer() function instead to get a one-off timer.
Arguments
time
Length of time in seconds that the timer will wait before firing.
call
A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
repeat
If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.
Example
Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.AppTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.apptimer(3.89, stop_saying_it)
616def charstr(char_id: babase.SpecialChar) -> str: 617 """Get a unicode string representing a special character. 618 619 Category: **General Utility Functions** 620 621 Note that these utilize the private-use block of unicode characters 622 (U+E000-U+F8FF) and are specific to the game; exporting or rendering 623 them elsewhere will be meaningless. 624 625 See babase.SpecialChar for the list of available characters. 626 """ 627 return str()
Get a unicode string representing a special character.
Category: General Utility Functions
Note that these utilize the private-use block of unicode characters (U+E000-U+F8FF) and are specific to the game; exporting or rendering them elsewhere will be meaningless.
See babase.SpecialChar for the list of available characters.
204def checkboxwidget( 205 edit: bauiv1.Widget | None = None, 206 parent: bauiv1.Widget | None = None, 207 size: Sequence[float] | None = None, 208 position: Sequence[float] | None = None, 209 text: str | bauiv1.Lstr | None = None, 210 value: bool | None = None, 211 on_value_change_call: Callable[[bool], None] | None = None, 212 on_select_call: Callable[[], None] | None = None, 213 text_scale: float | None = None, 214 textcolor: Sequence[float] | None = None, 215 scale: float | None = None, 216 is_radio_button: bool | None = None, 217 maxwidth: float | None = None, 218 autoselect: bool | None = None, 219 color: Sequence[float] | None = None, 220) -> bauiv1.Widget: 221 """Create or edit a check-box widget. 222 223 Category: **User Interface Functions** 224 225 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 226 a new one is created and returned. Arguments that are not set to None 227 are applied to the Widget. 228 """ 229 import bauiv1 # pylint: disable=cyclic-import 230 231 return bauiv1.Widget()
Create or edit a check-box widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
652def clipboard_is_supported() -> bool: 653 """Return whether this platform supports clipboard operations at all. 654 655 Category: **General Utility Functions** 656 657 If this returns False, UIs should not show 'copy to clipboard' 658 buttons, etc. 659 """ 660 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.
663def clipboard_set_text(value: str) -> None: 664 """Copy a string to the system clipboard. 665 666 Category: **General Utility Functions** 667 668 Ensure that babase.clipboard_is_supported() returns True before adding 669 buttons/etc. that make use of this functionality. 670 """ 671 return None
Copy a string to the system clipboard.
Category: General Utility Functions
Ensure that babase.clipboard_is_supported() returns True before adding buttons/etc. that make use of this functionality.
234def columnwidget( 235 edit: bauiv1.Widget | None = None, 236 parent: bauiv1.Widget | None = None, 237 size: Sequence[float] | None = None, 238 position: Sequence[float] | None = None, 239 background: bool | None = None, 240 selected_child: bauiv1.Widget | None = None, 241 visible_child: bauiv1.Widget | None = None, 242 single_depth: bool | None = None, 243 print_list_exit_instructions: bool | None = None, 244 left_border: float | None = None, 245 top_border: float | None = None, 246 bottom_border: float | None = None, 247 selection_loops_to_parent: bool | None = None, 248 border: float | None = None, 249 margin: float | None = None, 250 claims_left_right: bool | None = None, 251 claims_tab: bool | None = None, 252) -> bauiv1.Widget: 253 """Create or edit a column widget. 254 255 Category: **User Interface Functions** 256 257 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 258 a new one is created and returned. Arguments that are not set to None 259 are applied to the Widget. 260 """ 261 import bauiv1 # pylint: disable=cyclic-import 262 263 return bauiv1.Widget()
Create or edit a column widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
266def containerwidget( 267 edit: bauiv1.Widget | None = None, 268 parent: bauiv1.Widget | None = None, 269 size: Sequence[float] | None = None, 270 position: Sequence[float] | None = None, 271 background: bool | None = None, 272 selected_child: bauiv1.Widget | None = None, 273 transition: str | None = None, 274 cancel_button: bauiv1.Widget | None = None, 275 start_button: bauiv1.Widget | None = None, 276 root_selectable: bool | None = None, 277 on_activate_call: Callable[[], None] | None = None, 278 claims_left_right: bool | None = None, 279 claims_tab: bool | None = None, 280 selection_loops: bool | None = None, 281 selection_loops_to_parent: bool | None = None, 282 scale: float | None = None, 283 on_outside_click_call: Callable[[], None] | None = None, 284 single_depth: bool | None = None, 285 visible_child: bauiv1.Widget | None = None, 286 stack_offset: Sequence[float] | None = None, 287 color: Sequence[float] | None = None, 288 on_cancel_call: Callable[[], None] | None = None, 289 print_list_exit_instructions: bool | None = None, 290 click_activate: bool | None = None, 291 always_highlight: bool | None = None, 292 selectable: bool | None = None, 293 scale_origin_stack_offset: Sequence[float] | None = None, 294 toolbar_visibility: str | None = None, 295 on_select_call: Callable[[], None] | None = None, 296 claim_outside_clicks: bool | None = None, 297 claims_up_down: bool | None = None, 298) -> bauiv1.Widget: 299 """Create or edit a container widget. 300 301 Category: **User Interface Functions** 302 303 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 304 a new one is created and returned. Arguments that are not set to None 305 are applied to the Widget. 306 """ 307 import bauiv1 # pylint: disable=cyclic-import 308 309 return bauiv1.Widget()
Create or edit a container widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
148class ContextRef: 149 """Store or use a ballistica context. 150 151 Category: **General Utility Classes** 152 153 Many operations such as bascenev1.newnode() or bascenev1.gettexture() 154 operate implicitly on a current 'context'. A context is some sort of 155 state that functionality can implicitly use. Context determines, for 156 example, which scene nodes or textures get added to without having to 157 specify it explicitly in the newnode()/gettexture() call. Contexts can 158 also affect object lifecycles; for example a babase.ContextCall will 159 become a no-op when the context it was created in is destroyed. 160 161 In general, if you are a modder, you should not need to worry about 162 contexts; mod code should mostly be getting run in the correct 163 context and timers and other callbacks will take care of saving 164 and restoring contexts automatically. There may be rare cases, 165 however, where you need to deal directly with contexts, and that is 166 where this class comes in. 167 168 Creating a babase.ContextRef() will capture a reference to the current 169 context. Other modules may provide ways to access their contexts; for 170 example a bascenev1.Activity instance has a 'context' attribute. You 171 can also use babase.ContextRef.empty() to create a reference to *no* 172 context. Some code such as UI calls may expect this and may complain 173 if you try to use them within a context. 174 175 ##### Usage 176 ContextRefs are generally used with the Python 'with' statement, which 177 sets the context they point to as current on entry and resets it to 178 the previous value on exit. 179 180 ##### Example 181 Explicitly create a few UI bits with no context set. 182 (UI stuff may complain if called within a context): 183 >>> with bui.ContextRef.empty(): 184 ... my_container = bui.containerwidget() 185 """ 186 187 def __init__( 188 self, 189 ) -> None: 190 pass 191 192 def __enter__(self) -> None: 193 """Support for "with" statement.""" 194 pass 195 196 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: 197 """Support for "with" statement.""" 198 pass 199 200 @classmethod 201 def empty(cls) -> ContextRef: 202 """Return a ContextRef pointing to no context. 203 204 This is useful when code should be run free of a context. 205 For example, UI code generally insists on being run this way. 206 Otherwise, callbacks set on the UI could inadvertently stop working 207 due to a game activity ending, which would be unintuitive behavior. 208 """ 209 return ContextRef() 210 211 def is_empty(self) -> bool: 212 """Whether the context was created as empty.""" 213 return bool() 214 215 def is_expired(self) -> bool: 216 """Whether the context has expired.""" 217 return bool()
Store or use a ballistica context.
Category: General Utility Classes
Many operations such as bascenev1.newnode() or bascenev1.gettexture() operate implicitly on a current 'context'. A context is some sort of state that functionality can implicitly use. Context determines, for example, which scene nodes or textures get added to without having to specify it explicitly in the newnode()/gettexture() call. Contexts can also affect object lifecycles; for example a babase.ContextCall will become a no-op when the context it was created in is destroyed.
In general, if you are a modder, you should not need to worry about contexts; mod code should mostly be getting run in the correct context and timers and other callbacks will take care of saving and restoring contexts automatically. There may be rare cases, however, where you need to deal directly with contexts, and that is where this class comes in.
Creating a babase.ContextRef() will capture a reference to the current context. Other modules may provide ways to access their contexts; for example a bascenev1.Activity instance has a 'context' attribute. You can also use babase.ContextRef.empty() to create a reference to no context. Some code such as UI calls may expect this and may complain if you try to use them within a context.
Usage
ContextRefs are generally used with the Python 'with' statement, which sets the context they point to as current on entry and resets it to the previous value on exit.
Example
Explicitly create a few UI bits with no context set. (UI stuff may complain if called within a context):
>>> with bui.ContextRef.empty():
... my_container = bui.containerwidget()
200 @classmethod 201 def empty(cls) -> ContextRef: 202 """Return a ContextRef pointing to no context. 203 204 This is useful when code should be run free of a context. 205 For example, UI code generally insists on being run this way. 206 Otherwise, callbacks set on the UI could inadvertently stop working 207 due to a game activity ending, which would be unintuitive behavior. 208 """ 209 return ContextRef()
Return a ContextRef pointing to no context.
This is useful when code should be run free of a context. For example, UI code generally insists on being run this way. Otherwise, callbacks set on the UI could inadvertently stop working due to a game activity ending, which would be unintuitive behavior.
756def displaytime() -> babase.DisplayTime: 757 """Return the current display-time in seconds. 758 759 Category: **General Utility Functions** 760 761 Display-time is a time value intended to be used for animation and other 762 visual purposes. It will generally increment by a consistent amount each 763 frame. It will pass at an overall similar rate to AppTime, but trades 764 accuracy for smoothness. 765 766 Note that the value returned here is simply a float; it just has a 767 unique type in the type-checker's eyes to help prevent it from being 768 accidentally used with time functionality expecting other time types. 769 """ 770 import babase # pylint: disable=cyclic-import 771 772 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.
775def displaytimer(time: float, call: Callable[[], Any]) -> None: 776 """Schedule a callable object to run based on display-time. 777 778 Category: **General Utility Functions** 779 780 This function creates a one-off timer which cannot be canceled or 781 modified once created. If you require the ability to do so, or need 782 a repeating timer, use the babase.DisplayTimer class instead. 783 784 Display-time is a time value intended to be used for animation and other 785 visual purposes. It will generally increment by a consistent amount each 786 frame. It will pass at an overall similar rate to AppTime, but trades 787 accuracy for smoothness. 788 789 ##### Arguments 790 ###### time (float) 791 > Length of time in seconds that the timer will wait before firing. 792 793 ###### call (Callable[[], Any]) 794 > A callable Python object. Note that the timer will retain a 795 strong reference to the callable for as long as the timer exists, so you 796 may want to look into concepts such as babase.WeakCall if that is not 797 desired. 798 799 ##### Examples 800 Print some stuff through time: 801 >>> babase.screenmessage('hello from now!') 802 >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage, 803 ... 'hello from the future!')) 804 >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage, 805 ... 'hello from the future 2!')) 806 """ 807 return None
Schedule a callable object to run based on display-time.
Category: General Utility Functions
This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.DisplayTimer class instead.
Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.
Arguments
time (float)
Length of time in seconds that the timer will wait before firing.
call (Callable[[], Any])
A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
Examples
Print some stuff through time:
>>> babase.screenmessage('hello from now!')
>>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
... 'hello from the future!'))
>>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
... 'hello from the future 2!'))
220class DisplayTimer: 221 """Timers are used to run code at later points in time. 222 223 Category: **General Utility Classes** 224 225 This class encapsulates a timer based on display-time. 226 The underlying timer will be destroyed when this object is no longer 227 referenced. If you do not want to worry about keeping a reference to 228 your timer around, use the babase.displaytimer() function instead to get a 229 one-off timer. 230 231 Display-time is a time value intended to be used for animation and 232 other visual purposes. It will generally increment by a consistent 233 amount each frame. It will pass at an overall similar rate to AppTime, 234 but trades accuracy for smoothness. 235 236 ##### Arguments 237 ###### time 238 > Length of time in seconds that the timer will wait before firing. 239 240 ###### call 241 > A callable Python object. Remember that the timer will retain a 242 strong reference to the callable for as long as it exists, so you 243 may want to look into concepts such as babase.WeakCall if that is not 244 desired. 245 246 ###### repeat 247 > If True, the timer will fire repeatedly, with each successive 248 firing having the same delay as the first. 249 250 ##### Example 251 252 Use a Timer object to print repeatedly for a few seconds: 253 ... def say_it(): 254 ... babase.screenmessage('BADGER!') 255 ... def stop_saying_it(): 256 ... global g_timer 257 ... g_timer = None 258 ... babase.screenmessage('MUSHROOM MUSHROOM!') 259 ... # Create our timer; it will run as long as we have the self.t ref. 260 ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) 261 ... # Now fire off a one-shot timer to kill it. 262 ... babase.displaytimer(3.89, stop_saying_it) 263 """ 264 265 def __init__( 266 self, time: float, call: Callable[[], Any], repeat: bool = False 267 ) -> None: 268 pass
Timers are used to run code at later points in time.
Category: General Utility Classes
This class encapsulates a timer based on display-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.displaytimer() function instead to get a one-off timer.
Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.
Arguments
time
Length of time in seconds that the timer will wait before firing.
call
A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
repeat
If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.
Example
Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.displaytimer(3.89, stop_saying_it)
815def do_once() -> bool: 816 """Return whether this is the first time running a line of code. 817 818 Category: **General Utility Functions** 819 820 This is used by 'print_once()' type calls to keep from overflowing 821 logs. The call functions by registering the filename and line where 822 The call is made from. Returns True if this location has not been 823 registered already, and False if it has. 824 825 ##### Example 826 This print will only fire for the first loop iteration: 827 >>> for i in range(10): 828 ... if babase.do_once(): 829 ... print('HelloWorld once from loop!') 830 """ 831 return bool()
Return whether this is the first time running a line of code.
Category: General Utility Functions
This is used by 'print_once()' type calls to keep from overflowing logs. The call functions by registering the filename and line where The call is made from. Returns True if this location has not been registered already, and False if it has.
Example
This print will only fire for the first loop iteration:
>>> for i in range(10):
... if babase.do_once():
... print('HelloWorld once from loop!')
980def get_input_idle_time() -> float: 981 """Return seconds since any local input occurred (touch, keypress, etc.).""" 982 return float()
Return seconds since any local input occurred (touch, keypress, etc.).
56def get_ip_address_type(addr: str) -> socket.AddressFamily: 57 """Return socket.AF_INET6 or socket.AF_INET4 for the provided address.""" 58 import socket 59 60 socket_type = None 61 62 # First try it as an ipv4 address. 63 try: 64 socket.inet_pton(socket.AF_INET, addr) 65 socket_type = socket.AF_INET 66 except OSError: 67 pass 68 69 # Hmm apparently not ipv4; try ipv6. 70 if socket_type is None: 71 try: 72 socket.inet_pton(socket.AF_INET6, addr) 73 socket_type = socket.AF_INET6 74 except OSError: 75 pass 76 if socket_type is None: 77 raise ValueError(f'addr seems to be neither v4 or v6: {addr}') 78 return socket_type
Return socket.AF_INET6 or socket.AF_INET4 for the provided address.
312def get_qrcode_texture(url: str) -> bauiv1.Texture: 313 """Return a QR code texture. 314 315 The provided url must be 64 bytes or less. 316 """ 317 import bauiv1 # pylint: disable=cyclic-import 318 319 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.
329def getmesh(name: str) -> bauiv1.Mesh: 330 """Load a mesh for use solely in the local user interface.""" 331 import bauiv1 # pylint: disable=cyclic-import 332 333 return bauiv1.Mesh()
Load a mesh for use solely in the local user interface.
336def getsound(name: str) -> bauiv1.Sound: 337 """Load a sound for use in the ui.""" 338 import bauiv1 # pylint: disable=cyclic-import 339 340 return bauiv1.Sound()
Load a sound for use in the ui.
343def gettexture(name: str) -> bauiv1.Texture: 344 """Load a texture for use in the ui.""" 345 import bauiv1 # pylint: disable=cyclic-import 346 347 return bauiv1.Texture()
Load a texture for use in the ui.
350def hscrollwidget( 351 edit: bauiv1.Widget | None = None, 352 parent: bauiv1.Widget | None = None, 353 size: Sequence[float] | None = None, 354 position: Sequence[float] | None = None, 355 background: bool | None = None, 356 selected_child: bauiv1.Widget | None = None, 357 capture_arrows: bool | None = None, 358 on_select_call: Callable[[], None] | None = None, 359 center_small_content: bool | None = None, 360 color: Sequence[float] | None = None, 361 highlight: bool | None = None, 362 border_opacity: float | None = None, 363 simple_culling_h: float | None = None, 364 claims_left_right: bool | None = None, 365 claims_up_down: bool | None = None, 366 claims_tab: bool | None = None, 367) -> bauiv1.Widget: 368 """Create or edit a horizontal scroll widget. 369 370 Category: **User Interface Functions** 371 372 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 373 a new one is created and returned. Arguments that are not set to None 374 are applied to the Widget. 375 """ 376 import bauiv1 # pylint: disable=cyclic-import 377 378 return bauiv1.Widget()
Create or edit a horizontal scroll widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
381def imagewidget( 382 edit: bauiv1.Widget | None = None, 383 parent: bauiv1.Widget | None = None, 384 size: Sequence[float] | None = None, 385 position: Sequence[float] | None = None, 386 color: Sequence[float] | None = None, 387 texture: bauiv1.Texture | None = None, 388 opacity: float | None = None, 389 mesh_transparent: bauiv1.Mesh | None = None, 390 mesh_opaque: bauiv1.Mesh | None = None, 391 has_alpha_channel: bool = True, 392 tint_texture: bauiv1.Texture | None = None, 393 tint_color: Sequence[float] | None = None, 394 transition_delay: float | None = None, 395 draw_controller: bauiv1.Widget | None = None, 396 tint2_color: Sequence[float] | None = None, 397 tilt_scale: float | None = None, 398 mask_texture: bauiv1.Texture | None = None, 399 radial_amount: float | None = None, 400 draw_controller_mult: float | None = None, 401) -> bauiv1.Widget: 402 """Create or edit an image widget. 403 404 Category: **User Interface Functions** 405 406 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 407 a new one is created and returned. Arguments that are not set to None 408 are applied to the Widget. 409 """ 410 import bauiv1 # pylint: disable=cyclic-import 411 412 return bauiv1.Widget()
Create or edit an image widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
27def is_browser_likely_available() -> bool: 28 """Return whether a browser likely exists on the current device. 29 30 category: General Utility Functions 31 32 If this returns False you may want to avoid calling babase.show_url() 33 with any lengthy addresses. (ba.show_url() will display an address 34 as a string in a window if unable to bring up a browser, but that 35 is only useful for simple URLs.) 36 """ 37 app = _babase.app 38 39 if app.classic is None: 40 logging.warning( 41 'is_browser_likely_available() needs to be updated' 42 ' to work without classic.' 43 ) 44 return True 45 46 platform = app.classic.platform 47 hastouchscreen = _babase.hastouchscreen() 48 49 # If we're on a vr device or an android device with no touchscreen, 50 # assume no browser. 51 # FIXME: Might not be the case anymore; should make this definable 52 # at the platform level. 53 if app.env.vr or (platform == 'android' and not hastouchscreen): 54 return False 55 56 # Anywhere else assume we've got one. 57 return True
Return whether a browser likely exists on the current device.
category: General Utility Functions
If this returns False you may want to avoid calling babase.show_url() with any lengthy addresses. (ba.show_url() will display an address as a string in a window if unable to bring up a browser, but that is only useful for simple URLs.)
14class Keyboard: 15 """Chars definitions for on-screen keyboard. 16 17 Category: **App Classes** 18 19 Keyboards are discoverable by the meta-tag system 20 and the user can select which one they want to use. 21 On-screen keyboard uses chars from active babase.Keyboard. 22 """ 23 24 name: str 25 """Displays when user selecting this keyboard.""" 26 27 chars: list[tuple[str, ...]] 28 """Used for row/column lengths.""" 29 30 pages: dict[str, tuple[str, ...]] 31 """Extra chars like emojis.""" 32 33 nums: tuple[str, ...] 34 """The 'num' page."""
Chars definitions for on-screen keyboard.
Category: App Classes
Keyboards are discoverable by the meta-tag system and the user can select which one they want to use. On-screen keyboard uses chars from active babase.Keyboard.
32class LoginAdapter: 33 """Allows using implicit login types in an explicit way. 34 35 Some login types such as Google Play Game Services or Game Center are 36 basically always present and often do not provide a way to log out 37 from within a running app, so this adapter exists to use them in a 38 flexible manner by 'attaching' and 'detaching' from an always-present 39 login, allowing for its use alongside other login types. It also 40 provides common functionality for server-side account verification and 41 other handy bits. 42 """ 43 44 @dataclass 45 class SignInResult: 46 """Describes the final result of a sign-in attempt.""" 47 48 credentials: str 49 50 @dataclass 51 class ImplicitLoginState: 52 """Describes the current state of an implicit login.""" 53 54 login_id: str 55 display_name: str 56 57 def __init__(self, login_type: LoginType): 58 assert _babase.in_logic_thread() 59 self.login_type = login_type 60 self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = ( 61 None 62 ) 63 self._on_app_loading_called = False 64 self._implicit_login_state_dirty = False 65 self._back_end_active = False 66 67 # Which login of our type (if any) is associated with the 68 # current active primary account. 69 self._active_login_id: str | None = None 70 71 self._last_sign_in_time: float | None = None 72 self._last_sign_in_desc: str | None = None 73 74 def on_app_loading(self) -> None: 75 """Should be called for each adapter in on_app_loading.""" 76 77 assert not self._on_app_loading_called 78 self._on_app_loading_called = True 79 80 # Any implicit state we received up until now needs to be pushed 81 # to the app account subsystem. 82 self._update_implicit_login_state() 83 84 def set_implicit_login_state( 85 self, state: ImplicitLoginState | None 86 ) -> None: 87 """Keep the adapter informed of implicit login states. 88 89 This should be called by the adapter back-end when an account 90 of their associated type gets logged in or out. 91 """ 92 assert _babase.in_logic_thread() 93 94 # Ignore redundant sets. 95 if state == self._implicit_login_state: 96 return 97 98 if DEBUG_LOG: 99 if state is None: 100 logging.debug( 101 'LoginAdapter: %s implicit state changed;' 102 ' now signed out.', 103 self.login_type.name, 104 ) 105 else: 106 logging.debug( 107 'LoginAdapter: %s implicit state changed;' 108 ' now signed in as %s.', 109 self.login_type.name, 110 state.display_name, 111 ) 112 113 self._implicit_login_state = state 114 self._implicit_login_state_dirty = True 115 116 # (possibly) push it to the app for handling. 117 self._update_implicit_login_state() 118 119 # This might affect whether we consider that back-end as 'active'. 120 self._update_back_end_active() 121 122 def set_active_logins(self, logins: dict[LoginType, str]) -> None: 123 """Keep the adapter informed of actively used logins. 124 125 This should be called by the app's account subsystem to 126 keep adapters up to date on the full set of logins attached 127 to the currently-in-use account. 128 Note that the logins dict passed in should be immutable as 129 only a reference to it is stored, not a copy. 130 """ 131 assert _babase.in_logic_thread() 132 if DEBUG_LOG: 133 logging.debug( 134 'LoginAdapter: %s adapter got active logins %s.', 135 self.login_type.name, 136 {k: v[:4] + '...' + v[-4:] for k, v in logins.items()}, 137 ) 138 139 self._active_login_id = logins.get(self.login_type) 140 self._update_back_end_active() 141 142 def on_back_end_active_change(self, active: bool) -> None: 143 """Called when active state for the back-end is (possibly) changing. 144 145 Meant to be overridden by subclasses. 146 Being active means that the implicit login provided by the back-end 147 is actually being used by the app. It should therefore register 148 unlocked achievements, leaderboard scores, allow viewing native 149 UIs, etc. When not active it should ignore everything and behave 150 as if signed out, even if it technically is still signed in. 151 """ 152 assert _babase.in_logic_thread() 153 del active # Unused. 154 155 @final 156 def sign_in( 157 self, 158 result_cb: Callable[[LoginAdapter, SignInResult | Exception], None], 159 description: str, 160 ) -> None: 161 """Attempt to sign in via this adapter. 162 163 This can be called even if the back-end is not implicitly signed in; 164 the adapter will attempt to sign in if possible. An exception will 165 be returned if the sign-in attempt fails. 166 """ 167 168 assert _babase.in_logic_thread() 169 170 # Have been seeing multiple sign-in attempts come through 171 # nearly simultaneously which can be problematic server-side. 172 # Let's error if a sign-in attempt is made within a few seconds 173 # of the last one to try and address this. 174 now = time.monotonic() 175 appnow = _babase.apptime() 176 if self._last_sign_in_time is not None: 177 since_last = now - self._last_sign_in_time 178 if since_last < 1.0: 179 logging.warning( 180 'LoginAdapter: %s adapter sign_in() called too soon' 181 ' (%.2fs) after last; this-desc="%s", last-desc="%s",' 182 ' ba-app-time=%.2f.', 183 self.login_type.name, 184 since_last, 185 description, 186 self._last_sign_in_desc, 187 appnow, 188 ) 189 _babase.pushcall( 190 partial( 191 result_cb, 192 self, 193 RuntimeError('sign_in called too soon after last.'), 194 ) 195 ) 196 return 197 198 self._last_sign_in_desc = description 199 self._last_sign_in_time = now 200 201 if DEBUG_LOG: 202 logging.debug( 203 'LoginAdapter: %s adapter sign_in() called;' 204 ' fetching sign-in-token...', 205 self.login_type.name, 206 ) 207 208 def _got_sign_in_token_result(result: str | None) -> None: 209 import bacommon.cloud 210 211 # Failed to get a sign-in-token. 212 if result is None: 213 if DEBUG_LOG: 214 logging.debug( 215 'LoginAdapter: %s adapter sign-in-token fetch failed;' 216 ' aborting sign-in.', 217 self.login_type.name, 218 ) 219 _babase.pushcall( 220 partial( 221 result_cb, 222 self, 223 RuntimeError('fetch-sign-in-token failed.'), 224 ) 225 ) 226 return 227 228 # Got a sign-in token! Now pass it to the cloud which will use 229 # it to verify our identity and give us app credentials on 230 # success. 231 if DEBUG_LOG: 232 logging.debug( 233 'LoginAdapter: %s adapter sign-in-token fetch succeeded;' 234 ' passing to cloud for verification...', 235 self.login_type.name, 236 ) 237 238 def _got_sign_in_response( 239 response: bacommon.cloud.SignInResponse | Exception, 240 ) -> None: 241 # This likely means we couldn't communicate with the server. 242 if isinstance(response, Exception): 243 if DEBUG_LOG: 244 logging.debug( 245 'LoginAdapter: %s adapter got error' 246 ' sign-in response: %s', 247 self.login_type.name, 248 response, 249 ) 250 _babase.pushcall(partial(result_cb, self, response)) 251 else: 252 # This means our credentials were explicitly rejected. 253 if response.credentials is None: 254 result2: LoginAdapter.SignInResult | Exception = ( 255 RuntimeError('Sign-in-token was rejected.') 256 ) 257 else: 258 if DEBUG_LOG: 259 logging.debug( 260 'LoginAdapter: %s adapter got successful' 261 ' sign-in response', 262 self.login_type.name, 263 ) 264 result2 = self.SignInResult( 265 credentials=response.credentials 266 ) 267 _babase.pushcall(partial(result_cb, self, result2)) 268 269 assert _babase.app.plus is not None 270 _babase.app.plus.cloud.send_message_cb( 271 bacommon.cloud.SignInMessage( 272 self.login_type, 273 result, 274 description=description, 275 apptime=appnow, 276 ), 277 on_response=_got_sign_in_response, 278 ) 279 280 # Kick off the sign-in process by fetching a sign-in token. 281 self.get_sign_in_token(completion_cb=_got_sign_in_token_result) 282 283 def is_back_end_active(self) -> bool: 284 """Is this adapter's back-end currently active?""" 285 return self._back_end_active 286 287 def get_sign_in_token( 288 self, completion_cb: Callable[[str | None], None] 289 ) -> None: 290 """Get a sign-in token from the adapter back end. 291 292 This token is then passed to the master-server to complete the 293 sign-in process. The adapter can use this opportunity to bring 294 up account creation UI, call its internal sign_in function, etc. 295 as needed. The provided completion_cb should then be called with 296 either a token or None if sign in failed or was cancelled. 297 """ 298 299 # Default implementation simply fails immediately. 300 _babase.pushcall(partial(completion_cb, None)) 301 302 def _update_implicit_login_state(self) -> None: 303 # If we've received an implicit login state, schedule it to be 304 # sent along to the app. We wait until on-app-loading has been 305 # called so that account-client-v2 has had a chance to load 306 # any existing state so it can properly respond to this. 307 if self._implicit_login_state_dirty and self._on_app_loading_called: 308 309 if DEBUG_LOG: 310 logging.debug( 311 'LoginAdapter: %s adapter sending' 312 ' implicit-state-changed to app.', 313 self.login_type.name, 314 ) 315 316 assert _babase.app.plus is not None 317 _babase.pushcall( 318 partial( 319 _babase.app.plus.accounts.on_implicit_login_state_changed, 320 self.login_type, 321 self._implicit_login_state, 322 ) 323 ) 324 self._implicit_login_state_dirty = False 325 326 def _update_back_end_active(self) -> None: 327 was_active = self._back_end_active 328 if self._implicit_login_state is None: 329 is_active = False 330 else: 331 is_active = ( 332 self._implicit_login_state.login_id == self._active_login_id 333 ) 334 if was_active != is_active: 335 if DEBUG_LOG: 336 logging.debug( 337 'LoginAdapter: %s adapter back-end-active is now %s.', 338 self.login_type.name, 339 is_active, 340 ) 341 self.on_back_end_active_change(is_active) 342 self._back_end_active = is_active
Allows using implicit login types in an explicit way.
Some login types such as Google Play Game Services or Game Center are basically always present and often do not provide a way to log out from within a running app, so this adapter exists to use them in a flexible manner by 'attaching' and 'detaching' from an always-present login, allowing for its use alongside other login types. It also provides common functionality for server-side account verification and other handy bits.
57 def __init__(self, login_type: LoginType): 58 assert _babase.in_logic_thread() 59 self.login_type = login_type 60 self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = ( 61 None 62 ) 63 self._on_app_loading_called = False 64 self._implicit_login_state_dirty = False 65 self._back_end_active = False 66 67 # Which login of our type (if any) is associated with the 68 # current active primary account. 69 self._active_login_id: str | None = None 70 71 self._last_sign_in_time: float | None = None 72 self._last_sign_in_desc: str | None = None
74 def on_app_loading(self) -> None: 75 """Should be called for each adapter in on_app_loading.""" 76 77 assert not self._on_app_loading_called 78 self._on_app_loading_called = True 79 80 # Any implicit state we received up until now needs to be pushed 81 # to the app account subsystem. 82 self._update_implicit_login_state()
Should be called for each adapter in on_app_loading.
84 def set_implicit_login_state( 85 self, state: ImplicitLoginState | None 86 ) -> None: 87 """Keep the adapter informed of implicit login states. 88 89 This should be called by the adapter back-end when an account 90 of their associated type gets logged in or out. 91 """ 92 assert _babase.in_logic_thread() 93 94 # Ignore redundant sets. 95 if state == self._implicit_login_state: 96 return 97 98 if DEBUG_LOG: 99 if state is None: 100 logging.debug( 101 'LoginAdapter: %s implicit state changed;' 102 ' now signed out.', 103 self.login_type.name, 104 ) 105 else: 106 logging.debug( 107 'LoginAdapter: %s implicit state changed;' 108 ' now signed in as %s.', 109 self.login_type.name, 110 state.display_name, 111 ) 112 113 self._implicit_login_state = state 114 self._implicit_login_state_dirty = True 115 116 # (possibly) push it to the app for handling. 117 self._update_implicit_login_state() 118 119 # This might affect whether we consider that back-end as 'active'. 120 self._update_back_end_active()
Keep the adapter informed of implicit login states.
This should be called by the adapter back-end when an account of their associated type gets logged in or out.
122 def set_active_logins(self, logins: dict[LoginType, str]) -> None: 123 """Keep the adapter informed of actively used logins. 124 125 This should be called by the app's account subsystem to 126 keep adapters up to date on the full set of logins attached 127 to the currently-in-use account. 128 Note that the logins dict passed in should be immutable as 129 only a reference to it is stored, not a copy. 130 """ 131 assert _babase.in_logic_thread() 132 if DEBUG_LOG: 133 logging.debug( 134 'LoginAdapter: %s adapter got active logins %s.', 135 self.login_type.name, 136 {k: v[:4] + '...' + v[-4:] for k, v in logins.items()}, 137 ) 138 139 self._active_login_id = logins.get(self.login_type) 140 self._update_back_end_active()
Keep the adapter informed of actively used logins.
This should be called by the app's account subsystem to keep adapters up to date on the full set of logins attached to the currently-in-use account. Note that the logins dict passed in should be immutable as only a reference to it is stored, not a copy.
142 def on_back_end_active_change(self, active: bool) -> None: 143 """Called when active state for the back-end is (possibly) changing. 144 145 Meant to be overridden by subclasses. 146 Being active means that the implicit login provided by the back-end 147 is actually being used by the app. It should therefore register 148 unlocked achievements, leaderboard scores, allow viewing native 149 UIs, etc. When not active it should ignore everything and behave 150 as if signed out, even if it technically is still signed in. 151 """ 152 assert _babase.in_logic_thread() 153 del active # Unused.
Called when active state for the back-end is (possibly) changing.
Meant to be overridden by subclasses. Being active means that the implicit login provided by the back-end is actually being used by the app. It should therefore register unlocked achievements, leaderboard scores, allow viewing native UIs, etc. When not active it should ignore everything and behave as if signed out, even if it technically is still signed in.
155 @final 156 def sign_in( 157 self, 158 result_cb: Callable[[LoginAdapter, SignInResult | Exception], None], 159 description: str, 160 ) -> None: 161 """Attempt to sign in via this adapter. 162 163 This can be called even if the back-end is not implicitly signed in; 164 the adapter will attempt to sign in if possible. An exception will 165 be returned if the sign-in attempt fails. 166 """ 167 168 assert _babase.in_logic_thread() 169 170 # Have been seeing multiple sign-in attempts come through 171 # nearly simultaneously which can be problematic server-side. 172 # Let's error if a sign-in attempt is made within a few seconds 173 # of the last one to try and address this. 174 now = time.monotonic() 175 appnow = _babase.apptime() 176 if self._last_sign_in_time is not None: 177 since_last = now - self._last_sign_in_time 178 if since_last < 1.0: 179 logging.warning( 180 'LoginAdapter: %s adapter sign_in() called too soon' 181 ' (%.2fs) after last; this-desc="%s", last-desc="%s",' 182 ' ba-app-time=%.2f.', 183 self.login_type.name, 184 since_last, 185 description, 186 self._last_sign_in_desc, 187 appnow, 188 ) 189 _babase.pushcall( 190 partial( 191 result_cb, 192 self, 193 RuntimeError('sign_in called too soon after last.'), 194 ) 195 ) 196 return 197 198 self._last_sign_in_desc = description 199 self._last_sign_in_time = now 200 201 if DEBUG_LOG: 202 logging.debug( 203 'LoginAdapter: %s adapter sign_in() called;' 204 ' fetching sign-in-token...', 205 self.login_type.name, 206 ) 207 208 def _got_sign_in_token_result(result: str | None) -> None: 209 import bacommon.cloud 210 211 # Failed to get a sign-in-token. 212 if result is None: 213 if DEBUG_LOG: 214 logging.debug( 215 'LoginAdapter: %s adapter sign-in-token fetch failed;' 216 ' aborting sign-in.', 217 self.login_type.name, 218 ) 219 _babase.pushcall( 220 partial( 221 result_cb, 222 self, 223 RuntimeError('fetch-sign-in-token failed.'), 224 ) 225 ) 226 return 227 228 # Got a sign-in token! Now pass it to the cloud which will use 229 # it to verify our identity and give us app credentials on 230 # success. 231 if DEBUG_LOG: 232 logging.debug( 233 'LoginAdapter: %s adapter sign-in-token fetch succeeded;' 234 ' passing to cloud for verification...', 235 self.login_type.name, 236 ) 237 238 def _got_sign_in_response( 239 response: bacommon.cloud.SignInResponse | Exception, 240 ) -> None: 241 # This likely means we couldn't communicate with the server. 242 if isinstance(response, Exception): 243 if DEBUG_LOG: 244 logging.debug( 245 'LoginAdapter: %s adapter got error' 246 ' sign-in response: %s', 247 self.login_type.name, 248 response, 249 ) 250 _babase.pushcall(partial(result_cb, self, response)) 251 else: 252 # This means our credentials were explicitly rejected. 253 if response.credentials is None: 254 result2: LoginAdapter.SignInResult | Exception = ( 255 RuntimeError('Sign-in-token was rejected.') 256 ) 257 else: 258 if DEBUG_LOG: 259 logging.debug( 260 'LoginAdapter: %s adapter got successful' 261 ' sign-in response', 262 self.login_type.name, 263 ) 264 result2 = self.SignInResult( 265 credentials=response.credentials 266 ) 267 _babase.pushcall(partial(result_cb, self, result2)) 268 269 assert _babase.app.plus is not None 270 _babase.app.plus.cloud.send_message_cb( 271 bacommon.cloud.SignInMessage( 272 self.login_type, 273 result, 274 description=description, 275 apptime=appnow, 276 ), 277 on_response=_got_sign_in_response, 278 ) 279 280 # Kick off the sign-in process by fetching a sign-in token. 281 self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
Attempt to sign in via this adapter.
This can be called even if the back-end is not implicitly signed in; the adapter will attempt to sign in if possible. An exception will be returned if the sign-in attempt fails.
283 def is_back_end_active(self) -> bool: 284 """Is this adapter's back-end currently active?""" 285 return self._back_end_active
Is this adapter's back-end currently active?
287 def get_sign_in_token( 288 self, completion_cb: Callable[[str | None], None] 289 ) -> None: 290 """Get a sign-in token from the adapter back end. 291 292 This token is then passed to the master-server to complete the 293 sign-in process. The adapter can use this opportunity to bring 294 up account creation UI, call its internal sign_in function, etc. 295 as needed. The provided completion_cb should then be called with 296 either a token or None if sign in failed or was cancelled. 297 """ 298 299 # Default implementation simply fails immediately. 300 _babase.pushcall(partial(completion_cb, None))
Get a sign-in token from the adapter back end.
This token is then passed to the master-server to complete the sign-in process. The adapter can use this opportunity to bring up account creation UI, call its internal sign_in function, etc. as needed. The provided completion_cb should then be called with either a token or None if sign in failed or was cancelled.
44 @dataclass 45 class SignInResult: 46 """Describes the final result of a sign-in attempt.""" 47 48 credentials: str
Describes the final result of a sign-in attempt.
50 @dataclass 51 class ImplicitLoginState: 52 """Describes the current state of an implicit login.""" 53 54 login_id: str 55 display_name: str
Describes the current state of an implicit login.
25@dataclass 26class LoginInfo: 27 """Basic info about a login available in the app.plus.accounts section.""" 28 29 name: str
Basic info about a login available in the app.plus.accounts section.
440class Lstr: 441 """Used to define strings in a language-independent way. 442 443 Category: **General Utility Classes** 444 445 These should be used whenever possible in place of hard-coded 446 strings so that in-game or UI elements show up correctly on all 447 clients in their currently-active language. 448 449 To see available resource keys, look at any of the bs_language_*.py 450 files in the game or the translations pages at 451 legacy.ballistica.net/translate. 452 453 ##### Examples 454 EXAMPLE 1: specify a string from a resource path 455 >>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText') 456 457 EXAMPLE 2: specify a translated string via a category and english 458 value; if a translated value is available, it will be used; otherwise 459 the english value will be. To see available translation categories, 460 look under the 'translations' resource section. 461 >>> mynode.text = babase.Lstr(translate=('gameDescriptions', 462 ... 'Defeat all enemies')) 463 464 EXAMPLE 3: specify a raw value and some substitutions. Substitutions 465 can be used with resource and translate modes as well. 466 >>> mynode.text = babase.Lstr(value='${A} / ${B}', 467 ... subs=[('${A}', str(score)), ('${B}', str(total))]) 468 469 EXAMPLE 4: babase.Lstr's can be nested. This example would display the 470 resource at res_a but replace ${NAME} with the value of the 471 resource at res_b 472 >>> mytextnode.text = babase.Lstr( 473 ... resource='res_a', 474 ... subs=[('${NAME}', babase.Lstr(resource='res_b'))]) 475 """ 476 477 # pylint: disable=dangerous-default-value 478 # noinspection PyDefaultArgument 479 @overload 480 def __init__( 481 self, 482 *, 483 resource: str, 484 fallback_resource: str = '', 485 fallback_value: str = '', 486 subs: Sequence[tuple[str, str | Lstr]] = [], 487 ) -> None: 488 """Create an Lstr from a string resource.""" 489 490 # noinspection PyShadowingNames,PyDefaultArgument 491 @overload 492 def __init__( 493 self, 494 *, 495 translate: tuple[str, str], 496 subs: Sequence[tuple[str, str | Lstr]] = [], 497 ) -> None: 498 """Create an Lstr by translating a string in a category.""" 499 500 # noinspection PyDefaultArgument 501 @overload 502 def __init__( 503 self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = [] 504 ) -> None: 505 """Create an Lstr from a raw string value.""" 506 507 # pylint: enable=redefined-outer-name, dangerous-default-value 508 509 def __init__(self, *args: Any, **keywds: Any) -> None: 510 """Instantiate a Lstr. 511 512 Pass a value for either 'resource', 'translate', 513 or 'value'. (see Lstr help for examples). 514 'subs' can be a sequence of 2-member sequences consisting of values 515 and replacements. 516 'fallback_resource' can be a resource key that will be used if the 517 main one is not present for 518 the current language in place of falling back to the english value 519 ('resource' mode only). 520 'fallback_value' can be a literal string that will be used if neither 521 the resource nor the fallback resource is found ('resource' mode only). 522 """ 523 # pylint: disable=too-many-branches 524 if args: 525 raise TypeError('Lstr accepts only keyword arguments') 526 527 # Basically just store the exact args they passed. 528 # However if they passed any Lstr values for subs, 529 # replace them with that Lstr's dict. 530 self.args = keywds 531 our_type = type(self) 532 533 if isinstance(self.args.get('value'), our_type): 534 raise TypeError("'value' must be a regular string; not an Lstr") 535 536 if 'subs' in self.args: 537 subs_new = [] 538 for key, value in keywds['subs']: 539 if isinstance(value, our_type): 540 subs_new.append((key, value.args)) 541 else: 542 subs_new.append((key, value)) 543 self.args['subs'] = subs_new 544 545 # As of protocol 31 we support compact key names 546 # ('t' instead of 'translate', etc). Convert as needed. 547 if 'translate' in keywds: 548 keywds['t'] = keywds['translate'] 549 del keywds['translate'] 550 if 'resource' in keywds: 551 keywds['r'] = keywds['resource'] 552 del keywds['resource'] 553 if 'value' in keywds: 554 keywds['v'] = keywds['value'] 555 del keywds['value'] 556 if 'fallback' in keywds: 557 from babase import _error 558 559 _error.print_error( 560 'deprecated "fallback" arg passed to Lstr(); use ' 561 'either "fallback_resource" or "fallback_value"', 562 once=True, 563 ) 564 keywds['f'] = keywds['fallback'] 565 del keywds['fallback'] 566 if 'fallback_resource' in keywds: 567 keywds['f'] = keywds['fallback_resource'] 568 del keywds['fallback_resource'] 569 if 'subs' in keywds: 570 keywds['s'] = keywds['subs'] 571 del keywds['subs'] 572 if 'fallback_value' in keywds: 573 keywds['fv'] = keywds['fallback_value'] 574 del keywds['fallback_value'] 575 576 def evaluate(self) -> str: 577 """Evaluate the Lstr and returns a flat string in the current language. 578 579 You should avoid doing this as much as possible and instead pass 580 and store Lstr values. 581 """ 582 return _babase.evaluate_lstr(self._get_json()) 583 584 def is_flat_value(self) -> bool: 585 """Return whether the Lstr is a 'flat' value. 586 587 This is defined as a simple string value incorporating no 588 translations, resources, or substitutions. In this case it may 589 be reasonable to replace it with a raw string value, perform 590 string manipulation on it, etc. 591 """ 592 return bool('v' in self.args and not self.args.get('s', [])) 593 594 def _get_json(self) -> str: 595 try: 596 return json.dumps(self.args, separators=(',', ':')) 597 except Exception: 598 from babase import _error 599 600 _error.print_exception('_get_json failed for', self.args) 601 return 'JSON_ERR' 602 603 @override 604 def __str__(self) -> str: 605 return '<ba.Lstr: ' + self._get_json() + '>' 606 607 @override 608 def __repr__(self) -> str: 609 return '<ba.Lstr: ' + self._get_json() + '>' 610 611 @staticmethod 612 def from_json(json_string: str) -> babase.Lstr: 613 """Given a json string, returns a babase.Lstr. Does no validation.""" 614 lstr = Lstr(value='') 615 lstr.args = json.loads(json_string) 616 return lstr
Used to define strings in a language-independent way.
Category: General Utility Classes
These should be used whenever possible in place of hard-coded strings so that in-game or UI elements show up correctly on all clients in their currently-active language.
To see available resource keys, look at any of the bs_language_*.py files in the game or the translations pages at legacy.ballistica.net/translate.
Examples
EXAMPLE 1: specify a string from a resource path
>>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')
EXAMPLE 2: specify a translated string via a category and english value; if a translated value is available, it will be used; otherwise the english value will be. To see available translation categories, look under the 'translations' resource section.
>>> mynode.text = babase.Lstr(translate=('gameDescriptions',
... 'Defeat all enemies'))
EXAMPLE 3: specify a raw value and some substitutions. Substitutions can be used with resource and translate modes as well.
>>> mynode.text = babase.Lstr(value='${A} / ${B}',
... subs=[('${A}', str(score)), ('${B}', str(total))])
EXAMPLE 4: babase.Lstr's can be nested. This example would display the resource at res_a but replace ${NAME} with the value of the resource at res_b
>>> mytextnode.text = babase.Lstr(
... resource='res_a',
... subs=[('${NAME}', babase.Lstr(resource='res_b'))])
509 def __init__(self, *args: Any, **keywds: Any) -> None: 510 """Instantiate a Lstr. 511 512 Pass a value for either 'resource', 'translate', 513 or 'value'. (see Lstr help for examples). 514 'subs' can be a sequence of 2-member sequences consisting of values 515 and replacements. 516 'fallback_resource' can be a resource key that will be used if the 517 main one is not present for 518 the current language in place of falling back to the english value 519 ('resource' mode only). 520 'fallback_value' can be a literal string that will be used if neither 521 the resource nor the fallback resource is found ('resource' mode only). 522 """ 523 # pylint: disable=too-many-branches 524 if args: 525 raise TypeError('Lstr accepts only keyword arguments') 526 527 # Basically just store the exact args they passed. 528 # However if they passed any Lstr values for subs, 529 # replace them with that Lstr's dict. 530 self.args = keywds 531 our_type = type(self) 532 533 if isinstance(self.args.get('value'), our_type): 534 raise TypeError("'value' must be a regular string; not an Lstr") 535 536 if 'subs' in self.args: 537 subs_new = [] 538 for key, value in keywds['subs']: 539 if isinstance(value, our_type): 540 subs_new.append((key, value.args)) 541 else: 542 subs_new.append((key, value)) 543 self.args['subs'] = subs_new 544 545 # As of protocol 31 we support compact key names 546 # ('t' instead of 'translate', etc). Convert as needed. 547 if 'translate' in keywds: 548 keywds['t'] = keywds['translate'] 549 del keywds['translate'] 550 if 'resource' in keywds: 551 keywds['r'] = keywds['resource'] 552 del keywds['resource'] 553 if 'value' in keywds: 554 keywds['v'] = keywds['value'] 555 del keywds['value'] 556 if 'fallback' in keywds: 557 from babase import _error 558 559 _error.print_error( 560 'deprecated "fallback" arg passed to Lstr(); use ' 561 'either "fallback_resource" or "fallback_value"', 562 once=True, 563 ) 564 keywds['f'] = keywds['fallback'] 565 del keywds['fallback'] 566 if 'fallback_resource' in keywds: 567 keywds['f'] = keywds['fallback_resource'] 568 del keywds['fallback_resource'] 569 if 'subs' in keywds: 570 keywds['s'] = keywds['subs'] 571 del keywds['subs'] 572 if 'fallback_value' in keywds: 573 keywds['fv'] = keywds['fallback_value'] 574 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).
576 def evaluate(self) -> str: 577 """Evaluate the Lstr and returns a flat string in the current language. 578 579 You should avoid doing this as much as possible and instead pass 580 and store Lstr values. 581 """ 582 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.
584 def is_flat_value(self) -> bool: 585 """Return whether the Lstr is a 'flat' value. 586 587 This is defined as a simple string value incorporating no 588 translations, resources, or substitutions. In this case it may 589 be reasonable to replace it with a raw string value, perform 590 string manipulation on it, etc. 591 """ 592 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.
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
1299def open_url(address: str, force_fallback: bool = False) -> None: 1300 """Open the provided URL. 1301 1302 Category: **General Utility Functions** 1303 1304 Attempts to open the provided url in a web-browser. If that is not 1305 possible (or force_fallback is True), instead displays the url as 1306 a string and/or qrcode. 1307 """ 1308 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.
1311def overlay_web_browser_close() -> bool: 1312 """Close any open overlay web browser. 1313 1314 Category: **General Utility Functions** 1315 """ 1316 return bool()
Close any open overlay web browser.
Category: General Utility Functions
1319def overlay_web_browser_is_open() -> bool: 1320 """Return whether an overlay web browser is open currently. 1321 1322 Category: **General Utility Functions** 1323 """ 1324 return bool()
Return whether an overlay web browser is open currently.
Category: General Utility Functions
1327def overlay_web_browser_is_supported() -> bool: 1328 """Return whether an overlay web browser is supported here. 1329 1330 Category: **General Utility Functions** 1331 1332 An overlay web browser is a small dialog that pops up over the top 1333 of the main engine window. It can be used for performing simple 1334 tasks such as sign-ins. 1335 """ 1336 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.
1339def overlay_web_browser_open_url(address: str) -> None: 1340 """Open the provided URL in an overlayw web browser. 1341 1342 Category: **General Utility Functions** 1343 1344 An overlay web browser is a small dialog that pops up over the top 1345 of the main engine window. It can be used for performing simple 1346 tasks such as sign-ins. 1347 """ 1348 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.
1378def pushcall( 1379 call: Callable, 1380 from_other_thread: bool = False, 1381 suppress_other_thread_warning: bool = False, 1382 other_thread_use_fg_context: bool = False, 1383 raw: bool = False, 1384) -> None: 1385 """Push a call to the logic event-loop. 1386 Category: **General Utility Functions** 1387 1388 This call expects to be used in the logic thread, and will automatically 1389 save and restore the babase.Context to behave seamlessly. 1390 1391 If you want to push a call from outside of the logic thread, 1392 however, you can pass 'from_other_thread' as True. In this case 1393 the call will always run in the UI context_ref on the logic thread 1394 or whichever context_ref is in the foreground if 1395 other_thread_use_fg_context is True. 1396 Passing raw=True will disable thread checks and context_ref sets/restores. 1397 """ 1398 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.
1402def quit( 1403 confirm: bool = False, quit_type: babase.QuitType | None = None 1404) -> None: 1405 """Quit the app. 1406 1407 Category: **General Utility Functions** 1408 1409 If 'confirm' is True, a confirm dialog will be presented if conditions 1410 allow; otherwise the quit will still be immediate. 1411 See docs for babase.QuitType for explanations of the optional 1412 'quit_type' arg. 1413 """ 1414 return None
Quit the app.
Category: General Utility Functions
If 'confirm' is True, a confirm dialog will be presented if conditions allow; otherwise the quit will still be immediate. See docs for babase.QuitType for explanations of the optional 'quit_type' arg.
42class QuitType(Enum): 43 """Types of input a controller can send to the game. 44 45 Category: Enums 46 47 'soft' may hide/reset the app but keep the process running, depending 48 on the platform. 49 50 'back' is a variant of 'soft' which may give 'back-button-pressed' 51 behavior depending on the platform. (returning to some previous 52 activity instead of dumping to the home screen, etc.) 53 54 'hard' leads to the process exiting. This generally should be avoided 55 on platforms such as mobile. 56 """ 57 58 SOFT = 0 59 BACK = 1 60 HARD = 2
Types of input a controller can send to the game.
Category: Enums
'soft' may hide/reset the app but keep the process running, depending on the platform.
'back' is a variant of 'soft' which may give 'back-button-pressed' behavior depending on the platform. (returning to some previous activity instead of dumping to the home screen, etc.)
'hard' leads to the process exiting. This generally should be avoided on platforms such as mobile.
Inherited Members
- enum.Enum
- name
- value
425def rowwidget( 426 edit: bauiv1.Widget | None = None, 427 parent: bauiv1.Widget | None = None, 428 size: Sequence[float] | None = None, 429 position: Sequence[float] | None = None, 430 background: bool | None = None, 431 selected_child: bauiv1.Widget | None = None, 432 visible_child: bauiv1.Widget | None = None, 433 claims_left_right: bool | None = None, 434 claims_tab: bool | None = None, 435 selection_loops_to_parent: bool | None = None, 436) -> bauiv1.Widget: 437 """Create or edit a row widget. 438 439 Category: **User Interface Functions** 440 441 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 442 a new one is created and returned. Arguments that are not set to None 443 are applied to the Widget. 444 """ 445 import bauiv1 # pylint: disable=cyclic-import 446 447 return bauiv1.Widget()
Create or edit a row widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
1452def safecolor( 1453 color: Sequence[float], target_intensity: float = 0.6 1454) -> tuple[float, ...]: 1455 """Given a color tuple, return a color safe to display as text. 1456 1457 Category: **General Utility Functions** 1458 1459 Accepts tuples of length 3 or 4. This will slightly brighten very 1460 dark colors, etc. 1461 """ 1462 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.
1465def screenmessage( 1466 message: str | babase.Lstr, 1467 color: Sequence[float] | None = None, 1468 log: bool = False, 1469) -> None: 1470 """Print a message to the local client's screen, in a given color. 1471 1472 Category: **General Utility Functions** 1473 1474 Note that this version of the function is purely for local display. 1475 To broadcast screen messages in network play, look for methods such as 1476 broadcastmessage() provided by the scene-version packages. 1477 """ 1478 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.
450def scrollwidget( 451 edit: bauiv1.Widget | None = None, 452 parent: bauiv1.Widget | None = None, 453 size: Sequence[float] | None = None, 454 position: Sequence[float] | None = None, 455 background: bool | None = None, 456 selected_child: bauiv1.Widget | None = None, 457 capture_arrows: bool = False, 458 on_select_call: Callable | None = None, 459 center_small_content: bool | None = None, 460 color: Sequence[float] | None = None, 461 highlight: bool | None = None, 462 border_opacity: float | None = None, 463 simple_culling_v: float | None = None, 464 selection_loops_to_parent: bool | None = None, 465 claims_left_right: bool | None = None, 466 claims_up_down: bool | None = None, 467 claims_tab: bool | None = None, 468 autoselect: bool | None = None, 469) -> bauiv1.Widget: 470 """Create or edit a scroll widget. 471 472 Category: **User Interface Functions** 473 474 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 475 a new one is created and returned. Arguments that are not set to None 476 are applied to the Widget. 477 """ 478 import bauiv1 # pylint: disable=cyclic-import 479 480 return bauiv1.Widget()
Create or edit a scroll widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
1481def set_analytics_screen(screen: str) -> None: 1482 """Used for analytics to see where in the app players spend their time. 1483 1484 Category: **General Utility Functions** 1485 1486 Generally called when opening a new window or entering some UI. 1487 'screen' should be a string description of an app location 1488 ('Main Menu', etc.) 1489 """ 1490 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
493def textwidget( 494 edit: bauiv1.Widget | None = None, 495 parent: bauiv1.Widget | None = None, 496 size: Sequence[float] | None = None, 497 position: Sequence[float] | None = None, 498 text: str | bauiv1.Lstr | None = None, 499 v_align: str | None = None, 500 h_align: str | None = None, 501 editable: bool | None = None, 502 padding: float | None = None, 503 on_return_press_call: Callable[[], None] | None = None, 504 on_activate_call: Callable[[], None] | None = None, 505 selectable: bool | None = None, 506 query: bauiv1.Widget | None = None, 507 max_chars: int | None = None, 508 color: Sequence[float] | None = None, 509 click_activate: bool | None = None, 510 on_select_call: Callable[[], None] | None = None, 511 always_highlight: bool | None = None, 512 draw_controller: bauiv1.Widget | None = None, 513 scale: float | None = None, 514 corner_scale: float | None = None, 515 description: str | bauiv1.Lstr | None = None, 516 transition_delay: float | None = None, 517 maxwidth: float | None = None, 518 max_height: float | None = None, 519 flatness: float | None = None, 520 shadow: float | None = None, 521 autoselect: bool | None = None, 522 rotate: float | None = None, 523 enabled: bool | None = None, 524 force_internal_editing: bool | None = None, 525 always_show_carat: bool | None = None, 526 big: bool | None = None, 527 extra_touch_border_scale: float | None = None, 528 res_scale: float | None = None, 529 query_max_chars: bauiv1.Widget | None = None, 530 query_description: bauiv1.Widget | None = None, 531 adapter_finished: bool | None = None, 532 glow_type: str | None = None, 533 allow_clear_button: bool | None = None, 534) -> bauiv1.Widget: 535 """Create or edit a text widget. 536 537 Category: **User Interface Functions** 538 539 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 540 a new one is created and returned. Arguments that are not set to None 541 are applied to the Widget. 542 """ 543 import bauiv1 # pylint: disable=cyclic-import 544 545 return bauiv1.Widget()
Create or edit a text widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
15def timestring( 16 timeval: float | int, 17 centi: bool = True, 18) -> babase.Lstr: 19 """Generate a babase.Lstr for displaying a time value. 20 21 Category: **General Utility Functions** 22 23 Given a time value, returns a babase.Lstr with: 24 (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True). 25 26 WARNING: the underlying Lstr value is somewhat large so don't use this 27 to rapidly update Node text values for an onscreen timer or you may 28 consume significant network bandwidth. For that purpose you should 29 use a 'timedisplay' Node and attribute connections. 30 31 """ 32 from babase._language import Lstr 33 34 # We take float seconds but operate on int milliseconds internally. 35 timeval = int(1000 * timeval) 36 bits = [] 37 subs = [] 38 hval = (timeval // 1000) // (60 * 60) 39 if hval != 0: 40 bits.append('${H}') 41 subs.append( 42 ( 43 '${H}', 44 Lstr( 45 resource='timeSuffixHoursText', 46 subs=[('${COUNT}', str(hval))], 47 ), 48 ) 49 ) 50 mval = ((timeval // 1000) // 60) % 60 51 if mval != 0: 52 bits.append('${M}') 53 subs.append( 54 ( 55 '${M}', 56 Lstr( 57 resource='timeSuffixMinutesText', 58 subs=[('${COUNT}', str(mval))], 59 ), 60 ) 61 ) 62 63 # We add seconds if its non-zero *or* we haven't added anything else. 64 if centi: 65 # pylint: disable=consider-using-f-string 66 sval = timeval / 1000.0 % 60.0 67 if sval >= 0.005 or not bits: 68 bits.append('${S}') 69 subs.append( 70 ( 71 '${S}', 72 Lstr( 73 resource='timeSuffixSecondsText', 74 subs=[('${COUNT}', ('%.2f' % sval))], 75 ), 76 ) 77 ) 78 else: 79 sval = timeval // 1000 % 60 80 if sval != 0 or not bits: 81 bits.append('${S}') 82 subs.append( 83 ( 84 '${S}', 85 Lstr( 86 resource='timeSuffixSecondsText', 87 subs=[('${COUNT}', str(sval))], 88 ), 89 ) 90 ) 91 return Lstr(value=' '.join(bits), subs=subs)
Generate a babase.Lstr for displaying a time value.
Category: General Utility Functions
Given a time value, returns a babase.Lstr with: (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).
WARNING: the underlying Lstr value is somewhat large so don't use this to rapidly update Node text values for an onscreen timer or you may consume significant network bandwidth. For that purpose you should use a 'timedisplay' Node and attribute connections.
173def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None: 174 """Add a check to ensure a widget-owning object gets cleaned up properly. 175 176 Category: User Interface Functions 177 178 This adds a check which will print an error message if the provided 179 object still exists ~5 seconds after the provided bauiv1.Widget dies. 180 181 This is a good sanity check for any sort of object that wraps or 182 controls a bauiv1.Widget. For instance, a 'Window' class instance has 183 no reason to still exist once its root container bauiv1.Widget has fully 184 transitioned out and been destroyed. Circular references or careless 185 strong referencing can lead to such objects never getting destroyed, 186 however, and this helps detect such cases to avoid memory leaks. 187 """ 188 if DEBUG_UI_CLEANUP_CHECKS: 189 print(f'adding uicleanup to {obj}') 190 if not isinstance(widget, _bauiv1.Widget): 191 raise TypeError('widget arg is not a bauiv1.Widget') 192 193 if bool(False): 194 195 def foobar() -> None: 196 """Just testing.""" 197 if DEBUG_UI_CLEANUP_CHECKS: 198 print('uicleanupcheck widget dying...') 199 200 widget.add_delete_callback(foobar) 201 202 assert babase.app.classic is not None 203 babase.app.ui_v1.cleanupchecks.append( 204 UICleanupCheck( 205 obj=weakref.ref(obj), widget=widget, widget_death_time=None 206 ) 207 )
Add a check to ensure a widget-owning object gets cleaned up properly.
Category: User Interface Functions
This adds a check which will print an error message if the provided object still exists ~5 seconds after the provided bauiv1.Widget dies.
This is a good sanity check for any sort of object that wraps or controls a bauiv1.Widget. For instance, a 'Window' class instance has no reason to still exist once its root container bauiv1.Widget has fully transitioned out and been destroyed. Circular references or careless strong referencing can lead to such objects never getting destroyed, however, and this helps detect such cases to avoid memory leaks.
63class UIScale(Enum): 64 """The overall scale the UI is being rendered for. Note that this is 65 independent of pixel resolution. For example, a phone and a desktop PC 66 might render the game at similar pixel resolutions but the size they 67 display content at will vary significantly. 68 69 Category: Enums 70 71 'large' is used for devices such as desktop PCs where fine details can 72 be clearly seen. UI elements are generally smaller on the screen 73 and more content can be seen at once. 74 75 'medium' is used for devices such as tablets, TVs, or VR headsets. 76 This mode strikes a balance between clean readability and amount of 77 content visible. 78 79 'small' is used primarily for phones or other small devices where 80 content needs to be presented as large and clear in order to remain 81 readable from an average distance. 82 """ 83 84 LARGE = 0 85 MEDIUM = 1 86 SMALL = 2
The overall scale the UI is being rendered for. Note that this is independent of pixel resolution. For example, a phone and a desktop PC might render the game at similar pixel resolutions but the size they display content at will vary significantly.
Category: Enums
'large' is used for devices such as desktop PCs where fine details can be clearly seen. UI elements are generally smaller on the screen and more content can be seen at once.
'medium' is used for devices such as tablets, TVs, or VR headsets. This mode strikes a balance between clean readability and amount of content visible.
'small' is used primarily for phones or other small devices where content needs to be presented as large and clear in order to remain readable from an average distance.
Inherited Members
- enum.Enum
- name
- value
23class UIV1Subsystem(babase.AppSubsystem): 24 """Consolidated UI functionality for the app. 25 26 Category: **App Classes** 27 28 To use this class, access the single instance of it at 'ba.app.ui'. 29 """ 30 31 def __init__(self) -> None: 32 super().__init__() 33 env = babase.env() 34 35 self.controller: UIController | None = None 36 37 self._main_menu_window: bauiv1.Widget | None = None 38 self._main_menu_location: str | None = None 39 self.quit_window: bauiv1.Widget | None = None 40 41 # From classic. 42 self.main_menu_resume_callbacks: list = [] # Can probably go away. 43 44 self._uiscale: babase.UIScale 45 46 interfacetype = babase.app.config.get('UI Scale', env['ui_scale']) 47 if interfacetype == 'auto': 48 interfacetype = env['ui_scale'] 49 50 if interfacetype == 'large': 51 self._uiscale = babase.UIScale.LARGE 52 elif interfacetype == 'medium': 53 self._uiscale = babase.UIScale.MEDIUM 54 elif interfacetype == 'small': 55 self._uiscale = babase.UIScale.SMALL 56 else: 57 raise RuntimeError(f'Invalid UIScale value: {interfacetype}') 58 59 self.window_states: dict[type, Any] = {} # FIXME: Kill this. 60 self.main_menu_selection: str | None = None # FIXME: Kill this. 61 self.have_party_queue_window = False 62 self.cleanupchecks: list[UICleanupCheck] = [] 63 self.upkeeptimer: babase.AppTimer | None = None 64 self.use_toolbars = _bauiv1.toolbar_test() 65 66 self.title_color = (0.72, 0.7, 0.75) 67 self.heading_color = (0.72, 0.7, 0.75) 68 self.infotextcolor = (0.7, 0.9, 0.7) 69 70 # Switch our overall game selection UI flow between Play and 71 # Private-party playlist selection modes; should do this in 72 # a more elegant way once we revamp high level UI stuff a bit. 73 self.selecting_private_party_playlist: bool = False 74 75 @property 76 def available(self) -> bool: 77 """Can uiv1 currently be used? 78 79 Code that may run in headless mode, before the UI has been spun up, 80 while other ui systems are active, etc. can check this to avoid 81 likely erroring. 82 """ 83 return _bauiv1.is_available() 84 85 @property 86 def uiscale(self) -> babase.UIScale: 87 """Current ui scale for the app.""" 88 return self._uiscale 89 90 @override 91 def on_app_loading(self) -> None: 92 from bauiv1._uitypes import UIController, ui_upkeep 93 94 # IMPORTANT: If tweaking UI stuff, make sure it behaves for small, 95 # medium, and large UI modes. (doesn't run off screen, etc). 96 # The overrides below can be used to test with different sizes. 97 # Generally small is used on phones, medium is used on tablets/tvs, 98 # and large is on desktop computers or perhaps large tablets. When 99 # possible, run in windowed mode and resize the window to assure 100 # this holds true at all aspect ratios. 101 102 # UPDATE: A better way to test this is now by setting the environment 103 # variable BA_UI_SCALE to "small", "medium", or "large". 104 # This will affect system UIs not covered by the values below such 105 # as screen-messages. The below values remain functional, however, 106 # for cases such as Android where environment variables can't be set 107 # easily. 108 109 if bool(False): # force-test ui scale 110 self._uiscale = babase.UIScale.SMALL 111 with babase.ContextRef.empty(): 112 babase.pushcall( 113 lambda: babase.screenmessage( 114 f'FORCING UISCALE {self._uiscale.name} FOR TESTING', 115 color=(1, 0, 1), 116 log=True, 117 ) 118 ) 119 120 self.controller = UIController() 121 122 # Kick off our periodic UI upkeep. 123 # FIXME: Can probably kill this if we do immediate UI death checks. 124 self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True) 125 126 def set_main_menu_window( 127 self, 128 window: bauiv1.Widget, 129 from_window: bauiv1.Widget | None | bool = True, 130 ) -> None: 131 """Set the current 'main' window, replacing any existing. 132 133 If 'from_window' is passed as a bauiv1.Widget or None, a warning 134 will be issued if it that value does not match the current main 135 window. This can help clean up flawed code that can lead to bad 136 UI states. A value of False will disable the check. 137 """ 138 139 existing = self._main_menu_window 140 141 try: 142 if isinstance(from_window, bool): 143 # For default val True we warn that the arg wasn't 144 # passed. False can be explicitly passed to disable this 145 # check. 146 if from_window is True: 147 caller_frame = inspect.stack()[1] 148 caller_filename = caller_frame.filename 149 caller_line_number = caller_frame.lineno 150 logging.warning( 151 'set_main_menu_window() should be passed a' 152 " 'from_window' value to help ensure proper UI behavior" 153 ' (%s line %i).', 154 caller_filename, 155 caller_line_number, 156 ) 157 else: 158 # For everything else, warn if what they passed wasn't 159 # the previous main menu widget. 160 if from_window is not existing: 161 caller_frame = inspect.stack()[1] 162 caller_filename = caller_frame.filename 163 caller_line_number = caller_frame.lineno 164 logging.warning( 165 "set_main_menu_window() was passed 'from_window' %s" 166 ' but existing main-menu-window is %s. (%s line %i).', 167 from_window, 168 existing, 169 caller_filename, 170 caller_line_number, 171 ) 172 except Exception: 173 # Prevent any bugs in these checks from causing problems. 174 logging.exception('Error checking from_window') 175 176 # Once the above code leads to us fixing all leftover window bugs 177 # at the source, we can kill the code below. 178 179 # Let's grab the location where we were called from to report 180 # if we have to force-kill the existing window (which normally 181 # should not happen). 182 frameline = None 183 try: 184 frame = inspect.currentframe() 185 if frame is not None: 186 frame = frame.f_back 187 if frame is not None: 188 frameinfo = inspect.getframeinfo(frame) 189 frameline = f'{frameinfo.filename} {frameinfo.lineno}' 190 except Exception: 191 logging.exception('Error calcing line for set_main_menu_window') 192 193 # With our legacy main-menu system, the caller is responsible for 194 # clearing out the old main menu window when assigning the new. 195 # However there are corner cases where that doesn't happen and we get 196 # old windows stuck under the new main one. So let's guard against 197 # that. However, we can't simply delete the existing main window when 198 # a new one is assigned because the user may transition the old out 199 # *after* the assignment. Sigh. So, as a happy medium, let's check in 200 # on the old after a short bit of time and kill it if its still alive. 201 # That will be a bit ugly on screen but at least should un-break 202 # things. 203 def _delay_kill() -> None: 204 import time 205 206 if existing: 207 print( 208 f'Killing old main_menu_window' 209 f' when called at: {frameline} t={time.time():.3f}' 210 ) 211 existing.delete() 212 213 babase.apptimer(1.0, _delay_kill) 214 self._main_menu_window = window 215 216 def clear_main_menu_window(self, transition: str | None = None) -> None: 217 """Clear any existing 'main' window with the provided transition.""" 218 assert transition is None or not transition.endswith('_in') 219 if self._main_menu_window: 220 if ( 221 transition is not None 222 and not self._main_menu_window.transitioning_out 223 ): 224 _bauiv1.containerwidget( 225 edit=self._main_menu_window, transition=transition 226 ) 227 else: 228 self._main_menu_window.delete() 229 self._main_menu_window = None 230 231 def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: 232 """(internal)""" 233 234 # If there's no main menu up, just call immediately. 235 if not self.has_main_menu_window(): 236 with babase.ContextRef.empty(): 237 call() 238 else: 239 self.main_menu_resume_callbacks.append(call) 240 241 def has_main_menu_window(self) -> bool: 242 """Return whether a main menu window is present.""" 243 return bool(self._main_menu_window) 244 245 def set_main_menu_location(self, location: str) -> None: 246 """Set the location represented by the current main menu window.""" 247 self._main_menu_location = location 248 249 def get_main_menu_location(self) -> str | None: 250 """Return the current named main menu location, if any.""" 251 return self._main_menu_location
Consolidated UI functionality for the app.
Category: App Classes
To use this class, access the single instance of it at 'ba.app.ui'.
75 @property 76 def available(self) -> bool: 77 """Can uiv1 currently be used? 78 79 Code that may run in headless mode, before the UI has been spun up, 80 while other ui systems are active, etc. can check this to avoid 81 likely erroring. 82 """ 83 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.
85 @property 86 def uiscale(self) -> babase.UIScale: 87 """Current ui scale for the app.""" 88 return self._uiscale
Current ui scale for the app.
90 @override 91 def on_app_loading(self) -> None: 92 from bauiv1._uitypes import UIController, ui_upkeep 93 94 # IMPORTANT: If tweaking UI stuff, make sure it behaves for small, 95 # medium, and large UI modes. (doesn't run off screen, etc). 96 # The overrides below can be used to test with different sizes. 97 # Generally small is used on phones, medium is used on tablets/tvs, 98 # and large is on desktop computers or perhaps large tablets. When 99 # possible, run in windowed mode and resize the window to assure 100 # this holds true at all aspect ratios. 101 102 # UPDATE: A better way to test this is now by setting the environment 103 # variable BA_UI_SCALE to "small", "medium", or "large". 104 # This will affect system UIs not covered by the values below such 105 # as screen-messages. The below values remain functional, however, 106 # for cases such as Android where environment variables can't be set 107 # easily. 108 109 if bool(False): # force-test ui scale 110 self._uiscale = babase.UIScale.SMALL 111 with babase.ContextRef.empty(): 112 babase.pushcall( 113 lambda: babase.screenmessage( 114 f'FORCING UISCALE {self._uiscale.name} FOR TESTING', 115 color=(1, 0, 1), 116 log=True, 117 ) 118 ) 119 120 self.controller = UIController() 121 122 # Kick off our periodic UI upkeep. 123 # FIXME: Can probably kill this if we do immediate UI death checks. 124 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.
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
564def widget( 565 edit: bauiv1.Widget | None = None, 566 up_widget: bauiv1.Widget | None = None, 567 down_widget: bauiv1.Widget | None = None, 568 left_widget: bauiv1.Widget | None = None, 569 right_widget: bauiv1.Widget | None = None, 570 show_buffer_top: float | None = None, 571 show_buffer_bottom: float | None = None, 572 show_buffer_left: float | None = None, 573 show_buffer_right: float | None = None, 574 autoselect: bool | None = None, 575) -> None: 576 """Edit common attributes of any widget. 577 578 Category: **User Interface Functions** 579 580 Unlike other UI calls, this can only be used to edit, not to create. 581 """ 582 return None
Edit common attributes of any widget.
Category: User Interface Functions
Unlike other UI calls, this can only be used to edit, not to create.
75class Widget: 76 """Internal type for low level UI elements; buttons, windows, etc. 77 78 Category: **User Interface Classes** 79 80 This class represents a weak reference to a widget object 81 in the internal C++ layer. Currently, functions such as 82 bauiv1.buttonwidget() must be used to instantiate or edit these. 83 """ 84 85 transitioning_out: bool 86 """Whether this widget is in the process of dying (read only). 87 88 It can be useful to check this on a window's root widget to 89 prevent multiple window actions from firing simultaneously, 90 potentially leaving the UI in a broken state.""" 91 92 def __bool__(self) -> bool: 93 """Support for bool evaluation.""" 94 return bool(True) # Slight obfuscation. 95 96 def activate(self) -> None: 97 """Activates a widget; the same as if it had been clicked.""" 98 return None 99 100 def add_delete_callback(self, call: Callable) -> None: 101 """Add a call to be run immediately after this widget is destroyed.""" 102 return None 103 104 def delete(self, ignore_missing: bool = True) -> None: 105 """Delete the Widget. Ignores already-deleted Widgets if ignore_missing 106 is True; otherwise an Exception is thrown. 107 """ 108 return None 109 110 def exists(self) -> bool: 111 """Returns whether the Widget still exists. 112 Most functionality will fail on a nonexistent widget. 113 114 Note that you can also use the boolean operator for this same 115 functionality, so a statement such as "if mywidget" will do 116 the right thing both for Widget objects and values of None. 117 """ 118 return bool() 119 120 def get_children(self) -> list[bauiv1.Widget]: 121 """Returns any child Widgets of this Widget.""" 122 import bauiv1 123 124 return [bauiv1.Widget()] 125 126 def get_screen_space_center(self) -> tuple[float, float]: 127 """Returns the coords of the bauiv1.Widget center relative to the center 128 of the screen. This can be useful for placing pop-up windows and other 129 special cases. 130 """ 131 return (0.0, 0.0) 132 133 def get_selected_child(self) -> bauiv1.Widget | None: 134 """Returns the selected child Widget or None if nothing is selected.""" 135 import bauiv1 136 137 return bauiv1.Widget() 138 139 def get_widget_type(self) -> str: 140 """Return the internal type of the Widget as a string. Note that this 141 is different from the Python bauiv1.Widget type, which is the same for 142 all widgets. 143 """ 144 return str()
Internal type for low level UI elements; buttons, windows, etc.
Category: User Interface Classes
This class represents a weak reference to a widget object in the internal C++ layer. Currently, functions such as bauiv1.buttonwidget() must be used to instantiate or edit these.
Whether this widget is in the process of dying (read only).
It can be useful to check this on a window's root widget to prevent multiple window actions from firing simultaneously, potentially leaving the UI in a broken state.
96 def activate(self) -> None: 97 """Activates a widget; the same as if it had been clicked.""" 98 return None
Activates a widget; the same as if it had been clicked.
100 def add_delete_callback(self, call: Callable) -> None: 101 """Add a call to be run immediately after this widget is destroyed.""" 102 return None
Add a call to be run immediately after this widget is destroyed.
104 def delete(self, ignore_missing: bool = True) -> None: 105 """Delete the Widget. Ignores already-deleted Widgets if ignore_missing 106 is True; otherwise an Exception is thrown. 107 """ 108 return None
Delete the Widget. Ignores already-deleted Widgets if ignore_missing is True; otherwise an Exception is thrown.
110 def exists(self) -> bool: 111 """Returns whether the Widget still exists. 112 Most functionality will fail on a nonexistent widget. 113 114 Note that you can also use the boolean operator for this same 115 functionality, so a statement such as "if mywidget" will do 116 the right thing both for Widget objects and values of None. 117 """ 118 return bool()
Returns whether the Widget still exists. Most functionality will fail on a nonexistent widget.
Note that you can also use the boolean operator for this same functionality, so a statement such as "if mywidget" will do the right thing both for Widget objects and values of None.
120 def get_children(self) -> list[bauiv1.Widget]: 121 """Returns any child Widgets of this Widget.""" 122 import bauiv1 123 124 return [bauiv1.Widget()]
Returns any child Widgets of this Widget.
126 def get_screen_space_center(self) -> tuple[float, float]: 127 """Returns the coords of the bauiv1.Widget center relative to the center 128 of the screen. This can be useful for placing pop-up windows and other 129 special cases. 130 """ 131 return (0.0, 0.0)
Returns the coords of the bauiv1.Widget center relative to the center of the screen. This can be useful for placing pop-up windows and other special cases.
133 def get_selected_child(self) -> bauiv1.Widget | None: 134 """Returns the selected child Widget or None if nothing is selected.""" 135 import bauiv1 136 137 return bauiv1.Widget()
Returns the selected child Widget or None if nothing is selected.
139 def get_widget_type(self) -> str: 140 """Return the internal type of the Widget as a string. Note that this 141 is different from the Python bauiv1.Widget type, which is the same for 142 all widgets. 143 """ 144 return str()
Return the internal type of the Widget as a string. Note that this is different from the Python bauiv1.Widget type, which is the same for all widgets.
27class Window: 28 """A basic window. 29 30 Category: User Interface Classes 31 """ 32 33 def __init__(self, root_widget: bauiv1.Widget, cleanupcheck: bool = True): 34 self._root_widget = root_widget 35 36 # Complain if we outlive our root widget. 37 if cleanupcheck: 38 uicleanupcheck(self, root_widget) 39 40 def get_root_widget(self) -> bauiv1.Widget: 41 """Return the root widget.""" 42 return self._root_widget
A basic window.
Category: User Interface Classes