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 can_toggle_fullscreen, 35 charstr, 36 clipboard_is_supported, 37 clipboard_set_text, 38 commit_app_config, 39 ContextRef, 40 displaytime, 41 DisplayTime, 42 displaytimer, 43 DisplayTimer, 44 do_once, 45 fade_screen, 46 get_display_resolution, 47 get_ip_address_type, 48 get_low_level_config_value, 49 get_max_graphics_quality, 50 get_remote_app_name, 51 get_replays_dir, 52 get_string_height, 53 get_string_width, 54 get_type_name, 55 getclass, 56 have_permission, 57 in_logic_thread, 58 increment_analytics_count, 59 is_browser_likely_available, 60 is_running_on_fire_tv, 61 is_xcode_build, 62 Keyboard, 63 lock_all_input, 64 LoginAdapter, 65 Lstr, 66 NotFoundError, 67 Permission, 68 Plugin, 69 PluginSpec, 70 pushcall, 71 quit, 72 request_permission, 73 safecolor, 74 screenmessage, 75 set_analytics_screen, 76 set_low_level_config_value, 77 set_ui_input_device, 78 SpecialChar, 79 supports_max_fps, 80 supports_vsync, 81 timestring, 82 UIScale, 83 unlock_all_input, 84 WeakCall, 85 workspaces_in_use, 86) 87 88from _bauiv1 import ( 89 buttonwidget, 90 can_show_ad, 91 checkboxwidget, 92 columnwidget, 93 containerwidget, 94 get_qrcode_texture, 95 get_special_widget, 96 getmesh, 97 getsound, 98 gettexture, 99 has_video_ads, 100 have_incentivized_ad, 101 hscrollwidget, 102 imagewidget, 103 is_party_icon_visible, 104 Mesh, 105 open_file_externally, 106 open_url, 107 rowwidget, 108 scrollwidget, 109 set_party_icon_always_visible, 110 set_party_window_open, 111 show_ad, 112 show_ad_2, 113 show_online_score_ui, 114 Sound, 115 Texture, 116 textwidget, 117 uibounds, 118 Widget, 119 widget, 120) 121from bauiv1._uitypes import Window, uicleanupcheck 122from bauiv1._subsystem import UIV1Subsystem 123 124__all__ = [ 125 'add_clean_frame_callback', 126 'app', 127 'AppIntent', 128 'AppIntentDefault', 129 'AppIntentExec', 130 'AppMode', 131 'appname', 132 'appnameupper', 133 'appnameupper', 134 'apptime', 135 'AppTime', 136 'apptimer', 137 'AppTimer', 138 'buttonwidget', 139 'Call', 140 'can_show_ad', 141 'can_toggle_fullscreen', 142 'charstr', 143 'checkboxwidget', 144 'clipboard_is_supported', 145 'clipboard_set_text', 146 'columnwidget', 147 'commit_app_config', 148 'containerwidget', 149 'ContextRef', 150 'displaytime', 151 'DisplayTime', 152 'displaytimer', 153 'DisplayTimer', 154 'do_once', 155 'fade_screen', 156 'get_display_resolution', 157 'get_ip_address_type', 158 'get_low_level_config_value', 159 'get_max_graphics_quality', 160 'get_qrcode_texture', 161 'get_remote_app_name', 162 'get_replays_dir', 163 'get_special_widget', 164 'get_string_height', 165 'get_string_width', 166 'get_type_name', 167 'getclass', 168 'getmesh', 169 'getsound', 170 'gettexture', 171 'has_video_ads', 172 'have_incentivized_ad', 173 'have_permission', 174 'hscrollwidget', 175 'imagewidget', 176 'in_logic_thread', 177 'increment_analytics_count', 178 'is_browser_likely_available', 179 'is_party_icon_visible', 180 'is_running_on_fire_tv', 181 'is_xcode_build', 182 'Keyboard', 183 'lock_all_input', 184 'LoginAdapter', 185 'Lstr', 186 'Mesh', 187 'NotFoundError', 188 'open_file_externally', 189 'open_url', 190 'Permission', 191 'Plugin', 192 'PluginSpec', 193 'pushcall', 194 'quit', 195 'request_permission', 196 'rowwidget', 197 'safecolor', 198 'screenmessage', 199 'scrollwidget', 200 'set_analytics_screen', 201 'set_low_level_config_value', 202 'set_party_icon_always_visible', 203 'set_party_window_open', 204 'set_ui_input_device', 205 'show_ad', 206 'show_ad_2', 207 'show_online_score_ui', 208 'Sound', 209 'SpecialChar', 210 'supports_max_fps', 211 'supports_vsync', 212 'Texture', 213 'textwidget', 214 'timestring', 215 'uibounds', 216 'uicleanupcheck', 217 'UIScale', 218 'UIV1Subsystem', 219 'unlock_all_input', 220 'WeakCall', 221 'widget', 222 'Widget', 223 'Window', 224 'workspaces_in_use', 225] 226 227# We want stuff to show up as bauiv1.Foo instead of bauiv1._sub.Foo. 228set_canonical_module_names(globals()) 229 230# Sanity check: we want to keep ballistica's dependencies and 231# bootstrapping order clearly defined; let's check a few particular 232# modules to make sure they never directly or indirectly import us 233# before their own execs complete. 234if __debug__: 235 for _mdl in 'babase', '_babase': 236 if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): 237 logging.warning( 238 '%s was imported before %s finished importing;' 239 ' should not happen.', 240 __name__, 241 _mdl, 242 )
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 return cls._supports_intent(intent) 36 37 @classmethod 38 def _supports_intent(cls, intent: AppIntent) -> bool: 39 """Return whether our mode can handle the provided intent. 40 41 AppModes should override this to define what they can handle. 42 Note that AppExperience does not have to be considered here; that 43 is handled automatically by the can_handle_intent() call.""" 44 raise NotImplementedError('AppMode subclasses must override this.') 45 46 def handle_intent(self, intent: AppIntent) -> None: 47 """Handle an intent.""" 48 raise NotImplementedError('AppMode subclasses must override this.') 49 50 def on_activate(self) -> None: 51 """Called when the mode is being activated.""" 52 53 def on_deactivate(self) -> None: 54 """Called when the mode is being deactivated."""
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 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.
46 def handle_intent(self, intent: AppIntent) -> None: 47 """Handle an intent.""" 48 raise NotImplementedError('AppMode subclasses must override this.')
Handle an intent.
539def apptime() -> babase.AppTime: 540 """Return the current app-time in seconds. 541 542 Category: **General Utility Functions** 543 544 App-time is a monotonic time value; it starts at 0.0 when the app 545 launches and will never jump by large amounts or go backwards, even if 546 the system time changes. Its progression will pause when the app is in 547 a suspended state. 548 549 Note that the AppTime returned here is simply float; it just has a 550 unique type in the type-checker's eyes to help prevent it from being 551 accidentally used with time functionality expecting other time types. 552 """ 553 import babase # pylint: disable=cyclic-import 554 555 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.
558def apptimer(time: float, call: Callable[[], Any]) -> None: 559 """Schedule a callable object to run based on app-time. 560 561 Category: **General Utility Functions** 562 563 This function creates a one-off timer which cannot be canceled or 564 modified once created. If you require the ability to do so, or need 565 a repeating timer, use the babase.AppTimer class instead. 566 567 ##### Arguments 568 ###### time (float) 569 > Length of time in seconds that the timer will wait before firing. 570 571 ###### call (Callable[[], Any]) 572 > A callable Python object. Note that the timer will retain a 573 strong reference to the callable for as long as the timer exists, so you 574 may want to look into concepts such as babase.WeakCall if that is not 575 desired. 576 577 ##### Examples 578 Print some stuff through time: 579 >>> babase.screenmessage('hello from now!') 580 >>> babase.apptimer(1.0, babase.Call(babase.screenmessage, 581 'hello from the future!')) 582 >>> babase.apptimer(2.0, babase.Call(babase.screenmessage, 583 ... 'hello from the future 2!')) 584 """ 585 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)
598def charstr(char_id: babase.SpecialChar) -> str: 599 """Get a unicode string representing a special character. 600 601 Category: **General Utility Functions** 602 603 Note that these utilize the private-use block of unicode characters 604 (U+E000-U+F8FF) and are specific to the game; exporting or rendering 605 them elsewhere will be meaningless. 606 607 See babase.SpecialChar for the list of available characters. 608 """ 609 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.
202def checkboxwidget( 203 edit: bauiv1.Widget | None = None, 204 parent: bauiv1.Widget | None = None, 205 size: Sequence[float] | None = None, 206 position: Sequence[float] | None = None, 207 text: str | bauiv1.Lstr | None = None, 208 value: bool | None = None, 209 on_value_change_call: Callable[[bool], None] | None = None, 210 on_select_call: Callable[[], None] | None = None, 211 text_scale: float | None = None, 212 textcolor: Sequence[float] | None = None, 213 scale: float | None = None, 214 is_radio_button: bool | None = None, 215 maxwidth: float | None = None, 216 autoselect: bool | None = None, 217 color: Sequence[float] | None = None, 218) -> bauiv1.Widget: 219 """Create or edit a check-box widget. 220 221 Category: **User Interface Functions** 222 223 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 224 a new one is created and returned. Arguments that are not set to None 225 are applied to the Widget. 226 """ 227 import bauiv1 # pylint: disable=cyclic-import 228 229 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.
634def clipboard_is_supported() -> bool: 635 """Return whether this platform supports clipboard operations at all. 636 637 Category: **General Utility Functions** 638 639 If this returns False, UIs should not show 'copy to clipboard' 640 buttons, etc. 641 """ 642 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.
645def clipboard_set_text(value: str) -> None: 646 """Copy a string to the system clipboard. 647 648 Category: **General Utility Functions** 649 650 Ensure that babase.clipboard_is_supported() returns True before adding 651 buttons/etc. that make use of this functionality. 652 """ 653 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.
232def columnwidget( 233 edit: bauiv1.Widget | None = None, 234 parent: bauiv1.Widget | None = None, 235 size: Sequence[float] | None = None, 236 position: Sequence[float] | None = None, 237 background: bool | None = None, 238 selected_child: bauiv1.Widget | None = None, 239 visible_child: bauiv1.Widget | None = None, 240 single_depth: bool | None = None, 241 print_list_exit_instructions: bool | None = None, 242 left_border: float | None = None, 243 top_border: float | None = None, 244 bottom_border: float | None = None, 245 selection_loops_to_parent: bool | None = None, 246 border: float | None = None, 247 margin: float | None = None, 248 claims_left_right: bool | None = None, 249 claims_tab: bool | None = None, 250) -> bauiv1.Widget: 251 """Create or edit a column widget. 252 253 Category: **User Interface Functions** 254 255 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 256 a new one is created and returned. Arguments that are not set to None 257 are applied to the Widget. 258 """ 259 import bauiv1 # pylint: disable=cyclic-import 260 261 return bauiv1.Widget()
Create or edit a column widget.
Category: User Interface Functions
Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.
274def containerwidget( 275 edit: bauiv1.Widget | None = None, 276 parent: bauiv1.Widget | None = None, 277 size: Sequence[float] | None = None, 278 position: Sequence[float] | None = None, 279 background: bool | None = None, 280 selected_child: bauiv1.Widget | None = None, 281 transition: str | None = None, 282 cancel_button: bauiv1.Widget | None = None, 283 start_button: bauiv1.Widget | None = None, 284 root_selectable: bool | None = None, 285 on_activate_call: Callable[[], None] | None = None, 286 claims_left_right: bool | None = None, 287 claims_tab: bool | None = None, 288 selection_loops: bool | None = None, 289 selection_loops_to_parent: bool | None = None, 290 scale: float | None = None, 291 on_outside_click_call: Callable[[], None] | None = None, 292 single_depth: bool | None = None, 293 visible_child: bauiv1.Widget | None = None, 294 stack_offset: Sequence[float] | None = None, 295 color: Sequence[float] | None = None, 296 on_cancel_call: Callable[[], None] | None = None, 297 print_list_exit_instructions: bool | None = None, 298 click_activate: bool | None = None, 299 always_highlight: bool | None = None, 300 selectable: bool | None = None, 301 scale_origin_stack_offset: Sequence[float] | None = None, 302 toolbar_visibility: str | None = None, 303 on_select_call: Callable[[], None] | None = None, 304 claim_outside_clicks: bool | None = None, 305 claims_up_down: bool | None = None, 306) -> bauiv1.Widget: 307 """Create or edit a container widget. 308 309 Category: **User Interface Functions** 310 311 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 312 a new one is created and returned. Arguments that are not set to None 313 are applied to the Widget. 314 """ 315 import bauiv1 # pylint: disable=cyclic-import 316 317 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.
694def displaytime() -> babase.DisplayTime: 695 """Return the current display-time in seconds. 696 697 Category: **General Utility Functions** 698 699 Display-time is a time value intended to be used for animation and other 700 visual purposes. It will generally increment by a consistent amount each 701 frame. It will pass at an overall similar rate to AppTime, but trades 702 accuracy for smoothness. 703 704 Note that the value returned here is simply a float; it just has a 705 unique type in the type-checker's eyes to help prevent it from being 706 accidentally used with time functionality expecting other time types. 707 """ 708 import babase # pylint: disable=cyclic-import 709 710 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.
713def displaytimer(time: float, call: Callable[[], Any]) -> None: 714 """Schedule a callable object to run based on display-time. 715 716 Category: **General Utility Functions** 717 718 This function creates a one-off timer which cannot be canceled or 719 modified once created. If you require the ability to do so, or need 720 a repeating timer, use the babase.DisplayTimer class instead. 721 722 Display-time is a time value intended to be used for animation and other 723 visual purposes. It will generally increment by a consistent amount each 724 frame. It will pass at an overall similar rate to AppTime, but trades 725 accuracy for smoothness. 726 727 ##### Arguments 728 ###### time (float) 729 > Length of time in seconds that the timer will wait before firing. 730 731 ###### call (Callable[[], Any]) 732 > A callable Python object. Note that the timer will retain a 733 strong reference to the callable for as long as the timer exists, so you 734 may want to look into concepts such as babase.WeakCall if that is not 735 desired. 736 737 ##### Examples 738 Print some stuff through time: 739 >>> babase.screenmessage('hello from now!') 740 >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage, 741 ... 'hello from the future!')) 742 >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage, 743 ... 'hello from the future 2!')) 744 """ 745 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)
753def do_once() -> bool: 754 """Return whether this is the first time running a line of code. 755 756 Category: **General Utility Functions** 757 758 This is used by 'print_once()' type calls to keep from overflowing 759 logs. The call functions by registering the filename and line where 760 The call is made from. Returns True if this location has not been 761 registered already, and False if it has. 762 763 ##### Example 764 This print will only fire for the first loop iteration: 765 >>> for i in range(10): 766 ... if babase.do_once(): 767 ... print('HelloWorld once from loop!') 768 """ 769 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!')
54def get_ip_address_type(addr: str) -> socket.AddressFamily: 55 """Return socket.AF_INET6 or socket.AF_INET4 for the provided address.""" 56 import socket 57 58 socket_type = None 59 60 # First try it as an ipv4 address. 61 try: 62 socket.inet_pton(socket.AF_INET, addr) 63 socket_type = socket.AF_INET 64 except OSError: 65 pass 66 67 # Hmm apparently not ipv4; try ipv6. 68 if socket_type is None: 69 try: 70 socket.inet_pton(socket.AF_INET6, addr) 71 socket_type = socket.AF_INET6 72 except OSError: 73 pass 74 if socket_type is None: 75 raise ValueError(f'addr seems to be neither v4 or v6: {addr}') 76 return socket_type
Return socket.AF_INET6 or socket.AF_INET4 for the provided address.
328def get_qrcode_texture(url: str) -> bauiv1.Texture: 329 """Return a QR code texture. 330 331 The provided url must be 64 bytes or less. 332 """ 333 import bauiv1 # pylint: disable=cyclic-import 334 335 return bauiv1.Texture()
Return a QR code texture.
The provided url must be 64 bytes or less.
107def get_type_name(cls: type) -> str: 108 """Return a full type name including module for a class.""" 109 return f'{cls.__module__}.{cls.__name__}'
Return a full type name including module for a class.
70def getclass(name: str, subclassof: type[T]) -> type[T]: 71 """Given a full class name such as foo.bar.MyClass, return the class. 72 73 Category: **General Utility Functions** 74 75 The class will be checked to make sure it is a subclass of the provided 76 'subclassof' class, and a TypeError will be raised if not. 77 """ 78 import importlib 79 80 splits = name.split('.') 81 modulename = '.'.join(splits[:-1]) 82 classname = splits[-1] 83 module = importlib.import_module(modulename) 84 cls: type = getattr(module, classname) 85 86 if not issubclass(cls, subclassof): 87 raise TypeError(f'{name} is not a subclass of {subclassof}.') 88 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.
345def getmesh(name: str) -> bauiv1.Mesh: 346 """Load a mesh for use solely in the local user interface.""" 347 import bauiv1 # pylint: disable=cyclic-import 348 349 return bauiv1.Mesh()
Load a mesh for use solely in the local user interface.
352def getsound(name: str) -> bauiv1.Sound: 353 """Load a sound for use in the ui.""" 354 import bauiv1 # pylint: disable=cyclic-import 355 356 return bauiv1.Sound()
Load a sound for use in the ui.
359def gettexture(name: str) -> bauiv1.Texture: 360 """Load a texture for use in the ui.""" 361 import bauiv1 # pylint: disable=cyclic-import 362 363 return bauiv1.Texture()
Load a texture for use in the ui.
376def hscrollwidget( 377 edit: bauiv1.Widget | None = None, 378 parent: bauiv1.Widget | None = None, 379 size: Sequence[float] | None = None, 380 position: Sequence[float] | None = None, 381 background: bool | None = None, 382 selected_child: bauiv1.Widget | None = None, 383 capture_arrows: bool | None = None, 384 on_select_call: Callable[[], None] | None = None, 385 center_small_content: bool | None = None, 386 color: Sequence[float] | None = None, 387 highlight: bool | None = None, 388 border_opacity: float | None = None, 389 simple_culling_h: float | None = None, 390 claims_left_right: bool | None = None, 391 claims_up_down: bool | None = None, 392 claims_tab: bool | None = None, 393) -> bauiv1.Widget: 394 """Create or edit a horizontal scroll widget. 395 396 Category: **User Interface Functions** 397 398 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 399 a new one is created and returned. Arguments that are not set to None 400 are applied to the Widget. 401 """ 402 import bauiv1 # pylint: disable=cyclic-import 403 404 return bauiv1.Widget()
Create or edit a horizontal scroll widget.
Category: User Interface Functions
Pass a valid existing 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.
407def imagewidget( 408 edit: bauiv1.Widget | None = None, 409 parent: bauiv1.Widget | None = None, 410 size: Sequence[float] | None = None, 411 position: Sequence[float] | None = None, 412 color: Sequence[float] | None = None, 413 texture: bauiv1.Texture | None = None, 414 opacity: float | None = None, 415 mesh_transparent: bauiv1.Mesh | None = None, 416 mesh_opaque: bauiv1.Mesh | None = None, 417 has_alpha_channel: bool = True, 418 tint_texture: bauiv1.Texture | None = None, 419 tint_color: Sequence[float] | None = None, 420 transition_delay: float | None = None, 421 draw_controller: bauiv1.Widget | None = None, 422 tint2_color: Sequence[float] | None = None, 423 tilt_scale: float | None = None, 424 mask_texture: bauiv1.Texture | None = None, 425 radial_amount: float | None = None, 426) -> bauiv1.Widget: 427 """Create or edit an image widget. 428 429 Category: **User Interface Functions** 430 431 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 432 a new one is created and returned. Arguments that are not set to None 433 are applied to the Widget. 434 """ 435 import bauiv1 # pylint: disable=cyclic-import 436 437 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.
26def is_browser_likely_available() -> bool: 27 """Return whether a browser likely exists on the current device. 28 29 category: General Utility Functions 30 31 If this returns False you may want to avoid calling babase.show_url() 32 with any lengthy addresses. (ba.show_url() will display an address 33 as a string in a window if unable to bring up a browser, but that 34 is only useful for simple URLs.) 35 """ 36 app = _babase.app 37 38 if app.classic is None: 39 logging.warning( 40 'is_browser_likely_available() needs to be updated' 41 ' to work without classic.' 42 ) 43 return True 44 45 platform = app.classic.platform 46 hastouchscreen = _babase.hastouchscreen() 47 48 # If we're on a vr device or an android device with no touchscreen, 49 # assume no browser. 50 # FIXME: Might not be the case anymore; should make this definable 51 # at the platform level. 52 if app.env.vr or (platform == 'android' and not hastouchscreen): 53 return False 54 55 # Anywhere else assume we've got one. 56 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.
24class LoginAdapter: 25 """Allows using implicit login types in an explicit way. 26 27 Some login types such as Google Play Game Services or Game Center are 28 basically always present and often do not provide a way to log out 29 from within a running app, so this adapter exists to use them in a 30 flexible manner by 'attaching' and 'detaching' from an always-present 31 login, allowing for its use alongside other login types. It also 32 provides common functionality for server-side account verification and 33 other handy bits. 34 """ 35 36 @dataclass 37 class SignInResult: 38 """Describes the final result of a sign-in attempt.""" 39 40 credentials: str 41 42 @dataclass 43 class ImplicitLoginState: 44 """Describes the current state of an implicit login.""" 45 46 login_id: str 47 display_name: str 48 49 def __init__(self, login_type: LoginType): 50 assert _babase.in_logic_thread() 51 self.login_type = login_type 52 self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = ( 53 None 54 ) 55 self._on_app_loading_called = False 56 self._implicit_login_state_dirty = False 57 self._back_end_active = False 58 59 # Which login of our type (if any) is associated with the 60 # current active primary account. 61 self._active_login_id: str | None = None 62 63 self._last_sign_in_time: float | None = None 64 self._last_sign_in_desc: str | None = None 65 66 def on_app_loading(self) -> None: 67 """Should be called for each adapter in on_app_loading.""" 68 69 assert not self._on_app_loading_called 70 self._on_app_loading_called = True 71 72 # Any implicit state we received up until now needs to be pushed 73 # to the app account subsystem. 74 self._update_implicit_login_state() 75 76 def set_implicit_login_state( 77 self, state: ImplicitLoginState | None 78 ) -> None: 79 """Keep the adapter informed of implicit login states. 80 81 This should be called by the adapter back-end when an account 82 of their associated type gets logged in or out. 83 """ 84 assert _babase.in_logic_thread() 85 86 # Ignore redundant sets. 87 if state == self._implicit_login_state: 88 return 89 90 if DEBUG_LOG: 91 if state is None: 92 logging.debug( 93 'LoginAdapter: %s implicit state changed;' 94 ' now signed out.', 95 self.login_type.name, 96 ) 97 else: 98 logging.debug( 99 'LoginAdapter: %s implicit state changed;' 100 ' now signed in as %s.', 101 self.login_type.name, 102 state.display_name, 103 ) 104 105 self._implicit_login_state = state 106 self._implicit_login_state_dirty = True 107 108 # (possibly) push it to the app for handling. 109 self._update_implicit_login_state() 110 111 # This might affect whether we consider that back-end as 'active'. 112 self._update_back_end_active() 113 114 def set_active_logins(self, logins: dict[LoginType, str]) -> None: 115 """Keep the adapter informed of actively used logins. 116 117 This should be called by the app's account subsystem to 118 keep adapters up to date on the full set of logins attached 119 to the currently-in-use account. 120 Note that the logins dict passed in should be immutable as 121 only a reference to it is stored, not a copy. 122 """ 123 assert _babase.in_logic_thread() 124 if DEBUG_LOG: 125 logging.debug( 126 'LoginAdapter: %s adapter got active logins %s.', 127 self.login_type.name, 128 {k: v[:4] + '...' + v[-4:] for k, v in logins.items()}, 129 ) 130 131 self._active_login_id = logins.get(self.login_type) 132 self._update_back_end_active() 133 134 def on_back_end_active_change(self, active: bool) -> None: 135 """Called when active state for the back-end is (possibly) changing. 136 137 Meant to be overridden by subclasses. 138 Being active means that the implicit login provided by the back-end 139 is actually being used by the app. It should therefore register 140 unlocked achievements, leaderboard scores, allow viewing native 141 UIs, etc. When not active it should ignore everything and behave 142 as if logged out, even if it technically is still logged in. 143 """ 144 assert _babase.in_logic_thread() 145 del active # Unused. 146 147 @final 148 def sign_in( 149 self, 150 result_cb: Callable[[LoginAdapter, SignInResult | Exception], None], 151 description: str, 152 ) -> None: 153 """Attempt an explicit sign in via this adapter. 154 155 This can be called even if the back-end is not implicitly signed in; 156 the adapter will attempt to sign in if possible. An exception will 157 be returned if the sign-in attempt fails. 158 """ 159 assert _babase.in_logic_thread() 160 from babase._general import Call 161 162 # Have been seeing multiple sign-in attempts come through 163 # nearly simultaneously which can be problematic server-side. 164 # Let's error if a sign-in attempt is made within a few seconds 165 # of the last one to address this. 166 now = time.monotonic() 167 appnow = _babase.apptime() 168 if self._last_sign_in_time is not None: 169 since_last = now - self._last_sign_in_time 170 if since_last < 1.0: 171 logging.warning( 172 'LoginAdapter: %s adapter sign_in() called too soon' 173 ' (%.2fs) after last; this-desc="%s", last-desc="%s",' 174 ' ba-app-time=%.2f.', 175 self.login_type.name, 176 since_last, 177 description, 178 self._last_sign_in_desc, 179 appnow, 180 ) 181 _babase.pushcall( 182 Call( 183 result_cb, 184 self, 185 RuntimeError('sign_in called too soon after last.'), 186 ) 187 ) 188 return 189 190 self._last_sign_in_desc = description 191 self._last_sign_in_time = now 192 193 if DEBUG_LOG: 194 logging.debug( 195 'LoginAdapter: %s adapter sign_in() called;' 196 ' fetching sign-in-token...', 197 self.login_type.name, 198 ) 199 200 def _got_sign_in_token_result(result: str | None) -> None: 201 import bacommon.cloud 202 203 # Failed to get a sign-in-token. 204 if result is None: 205 if DEBUG_LOG: 206 logging.debug( 207 'LoginAdapter: %s adapter sign-in-token fetch failed;' 208 ' aborting sign-in.', 209 self.login_type.name, 210 ) 211 _babase.pushcall( 212 Call( 213 result_cb, 214 self, 215 RuntimeError('fetch-sign-in-token failed.'), 216 ) 217 ) 218 return 219 220 # Got a sign-in token! Now pass it to the cloud which will use 221 # it to verify our identity and give us app credentials on 222 # success. 223 if DEBUG_LOG: 224 logging.debug( 225 'LoginAdapter: %s adapter sign-in-token fetch succeeded;' 226 ' passing to cloud for verification...', 227 self.login_type.name, 228 ) 229 230 def _got_sign_in_response( 231 response: bacommon.cloud.SignInResponse | Exception, 232 ) -> None: 233 if isinstance(response, Exception): 234 if DEBUG_LOG: 235 logging.debug( 236 'LoginAdapter: %s adapter got error' 237 ' sign-in response: %s', 238 self.login_type.name, 239 response, 240 ) 241 _babase.pushcall(Call(result_cb, self, response)) 242 else: 243 if DEBUG_LOG: 244 logging.debug( 245 'LoginAdapter: %s adapter got successful' 246 ' sign-in response', 247 self.login_type.name, 248 ) 249 if response.credentials is None: 250 result2: LoginAdapter.SignInResult | Exception = ( 251 RuntimeError( 252 'No credentials returned after' 253 ' submitting sign-in-token.' 254 ) 255 ) 256 else: 257 result2 = self.SignInResult( 258 credentials=response.credentials 259 ) 260 _babase.pushcall(Call(result_cb, self, result2)) 261 262 assert _babase.app.plus is not None 263 _babase.app.plus.cloud.send_message_cb( 264 bacommon.cloud.SignInMessage( 265 self.login_type, 266 result, 267 description=description, 268 apptime=appnow, 269 ), 270 on_response=_got_sign_in_response, 271 ) 272 273 # Kick off the process by fetching a sign-in token. 274 self.get_sign_in_token(completion_cb=_got_sign_in_token_result) 275 276 def is_back_end_active(self) -> bool: 277 """Is this adapter's back-end currently active?""" 278 return self._back_end_active 279 280 def get_sign_in_token( 281 self, completion_cb: Callable[[str | None], None] 282 ) -> None: 283 """Get a sign-in token from the adapter back end. 284 285 This token is then passed to the master-server to complete the 286 login process. 287 The adapter can use this opportunity to bring up account creation 288 UI, call its internal sign_in function, etc. as needed. 289 The provided completion_cb should then be called with either a token 290 or None if sign in failed or was cancelled. 291 """ 292 from babase._general import Call 293 294 # Default implementation simply fails immediately. 295 _babase.pushcall(Call(completion_cb, None)) 296 297 def _update_implicit_login_state(self) -> None: 298 # If we've received an implicit login state, schedule it to be 299 # sent along to the app. We wait until on-app-launch has been 300 # called so that account-client-v2 has had a chance to load 301 # any existing state so it can properly respond to this. 302 if self._implicit_login_state_dirty and self._on_app_loading_called: 303 from babase._general import Call 304 305 if DEBUG_LOG: 306 logging.debug( 307 'LoginAdapter: %s adapter sending' 308 ' implicit-state-changed to app.', 309 self.login_type.name, 310 ) 311 312 assert _babase.app.plus is not None 313 _babase.pushcall( 314 Call( 315 _babase.app.plus.accounts.on_implicit_login_state_changed, 316 self.login_type, 317 self._implicit_login_state, 318 ) 319 ) 320 self._implicit_login_state_dirty = False 321 322 def _update_back_end_active(self) -> None: 323 was_active = self._back_end_active 324 if self._implicit_login_state is None: 325 is_active = False 326 else: 327 is_active = ( 328 self._implicit_login_state.login_id == self._active_login_id 329 ) 330 if was_active != is_active: 331 if DEBUG_LOG: 332 logging.debug( 333 'LoginAdapter: %s adapter back-end-active is now %s.', 334 self.login_type.name, 335 is_active, 336 ) 337 self.on_back_end_active_change(is_active) 338 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.
49 def __init__(self, login_type: LoginType): 50 assert _babase.in_logic_thread() 51 self.login_type = login_type 52 self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = ( 53 None 54 ) 55 self._on_app_loading_called = False 56 self._implicit_login_state_dirty = False 57 self._back_end_active = False 58 59 # Which login of our type (if any) is associated with the 60 # current active primary account. 61 self._active_login_id: str | None = None 62 63 self._last_sign_in_time: float | None = None 64 self._last_sign_in_desc: str | None = None
66 def on_app_loading(self) -> None: 67 """Should be called for each adapter in on_app_loading.""" 68 69 assert not self._on_app_loading_called 70 self._on_app_loading_called = True 71 72 # Any implicit state we received up until now needs to be pushed 73 # to the app account subsystem. 74 self._update_implicit_login_state()
Should be called for each adapter in on_app_loading.
76 def set_implicit_login_state( 77 self, state: ImplicitLoginState | None 78 ) -> None: 79 """Keep the adapter informed of implicit login states. 80 81 This should be called by the adapter back-end when an account 82 of their associated type gets logged in or out. 83 """ 84 assert _babase.in_logic_thread() 85 86 # Ignore redundant sets. 87 if state == self._implicit_login_state: 88 return 89 90 if DEBUG_LOG: 91 if state is None: 92 logging.debug( 93 'LoginAdapter: %s implicit state changed;' 94 ' now signed out.', 95 self.login_type.name, 96 ) 97 else: 98 logging.debug( 99 'LoginAdapter: %s implicit state changed;' 100 ' now signed in as %s.', 101 self.login_type.name, 102 state.display_name, 103 ) 104 105 self._implicit_login_state = state 106 self._implicit_login_state_dirty = True 107 108 # (possibly) push it to the app for handling. 109 self._update_implicit_login_state() 110 111 # This might affect whether we consider that back-end as 'active'. 112 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.
114 def set_active_logins(self, logins: dict[LoginType, str]) -> None: 115 """Keep the adapter informed of actively used logins. 116 117 This should be called by the app's account subsystem to 118 keep adapters up to date on the full set of logins attached 119 to the currently-in-use account. 120 Note that the logins dict passed in should be immutable as 121 only a reference to it is stored, not a copy. 122 """ 123 assert _babase.in_logic_thread() 124 if DEBUG_LOG: 125 logging.debug( 126 'LoginAdapter: %s adapter got active logins %s.', 127 self.login_type.name, 128 {k: v[:4] + '...' + v[-4:] for k, v in logins.items()}, 129 ) 130 131 self._active_login_id = logins.get(self.login_type) 132 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.
134 def on_back_end_active_change(self, active: bool) -> None: 135 """Called when active state for the back-end is (possibly) changing. 136 137 Meant to be overridden by subclasses. 138 Being active means that the implicit login provided by the back-end 139 is actually being used by the app. It should therefore register 140 unlocked achievements, leaderboard scores, allow viewing native 141 UIs, etc. When not active it should ignore everything and behave 142 as if logged out, even if it technically is still logged in. 143 """ 144 assert _babase.in_logic_thread() 145 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 logged out, even if it technically is still logged in.
147 @final 148 def sign_in( 149 self, 150 result_cb: Callable[[LoginAdapter, SignInResult | Exception], None], 151 description: str, 152 ) -> None: 153 """Attempt an explicit sign in via this adapter. 154 155 This can be called even if the back-end is not implicitly signed in; 156 the adapter will attempt to sign in if possible. An exception will 157 be returned if the sign-in attempt fails. 158 """ 159 assert _babase.in_logic_thread() 160 from babase._general import Call 161 162 # Have been seeing multiple sign-in attempts come through 163 # nearly simultaneously which can be problematic server-side. 164 # Let's error if a sign-in attempt is made within a few seconds 165 # of the last one to address this. 166 now = time.monotonic() 167 appnow = _babase.apptime() 168 if self._last_sign_in_time is not None: 169 since_last = now - self._last_sign_in_time 170 if since_last < 1.0: 171 logging.warning( 172 'LoginAdapter: %s adapter sign_in() called too soon' 173 ' (%.2fs) after last; this-desc="%s", last-desc="%s",' 174 ' ba-app-time=%.2f.', 175 self.login_type.name, 176 since_last, 177 description, 178 self._last_sign_in_desc, 179 appnow, 180 ) 181 _babase.pushcall( 182 Call( 183 result_cb, 184 self, 185 RuntimeError('sign_in called too soon after last.'), 186 ) 187 ) 188 return 189 190 self._last_sign_in_desc = description 191 self._last_sign_in_time = now 192 193 if DEBUG_LOG: 194 logging.debug( 195 'LoginAdapter: %s adapter sign_in() called;' 196 ' fetching sign-in-token...', 197 self.login_type.name, 198 ) 199 200 def _got_sign_in_token_result(result: str | None) -> None: 201 import bacommon.cloud 202 203 # Failed to get a sign-in-token. 204 if result is None: 205 if DEBUG_LOG: 206 logging.debug( 207 'LoginAdapter: %s adapter sign-in-token fetch failed;' 208 ' aborting sign-in.', 209 self.login_type.name, 210 ) 211 _babase.pushcall( 212 Call( 213 result_cb, 214 self, 215 RuntimeError('fetch-sign-in-token failed.'), 216 ) 217 ) 218 return 219 220 # Got a sign-in token! Now pass it to the cloud which will use 221 # it to verify our identity and give us app credentials on 222 # success. 223 if DEBUG_LOG: 224 logging.debug( 225 'LoginAdapter: %s adapter sign-in-token fetch succeeded;' 226 ' passing to cloud for verification...', 227 self.login_type.name, 228 ) 229 230 def _got_sign_in_response( 231 response: bacommon.cloud.SignInResponse | Exception, 232 ) -> None: 233 if isinstance(response, Exception): 234 if DEBUG_LOG: 235 logging.debug( 236 'LoginAdapter: %s adapter got error' 237 ' sign-in response: %s', 238 self.login_type.name, 239 response, 240 ) 241 _babase.pushcall(Call(result_cb, self, response)) 242 else: 243 if DEBUG_LOG: 244 logging.debug( 245 'LoginAdapter: %s adapter got successful' 246 ' sign-in response', 247 self.login_type.name, 248 ) 249 if response.credentials is None: 250 result2: LoginAdapter.SignInResult | Exception = ( 251 RuntimeError( 252 'No credentials returned after' 253 ' submitting sign-in-token.' 254 ) 255 ) 256 else: 257 result2 = self.SignInResult( 258 credentials=response.credentials 259 ) 260 _babase.pushcall(Call(result_cb, self, result2)) 261 262 assert _babase.app.plus is not None 263 _babase.app.plus.cloud.send_message_cb( 264 bacommon.cloud.SignInMessage( 265 self.login_type, 266 result, 267 description=description, 268 apptime=appnow, 269 ), 270 on_response=_got_sign_in_response, 271 ) 272 273 # Kick off the process by fetching a sign-in token. 274 self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
Attempt an explicit 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.
276 def is_back_end_active(self) -> bool: 277 """Is this adapter's back-end currently active?""" 278 return self._back_end_active
Is this adapter's back-end currently active?
280 def get_sign_in_token( 281 self, completion_cb: Callable[[str | None], None] 282 ) -> None: 283 """Get a sign-in token from the adapter back end. 284 285 This token is then passed to the master-server to complete the 286 login process. 287 The adapter can use this opportunity to bring up account creation 288 UI, call its internal sign_in function, etc. as needed. 289 The provided completion_cb should then be called with either a token 290 or None if sign in failed or was cancelled. 291 """ 292 from babase._general import Call 293 294 # Default implementation simply fails immediately. 295 _babase.pushcall(Call(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 login 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.
36 @dataclass 37 class SignInResult: 38 """Describes the final result of a sign-in attempt.""" 39 40 credentials: str
Describes the final result of a sign-in attempt.
42 @dataclass 43 class ImplicitLoginState: 44 """Describes the current state of an implicit login.""" 45 46 login_id: str 47 display_name: str
Describes the current state of an implicit login.
439class Lstr: 440 """Used to define strings in a language-independent way. 441 442 Category: **General Utility Classes** 443 444 These should be used whenever possible in place of hard-coded 445 strings so that in-game or UI elements show up correctly on all 446 clients in their currently-active language. 447 448 To see available resource keys, look at any of the bs_language_*.py 449 files in the game or the translations pages at 450 legacy.ballistica.net/translate. 451 452 ##### Examples 453 EXAMPLE 1: specify a string from a resource path 454 >>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText') 455 456 EXAMPLE 2: specify a translated string via a category and english 457 value; if a translated value is available, it will be used; otherwise 458 the english value will be. To see available translation categories, 459 look under the 'translations' resource section. 460 >>> mynode.text = babase.Lstr(translate=('gameDescriptions', 461 ... 'Defeat all enemies')) 462 463 EXAMPLE 3: specify a raw value and some substitutions. Substitutions 464 can be used with resource and translate modes as well. 465 >>> mynode.text = babase.Lstr(value='${A} / ${B}', 466 ... subs=[('${A}', str(score)), ('${B}', str(total))]) 467 468 EXAMPLE 4: babase.Lstr's can be nested. This example would display the 469 resource at res_a but replace ${NAME} with the value of the 470 resource at res_b 471 >>> mytextnode.text = babase.Lstr( 472 ... resource='res_a', 473 ... subs=[('${NAME}', babase.Lstr(resource='res_b'))]) 474 """ 475 476 # pylint: disable=dangerous-default-value 477 # noinspection PyDefaultArgument 478 @overload 479 def __init__( 480 self, 481 *, 482 resource: str, 483 fallback_resource: str = '', 484 fallback_value: str = '', 485 subs: Sequence[tuple[str, str | Lstr]] = [], 486 ) -> None: 487 """Create an Lstr from a string resource.""" 488 489 # noinspection PyShadowingNames,PyDefaultArgument 490 @overload 491 def __init__( 492 self, 493 *, 494 translate: tuple[str, str], 495 subs: Sequence[tuple[str, str | Lstr]] = [], 496 ) -> None: 497 """Create an Lstr by translating a string in a category.""" 498 499 # noinspection PyDefaultArgument 500 @overload 501 def __init__( 502 self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = [] 503 ) -> None: 504 """Create an Lstr from a raw string value.""" 505 506 # pylint: enable=redefined-outer-name, dangerous-default-value 507 508 def __init__(self, *args: Any, **keywds: Any) -> None: 509 """Instantiate a Lstr. 510 511 Pass a value for either 'resource', 'translate', 512 or 'value'. (see Lstr help for examples). 513 'subs' can be a sequence of 2-member sequences consisting of values 514 and replacements. 515 'fallback_resource' can be a resource key that will be used if the 516 main one is not present for 517 the current language in place of falling back to the english value 518 ('resource' mode only). 519 'fallback_value' can be a literal string that will be used if neither 520 the resource nor the fallback resource is found ('resource' mode only). 521 """ 522 # pylint: disable=too-many-branches 523 if args: 524 raise TypeError('Lstr accepts only keyword arguments') 525 526 # Basically just store the exact args they passed. 527 # However if they passed any Lstr values for subs, 528 # replace them with that Lstr's dict. 529 self.args = keywds 530 our_type = type(self) 531 532 if isinstance(self.args.get('value'), our_type): 533 raise TypeError("'value' must be a regular string; not an Lstr") 534 535 if 'subs' in self.args: 536 subs_new = [] 537 for key, value in keywds['subs']: 538 if isinstance(value, our_type): 539 subs_new.append((key, value.args)) 540 else: 541 subs_new.append((key, value)) 542 self.args['subs'] = subs_new 543 544 # As of protocol 31 we support compact key names 545 # ('t' instead of 'translate', etc). Convert as needed. 546 if 'translate' in keywds: 547 keywds['t'] = keywds['translate'] 548 del keywds['translate'] 549 if 'resource' in keywds: 550 keywds['r'] = keywds['resource'] 551 del keywds['resource'] 552 if 'value' in keywds: 553 keywds['v'] = keywds['value'] 554 del keywds['value'] 555 if 'fallback' in keywds: 556 from babase import _error 557 558 _error.print_error( 559 'deprecated "fallback" arg passed to Lstr(); use ' 560 'either "fallback_resource" or "fallback_value"', 561 once=True, 562 ) 563 keywds['f'] = keywds['fallback'] 564 del keywds['fallback'] 565 if 'fallback_resource' in keywds: 566 keywds['f'] = keywds['fallback_resource'] 567 del keywds['fallback_resource'] 568 if 'subs' in keywds: 569 keywds['s'] = keywds['subs'] 570 del keywds['subs'] 571 if 'fallback_value' in keywds: 572 keywds['fv'] = keywds['fallback_value'] 573 del keywds['fallback_value'] 574 575 def evaluate(self) -> str: 576 """Evaluate the Lstr and returns a flat string in the current language. 577 578 You should avoid doing this as much as possible and instead pass 579 and store Lstr values. 580 """ 581 return _babase.evaluate_lstr(self._get_json()) 582 583 def is_flat_value(self) -> bool: 584 """Return whether the Lstr is a 'flat' value. 585 586 This is defined as a simple string value incorporating no 587 translations, resources, or substitutions. In this case it may 588 be reasonable to replace it with a raw string value, perform 589 string manipulation on it, etc. 590 """ 591 return bool('v' in self.args and not self.args.get('s', [])) 592 593 def _get_json(self) -> str: 594 try: 595 return json.dumps(self.args, separators=(',', ':')) 596 except Exception: 597 from babase import _error 598 599 _error.print_exception('_get_json failed for', self.args) 600 return 'JSON_ERR' 601 602 def __str__(self) -> str: 603 return '<ba.Lstr: ' + self._get_json() + '>' 604 605 def __repr__(self) -> str: 606 return '<ba.Lstr: ' + self._get_json() + '>' 607 608 @staticmethod 609 def from_json(json_string: str) -> babase.Lstr: 610 """Given a json string, returns a babase.Lstr. Does no validation.""" 611 lstr = Lstr(value='') 612 lstr.args = json.loads(json_string) 613 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'))])
508 def __init__(self, *args: Any, **keywds: Any) -> None: 509 """Instantiate a Lstr. 510 511 Pass a value for either 'resource', 'translate', 512 or 'value'. (see Lstr help for examples). 513 'subs' can be a sequence of 2-member sequences consisting of values 514 and replacements. 515 'fallback_resource' can be a resource key that will be used if the 516 main one is not present for 517 the current language in place of falling back to the english value 518 ('resource' mode only). 519 'fallback_value' can be a literal string that will be used if neither 520 the resource nor the fallback resource is found ('resource' mode only). 521 """ 522 # pylint: disable=too-many-branches 523 if args: 524 raise TypeError('Lstr accepts only keyword arguments') 525 526 # Basically just store the exact args they passed. 527 # However if they passed any Lstr values for subs, 528 # replace them with that Lstr's dict. 529 self.args = keywds 530 our_type = type(self) 531 532 if isinstance(self.args.get('value'), our_type): 533 raise TypeError("'value' must be a regular string; not an Lstr") 534 535 if 'subs' in self.args: 536 subs_new = [] 537 for key, value in keywds['subs']: 538 if isinstance(value, our_type): 539 subs_new.append((key, value.args)) 540 else: 541 subs_new.append((key, value)) 542 self.args['subs'] = subs_new 543 544 # As of protocol 31 we support compact key names 545 # ('t' instead of 'translate', etc). Convert as needed. 546 if 'translate' in keywds: 547 keywds['t'] = keywds['translate'] 548 del keywds['translate'] 549 if 'resource' in keywds: 550 keywds['r'] = keywds['resource'] 551 del keywds['resource'] 552 if 'value' in keywds: 553 keywds['v'] = keywds['value'] 554 del keywds['value'] 555 if 'fallback' in keywds: 556 from babase import _error 557 558 _error.print_error( 559 'deprecated "fallback" arg passed to Lstr(); use ' 560 'either "fallback_resource" or "fallback_value"', 561 once=True, 562 ) 563 keywds['f'] = keywds['fallback'] 564 del keywds['fallback'] 565 if 'fallback_resource' in keywds: 566 keywds['f'] = keywds['fallback_resource'] 567 del keywds['fallback_resource'] 568 if 'subs' in keywds: 569 keywds['s'] = keywds['subs'] 570 del keywds['subs'] 571 if 'fallback_value' in keywds: 572 keywds['fv'] = keywds['fallback_value'] 573 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).
575 def evaluate(self) -> str: 576 """Evaluate the Lstr and returns a flat string in the current language. 577 578 You should avoid doing this as much as possible and instead pass 579 and store Lstr values. 580 """ 581 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.
583 def is_flat_value(self) -> bool: 584 """Return whether the Lstr is a 'flat' value. 585 586 This is defined as a simple string value incorporating no 587 translations, resources, or substitutions. In this case it may 588 be reasonable to replace it with a raw string value, perform 589 string manipulation on it, etc. 590 """ 591 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
453def open_url(address: str, force_internal: bool = False) -> None: 454 """Open a provided URL. 455 456 Category: **General Utility Functions** 457 458 Open the provided url in a web-browser, or display the URL 459 string in a window if that isn't possible (or if force_internal 460 is True). 461 """ 462 return None
Open a provided URL.
Category: General Utility Functions
Open the provided url in a web-browser, or display the URL string in a window if that isn't possible (or if force_internal is True).
101class Permission(Enum): 102 """Permissions that can be requested from the OS. 103 104 Category: Enums 105 """ 106 107 STORAGE = 0
Permissions that can be requested from the OS.
Category: Enums
Inherited Members
- enum.Enum
- name
- value
317class Plugin: 318 """A plugin to alter app behavior in some way. 319 320 Category: **App Classes** 321 322 Plugins are discoverable by the meta-tag system 323 and the user can select which ones they want to enable. 324 Enabled plugins are then called at specific times as the 325 app is running in order to modify its behavior in some way. 326 """ 327 328 def on_app_running(self) -> None: 329 """Called when the app reaches the running state.""" 330 331 def on_app_pause(self) -> None: 332 """Called when the app is switching to a paused state.""" 333 334 def on_app_resume(self) -> None: 335 """Called when the app is resuming from a paused state.""" 336 337 def on_app_shutdown(self) -> None: 338 """Called when the app is beginning the shutdown process.""" 339 340 def on_app_shutdown_complete(self) -> None: 341 """Called when the app has completed the shutdown process.""" 342 343 def has_settings_ui(self) -> bool: 344 """Called to ask if we have settings UI we can show.""" 345 return False 346 347 def show_settings_ui(self, source_widget: Any | None) -> None: 348 """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.
337 def on_app_shutdown(self) -> None: 338 """Called when the app is beginning the shutdown process."""
Called when the app is beginning the shutdown process.
340 def on_app_shutdown_complete(self) -> None: 341 """Called when the app has completed the shutdown process."""
Called when the app has completed the shutdown process.
222class PluginSpec: 223 """Represents a plugin the engine knows about. 224 225 Category: **App Classes** 226 227 The 'enabled' attr represents whether this plugin is set to load. 228 Getting or setting that attr affects the corresponding app-config 229 key. Remember to commit the app-config after making any changes. 230 231 The 'attempted_load' attr will be True if the engine has attempted 232 to load the plugin. If 'attempted_load' is True for a PluginSpec 233 but the 'plugin' attr is None, it means there was an error loading 234 the plugin. If a plugin's api-version does not match the running 235 app, if a new plugin is detected with auto-enable-plugins disabled, 236 or if the user has explicitly disabled a plugin, the engine will not 237 even attempt to load it. 238 """ 239 240 def __init__(self, class_path: str, loadable: bool): 241 self.class_path = class_path 242 self.loadable = loadable 243 self.attempted_load = False 244 self.plugin: Plugin | None = None 245 246 @property 247 def enabled(self) -> bool: 248 """Whether the user wants this plugin to load.""" 249 plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {}) 250 assert isinstance(plugstates, dict) 251 val = plugstates.get(self.class_path, {}).get('enabled', False) is True 252 return val 253 254 @enabled.setter 255 def enabled(self, val: bool) -> None: 256 plugstates: dict[str, dict] = _babase.app.config.setdefault( 257 'Plugins', {} 258 ) 259 assert isinstance(plugstates, dict) 260 plugstate = plugstates.setdefault(self.class_path, {}) 261 plugstate['enabled'] = val 262 263 def attempt_load_if_enabled(self) -> Plugin | None: 264 """Possibly load the plugin and log any errors.""" 265 from babase._general import getclass 266 from babase._language import Lstr 267 268 assert not self.attempted_load 269 assert self.plugin is None 270 271 if not self.enabled: 272 return None 273 self.attempted_load = True 274 if not self.loadable: 275 return None 276 try: 277 cls = getclass(self.class_path, Plugin) 278 except Exception as exc: 279 _babase.getsimplesound('error').play() 280 _babase.screenmessage( 281 Lstr( 282 resource='pluginClassLoadErrorText', 283 subs=[ 284 ('${PLUGIN}', self.class_path), 285 ('${ERROR}', str(exc)), 286 ], 287 ), 288 color=(1, 0, 0), 289 ) 290 logging.exception( 291 "Error loading plugin class '%s'.", self.class_path 292 ) 293 return None 294 try: 295 self.plugin = cls() 296 return self.plugin 297 except Exception as exc: 298 from babase import _error 299 300 _babase.getsimplesound('error').play() 301 _babase.screenmessage( 302 Lstr( 303 resource='pluginInitErrorText', 304 subs=[ 305 ('${PLUGIN}', self.class_path), 306 ('${ERROR}', str(exc)), 307 ], 308 ), 309 color=(1, 0, 0), 310 ) 311 logging.exception( 312 "Error initing plugin class: '%s'.", self.class_path 313 ) 314 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.
263 def attempt_load_if_enabled(self) -> Plugin | None: 264 """Possibly load the plugin and log any errors.""" 265 from babase._general import getclass 266 from babase._language import Lstr 267 268 assert not self.attempted_load 269 assert self.plugin is None 270 271 if not self.enabled: 272 return None 273 self.attempted_load = True 274 if not self.loadable: 275 return None 276 try: 277 cls = getclass(self.class_path, Plugin) 278 except Exception as exc: 279 _babase.getsimplesound('error').play() 280 _babase.screenmessage( 281 Lstr( 282 resource='pluginClassLoadErrorText', 283 subs=[ 284 ('${PLUGIN}', self.class_path), 285 ('${ERROR}', str(exc)), 286 ], 287 ), 288 color=(1, 0, 0), 289 ) 290 logging.exception( 291 "Error loading plugin class '%s'.", self.class_path 292 ) 293 return None 294 try: 295 self.plugin = cls() 296 return self.plugin 297 except Exception as exc: 298 from babase import _error 299 300 _babase.getsimplesound('error').play() 301 _babase.screenmessage( 302 Lstr( 303 resource='pluginInitErrorText', 304 subs=[ 305 ('${PLUGIN}', self.class_path), 306 ('${ERROR}', str(exc)), 307 ], 308 ), 309 color=(1, 0, 0), 310 ) 311 logging.exception( 312 "Error initing plugin class: '%s'.", self.class_path 313 ) 314 return None
Possibly load the plugin and log any errors.
1208def pushcall( 1209 call: Callable, 1210 from_other_thread: bool = False, 1211 suppress_other_thread_warning: bool = False, 1212 other_thread_use_fg_context: bool = False, 1213 raw: bool = False, 1214) -> None: 1215 """Push a call to the logic event-loop. 1216 Category: **General Utility Functions** 1217 1218 This call expects to be used in the logic thread, and will automatically 1219 save and restore the babase.Context to behave seamlessly. 1220 1221 If you want to push a call from outside of the logic thread, 1222 however, you can pass 'from_other_thread' as True. In this case 1223 the call will always run in the UI context_ref on the logic thread 1224 or whichever context_ref is in the foreground if 1225 other_thread_use_fg_context is True. 1226 Passing raw=True will disable thread checks and context_ref sets/restores. 1227 """ 1228 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.
1232def quit(soft: bool = True, back: bool = False) -> None: 1233 """Quit the app. 1234 1235 Category: **General Utility Functions** 1236 1237 On platforms such as mobile, a 'soft' quit may background and/or reset 1238 the app but keep it running. A 'back' quit is a special form of soft 1239 quit that may trigger different behavior in the OS. On Android, for 1240 example, a back-quit may simply jump to the previous Android activity, 1241 while a regular soft quit may just exit the current activity and dump 1242 the user at their home screen. 1243 """ 1244 return None
Quit the app.
Category: General Utility Functions
On platforms such as mobile, a 'soft' quit may background and/or reset the app but keep it running. A 'back' quit is a special form of soft quit that may trigger different behavior in the OS. On Android, for example, a back-quit may simply jump to the previous Android activity, while a regular soft quit may just exit the current activity and dump the user at their home screen.
465def rowwidget( 466 edit: bauiv1.Widget | None = None, 467 parent: bauiv1.Widget | None = None, 468 size: Sequence[float] | None = None, 469 position: Sequence[float] | None = None, 470 background: bool | None = None, 471 selected_child: bauiv1.Widget | None = None, 472 visible_child: bauiv1.Widget | None = None, 473 claims_left_right: bool | None = None, 474 claims_tab: bool | None = None, 475 selection_loops_to_parent: bool | None = None, 476) -> bauiv1.Widget: 477 """Create or edit a row widget. 478 479 Category: **User Interface Functions** 480 481 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 482 a new one is created and returned. Arguments that are not set to None 483 are applied to the Widget. 484 """ 485 import bauiv1 # pylint: disable=cyclic-import 486 487 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.
1282def safecolor( 1283 color: Sequence[float], target_intensity: float = 0.6 1284) -> tuple[float, ...]: 1285 """Given a color tuple, return a color safe to display as text. 1286 1287 Category: **General Utility Functions** 1288 1289 Accepts tuples of length 3 or 4. This will slightly brighten very 1290 dark colors, etc. 1291 """ 1292 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.
1295def screenmessage( 1296 message: str | babase.Lstr, 1297 color: Sequence[float] | None = None, 1298 log: bool = False, 1299) -> None: 1300 """Print a message to the local client's screen, in a given color. 1301 1302 Category: **General Utility Functions** 1303 1304 Note that this version of the function is purely for local display. 1305 To broadcast screen messages in network play, look for methods such as 1306 broadcastmessage() provided by the scene-version packages. 1307 """ 1308 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.
490def scrollwidget( 491 edit: bauiv1.Widget | None = None, 492 parent: bauiv1.Widget | None = None, 493 size: Sequence[float] | None = None, 494 position: Sequence[float] | None = None, 495 background: bool | None = None, 496 selected_child: bauiv1.Widget | None = None, 497 capture_arrows: bool = False, 498 on_select_call: Callable | None = None, 499 center_small_content: bool | None = None, 500 color: Sequence[float] | None = None, 501 highlight: bool | None = None, 502 border_opacity: float | None = None, 503 simple_culling_v: float | None = None, 504 selection_loops_to_parent: bool | None = None, 505 claims_left_right: bool | None = None, 506 claims_up_down: bool | None = None, 507 claims_tab: bool | None = None, 508 autoselect: bool | None = None, 509) -> bauiv1.Widget: 510 """Create or edit a scroll widget. 511 512 Category: **User Interface Functions** 513 514 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 515 a new one is created and returned. Arguments that are not set to None 516 are applied to the Widget. 517 """ 518 import bauiv1 # pylint: disable=cyclic-import 519 520 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.
1311def set_analytics_screen(screen: str) -> None: 1312 """Used for analytics to see where in the app players spend their time. 1313 1314 Category: **General Utility Functions** 1315 1316 Generally called when opening a new window or entering some UI. 1317 'screen' should be a string description of an app location 1318 ('Main Menu', etc.) 1319 """ 1320 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
110class SpecialChar(Enum): 111 """Special characters the game can print. 112 113 Category: Enums 114 """ 115 116 DOWN_ARROW = 0 117 UP_ARROW = 1 118 LEFT_ARROW = 2 119 RIGHT_ARROW = 3 120 TOP_BUTTON = 4 121 LEFT_BUTTON = 5 122 RIGHT_BUTTON = 6 123 BOTTOM_BUTTON = 7 124 DELETE = 8 125 SHIFT = 9 126 BACK = 10 127 LOGO_FLAT = 11 128 REWIND_BUTTON = 12 129 PLAY_PAUSE_BUTTON = 13 130 FAST_FORWARD_BUTTON = 14 131 DPAD_CENTER_BUTTON = 15 132 OUYA_BUTTON_O = 16 133 OUYA_BUTTON_U = 17 134 OUYA_BUTTON_Y = 18 135 OUYA_BUTTON_A = 19 136 OUYA_LOGO = 20 137 LOGO = 21 138 TICKET = 22 139 GOOGLE_PLAY_GAMES_LOGO = 23 140 GAME_CENTER_LOGO = 24 141 DICE_BUTTON1 = 25 142 DICE_BUTTON2 = 26 143 DICE_BUTTON3 = 27 144 DICE_BUTTON4 = 28 145 GAME_CIRCLE_LOGO = 29 146 PARTY_ICON = 30 147 TEST_ACCOUNT = 31 148 TICKET_BACKING = 32 149 TROPHY1 = 33 150 TROPHY2 = 34 151 TROPHY3 = 35 152 TROPHY0A = 36 153 TROPHY0B = 37 154 TROPHY4 = 38 155 LOCAL_ACCOUNT = 39 156 EXPLODINARY_LOGO = 40 157 FLAG_UNITED_STATES = 41 158 FLAG_MEXICO = 42 159 FLAG_GERMANY = 43 160 FLAG_BRAZIL = 44 161 FLAG_RUSSIA = 45 162 FLAG_CHINA = 46 163 FLAG_UNITED_KINGDOM = 47 164 FLAG_CANADA = 48 165 FLAG_INDIA = 49 166 FLAG_JAPAN = 50 167 FLAG_FRANCE = 51 168 FLAG_INDONESIA = 52 169 FLAG_ITALY = 53 170 FLAG_SOUTH_KOREA = 54 171 FLAG_NETHERLANDS = 55 172 FEDORA = 56 173 HAL = 57 174 CROWN = 58 175 YIN_YANG = 59 176 EYE_BALL = 60 177 SKULL = 61 178 HEART = 62 179 DRAGON = 63 180 HELMET = 64 181 MUSHROOM = 65 182 NINJA_STAR = 66 183 VIKING_HELMET = 67 184 MOON = 68 185 SPIDER = 69 186 FIREBALL = 70 187 FLAG_UNITED_ARAB_EMIRATES = 71 188 FLAG_QATAR = 72 189 FLAG_EGYPT = 73 190 FLAG_KUWAIT = 74 191 FLAG_ALGERIA = 75 192 FLAG_SAUDI_ARABIA = 76 193 FLAG_MALAYSIA = 77 194 FLAG_CZECH_REPUBLIC = 78 195 FLAG_AUSTRALIA = 79 196 FLAG_SINGAPORE = 80 197 OCULUS_LOGO = 81 198 STEAM_LOGO = 82 199 NVIDIA_LOGO = 83 200 FLAG_IRAN = 84 201 FLAG_POLAND = 85 202 FLAG_ARGENTINA = 86 203 FLAG_PHILIPPINES = 87 204 FLAG_CHILE = 88 205 MIKIROG = 89 206 V2_LOGO = 90
Special characters the game can print.
Category: Enums
Inherited Members
- enum.Enum
- name
- value
Category: User Interface Classes
556def textwidget( 557 edit: bauiv1.Widget | None = None, 558 parent: bauiv1.Widget | None = None, 559 size: Sequence[float] | None = None, 560 position: Sequence[float] | None = None, 561 text: str | bauiv1.Lstr | None = None, 562 v_align: str | None = None, 563 h_align: str | None = None, 564 editable: bool | None = None, 565 padding: float | None = None, 566 on_return_press_call: Callable[[], None] | None = None, 567 on_activate_call: Callable[[], None] | None = None, 568 selectable: bool | None = None, 569 query: bauiv1.Widget | None = None, 570 max_chars: int | None = None, 571 color: Sequence[float] | None = None, 572 click_activate: bool | None = None, 573 on_select_call: Callable[[], None] | None = None, 574 always_highlight: bool | None = None, 575 draw_controller: bauiv1.Widget | None = None, 576 scale: float | None = None, 577 corner_scale: float | None = None, 578 description: str | bauiv1.Lstr | None = None, 579 transition_delay: float | None = None, 580 maxwidth: float | None = None, 581 max_height: float | None = None, 582 flatness: float | None = None, 583 shadow: float | None = None, 584 autoselect: bool | None = None, 585 rotate: float | None = None, 586 enabled: bool | None = None, 587 force_internal_editing: bool | None = None, 588 always_show_carat: bool | None = None, 589 big: bool | None = None, 590 extra_touch_border_scale: float | None = None, 591 res_scale: float | None = None, 592 query_max_chars: bauiv1.Widget | None = None, 593 query_description: bauiv1.Widget | None = None, 594 adapter_finished: bool | None = None, 595) -> bauiv1.Widget: 596 """Create or edit a text widget. 597 598 Category: **User Interface Functions** 599 600 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 601 a new one is created and returned. Arguments that are not set to None 602 are applied to the Widget. 603 """ 604 import bauiv1 # pylint: disable=cyclic-import 605 606 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.
175def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None: 176 """Add a check to ensure a widget-owning object gets cleaned up properly. 177 178 Category: User Interface Functions 179 180 This adds a check which will print an error message if the provided 181 object still exists ~5 seconds after the provided bauiv1.Widget dies. 182 183 This is a good sanity check for any sort of object that wraps or 184 controls a bauiv1.Widget. For instance, a 'Window' class instance has 185 no reason to still exist once its root container bauiv1.Widget has fully 186 transitioned out and been destroyed. Circular references or careless 187 strong referencing can lead to such objects never getting destroyed, 188 however, and this helps detect such cases to avoid memory leaks. 189 """ 190 if DEBUG_UI_CLEANUP_CHECKS: 191 print(f'adding uicleanup to {obj}') 192 if not isinstance(widget, _bauiv1.Widget): 193 raise TypeError('widget arg is not a bauiv1.Widget') 194 195 if bool(False): 196 197 def foobar() -> None: 198 """Just testing.""" 199 if DEBUG_UI_CLEANUP_CHECKS: 200 print('uicleanupcheck widget dying...') 201 202 widget.add_delete_callback(foobar) 203 204 assert babase.app.classic is not None 205 babase.app.ui_v1.cleanupchecks.append( 206 UICleanupCheck( 207 obj=weakref.ref(obj), widget=widget, widget_death_time=None 208 ) 209 )
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.
42class UIScale(Enum): 43 """The overall scale the UI is being rendered for. Note that this is 44 independent of pixel resolution. For example, a phone and a desktop PC 45 might render the game at similar pixel resolutions but the size they 46 display content at will vary significantly. 47 48 Category: Enums 49 50 'large' is used for devices such as desktop PCs where fine details can 51 be clearly seen. UI elements are generally smaller on the screen 52 and more content can be seen at once. 53 54 'medium' is used for devices such as tablets, TVs, or VR headsets. 55 This mode strikes a balance between clean readability and amount of 56 content visible. 57 58 'small' is used primarily for phones or other small devices where 59 content needs to be presented as large and clear in order to remain 60 readable from an average distance. 61 """ 62 63 LARGE = 0 64 MEDIUM = 1 65 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
21class UIV1Subsystem(babase.AppSubsystem): 22 """Consolidated UI functionality for the app. 23 24 Category: **App Classes** 25 26 To use this class, access the single instance of it at 'ba.app.ui'. 27 """ 28 29 def __init__(self) -> None: 30 super().__init__() 31 env = babase.env() 32 33 self.controller: UIController | None = None 34 35 self._main_menu_window: bauiv1.Widget | None = None 36 self._main_menu_location: str | None = None 37 self.quit_window: bauiv1.Widget | None = None 38 39 # From classic. 40 self.main_menu_resume_callbacks: list = [] # Can probably go away. 41 42 self._uiscale: babase.UIScale 43 44 interfacetype = env['ui_scale'] 45 if interfacetype == 'large': 46 self._uiscale = babase.UIScale.LARGE 47 elif interfacetype == 'medium': 48 self._uiscale = babase.UIScale.MEDIUM 49 elif interfacetype == 'small': 50 self._uiscale = babase.UIScale.SMALL 51 else: 52 raise RuntimeError(f'Invalid UIScale value: {interfacetype}') 53 54 self.window_states: dict[type, Any] = {} # FIXME: Kill this. 55 self.main_menu_selection: str | None = None # FIXME: Kill this. 56 self.have_party_queue_window = False 57 self.cleanupchecks: list[UICleanupCheck] = [] 58 self.upkeeptimer: babase.AppTimer | None = None 59 self.use_toolbars = _bauiv1.toolbar_test() 60 61 self.title_color = (0.72, 0.7, 0.75) 62 self.heading_color = (0.72, 0.7, 0.75) 63 self.infotextcolor = (0.7, 0.9, 0.7) 64 65 # Switch our overall game selection UI flow between Play and 66 # Private-party playlist selection modes; should do this in 67 # a more elegant way once we revamp high level UI stuff a bit. 68 self.selecting_private_party_playlist: bool = False 69 70 @property 71 def uiscale(self) -> babase.UIScale: 72 """Current ui scale for the app.""" 73 return self._uiscale 74 75 def on_app_loading(self) -> None: 76 from bauiv1._uitypes import UIController, ui_upkeep 77 78 # IMPORTANT: If tweaking UI stuff, make sure it behaves for small, 79 # medium, and large UI modes. (doesn't run off screen, etc). 80 # The overrides below can be used to test with different sizes. 81 # Generally small is used on phones, medium is used on tablets/tvs, 82 # and large is on desktop computers or perhaps large tablets. When 83 # possible, run in windowed mode and resize the window to assure 84 # this holds true at all aspect ratios. 85 86 # UPDATE: A better way to test this is now by setting the environment 87 # variable BA_UI_SCALE to "small", "medium", or "large". 88 # This will affect system UIs not covered by the values below such 89 # as screen-messages. The below values remain functional, however, 90 # for cases such as Android where environment variables can't be set 91 # easily. 92 93 if bool(False): # force-test ui scale 94 self._uiscale = babase.UIScale.SMALL 95 with babase.ContextRef.empty(): 96 babase.pushcall( 97 lambda: babase.screenmessage( 98 f'FORCING UISCALE {self._uiscale.name} FOR TESTING', 99 color=(1, 0, 1), 100 log=True, 101 ) 102 ) 103 104 self.controller = UIController() 105 106 # Kick off our periodic UI upkeep. 107 # FIXME: Can probably kill this if we do immediate UI death checks. 108 self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True) 109 110 def set_main_menu_window(self, window: bauiv1.Widget) -> None: 111 """Set the current 'main' window, replacing any existing.""" 112 existing = self._main_menu_window 113 from inspect import currentframe, getframeinfo 114 115 # Let's grab the location where we were called from to report 116 # if we have to force-kill the existing window (which normally 117 # should not happen). 118 frameline = None 119 try: 120 frame = currentframe() 121 if frame is not None: 122 frame = frame.f_back 123 if frame is not None: 124 frameinfo = getframeinfo(frame) 125 frameline = f'{frameinfo.filename} {frameinfo.lineno}' 126 except Exception: 127 logging.exception('Error calcing line for set_main_menu_window') 128 129 # With our legacy main-menu system, the caller is responsible for 130 # clearing out the old main menu window when assigning the new. 131 # However there are corner cases where that doesn't happen and we get 132 # old windows stuck under the new main one. So let's guard against 133 # that. However, we can't simply delete the existing main window when 134 # a new one is assigned because the user may transition the old out 135 # *after* the assignment. Sigh. So, as a happy medium, let's check in 136 # on the old after a short bit of time and kill it if its still alive. 137 # That will be a bit ugly on screen but at least should un-break 138 # things. 139 def _delay_kill() -> None: 140 import time 141 142 if existing: 143 print( 144 f'Killing old main_menu_window' 145 f' when called at: {frameline} t={time.time():.3f}' 146 ) 147 existing.delete() 148 149 babase.apptimer(1.0, _delay_kill) 150 self._main_menu_window = window 151 152 def clear_main_menu_window(self, transition: str | None = None) -> None: 153 """Clear any existing 'main' window with the provided transition.""" 154 if self._main_menu_window: 155 if transition is not None: 156 _bauiv1.containerwidget( 157 edit=self._main_menu_window, transition=transition 158 ) 159 else: 160 self._main_menu_window.delete() 161 162 def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: 163 """(internal)""" 164 165 # If there's no main menu up, just call immediately. 166 if not self.has_main_menu_window(): 167 with babase.ContextRef.empty(): 168 call() 169 else: 170 self.main_menu_resume_callbacks.append(call) 171 172 def has_main_menu_window(self) -> bool: 173 """Return whether a main menu window is present.""" 174 return bool(self._main_menu_window) 175 176 def set_main_menu_location(self, location: str) -> None: 177 """Set the location represented by the current main menu window.""" 178 self._main_menu_location = location 179 180 def get_main_menu_location(self) -> str | None: 181 """Return the current named main menu location, if any.""" 182 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 def on_app_loading(self) -> None: 76 from bauiv1._uitypes import UIController, ui_upkeep 77 78 # IMPORTANT: If tweaking UI stuff, make sure it behaves for small, 79 # medium, and large UI modes. (doesn't run off screen, etc). 80 # The overrides below can be used to test with different sizes. 81 # Generally small is used on phones, medium is used on tablets/tvs, 82 # and large is on desktop computers or perhaps large tablets. When 83 # possible, run in windowed mode and resize the window to assure 84 # this holds true at all aspect ratios. 85 86 # UPDATE: A better way to test this is now by setting the environment 87 # variable BA_UI_SCALE to "small", "medium", or "large". 88 # This will affect system UIs not covered by the values below such 89 # as screen-messages. The below values remain functional, however, 90 # for cases such as Android where environment variables can't be set 91 # easily. 92 93 if bool(False): # force-test ui scale 94 self._uiscale = babase.UIScale.SMALL 95 with babase.ContextRef.empty(): 96 babase.pushcall( 97 lambda: babase.screenmessage( 98 f'FORCING UISCALE {self._uiscale.name} FOR TESTING', 99 color=(1, 0, 1), 100 log=True, 101 ) 102 ) 103 104 self.controller = UIController() 105 106 # Kick off our periodic UI upkeep. 107 # FIXME: Can probably kill this if we do immediate UI death checks. 108 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_pause
- on_app_resume
- on_app_shutdown
- on_app_shutdown_complete
- do_apply_app_config
625def widget( 626 edit: bauiv1.Widget | None = None, 627 up_widget: bauiv1.Widget | None = None, 628 down_widget: bauiv1.Widget | None = None, 629 left_widget: bauiv1.Widget | None = None, 630 right_widget: bauiv1.Widget | None = None, 631 show_buffer_top: float | None = None, 632 show_buffer_bottom: float | None = None, 633 show_buffer_left: float | None = None, 634 show_buffer_right: float | None = None, 635 autoselect: bool | None = None, 636) -> None: 637 """Edit common attributes of any widget. 638 639 Category: **User Interface Functions** 640 641 Unlike other UI calls, this can only be used to edit, not to create. 642 """ 643 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 babase.buttonwidget() must be used to instantiate or edit these. 83 """ 84 85 def __bool__(self) -> bool: 86 """Support for bool evaluation.""" 87 return bool(True) # Slight obfuscation. 88 89 def activate(self) -> None: 90 """Activates a widget; the same as if it had been clicked.""" 91 return None 92 93 def add_delete_callback(self, call: Callable) -> None: 94 """Add a call to be run immediately after this widget is destroyed.""" 95 return None 96 97 def delete(self, ignore_missing: bool = True) -> None: 98 """Delete the Widget. Ignores already-deleted Widgets if ignore_missing 99 is True; otherwise an Exception is thrown. 100 """ 101 return None 102 103 def exists(self) -> bool: 104 """Returns whether the Widget still exists. 105 Most functionality will fail on a nonexistent widget. 106 107 Note that you can also use the boolean operator for this same 108 functionality, so a statement such as "if mywidget" will do 109 the right thing both for Widget objects and values of None. 110 """ 111 return bool() 112 113 def get_children(self) -> list[bauiv1.Widget]: 114 """Returns any child Widgets of this Widget.""" 115 import bauiv1 116 117 return [bauiv1.Widget()] 118 119 def get_screen_space_center(self) -> tuple[float, float]: 120 """Returns the coords of the bauiv1.Widget center relative to the center 121 of the screen. This can be useful for placing pop-up windows and other 122 special cases. 123 """ 124 return (0.0, 0.0) 125 126 def get_selected_child(self) -> bauiv1.Widget | None: 127 """Returns the selected child Widget or None if nothing is selected.""" 128 import bauiv1 129 130 return bauiv1.Widget() 131 132 def get_widget_type(self) -> str: 133 """Return the internal type of the Widget as a string. Note that this 134 is different from the Python bauiv1.Widget type, which is the same for 135 all widgets. 136 """ 137 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 babase.buttonwidget() must be used to instantiate or edit these.
89 def activate(self) -> None: 90 """Activates a widget; the same as if it had been clicked.""" 91 return None
Activates a widget; the same as if it had been clicked.
93 def add_delete_callback(self, call: Callable) -> None: 94 """Add a call to be run immediately after this widget is destroyed.""" 95 return None
Add a call to be run immediately after this widget is destroyed.
97 def delete(self, ignore_missing: bool = True) -> None: 98 """Delete the Widget. Ignores already-deleted Widgets if ignore_missing 99 is True; otherwise an Exception is thrown. 100 """ 101 return None
Delete the Widget. Ignores already-deleted Widgets if ignore_missing is True; otherwise an Exception is thrown.
103 def exists(self) -> bool: 104 """Returns whether the Widget still exists. 105 Most functionality will fail on a nonexistent widget. 106 107 Note that you can also use the boolean operator for this same 108 functionality, so a statement such as "if mywidget" will do 109 the right thing both for Widget objects and values of None. 110 """ 111 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.
113 def get_children(self) -> list[bauiv1.Widget]: 114 """Returns any child Widgets of this Widget.""" 115 import bauiv1 116 117 return [bauiv1.Widget()]
Returns any child Widgets of this Widget.
119 def get_screen_space_center(self) -> tuple[float, float]: 120 """Returns the coords of the bauiv1.Widget center relative to the center 121 of the screen. This can be useful for placing pop-up windows and other 122 special cases. 123 """ 124 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.
126 def get_selected_child(self) -> bauiv1.Widget | None: 127 """Returns the selected child Widget or None if nothing is selected.""" 128 import bauiv1 129 130 return bauiv1.Widget()
Returns the selected child Widget or None if nothing is selected.
132 def get_widget_type(self) -> str: 133 """Return the internal type of the Widget as a string. Note that this 134 is different from the Python bauiv1.Widget type, which is the same for 135 all widgets. 136 """ 137 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