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