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