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 App, 25 AppIntent, 26 AppIntentDefault, 27 AppIntentExec, 28 AppMode, 29 appname, 30 appnameupper, 31 apptime, 32 AppTime, 33 apptimer, 34 AppTimer, 35 Call, 36 fullscreen_control_available, 37 fullscreen_control_get, 38 fullscreen_control_key_shortcut, 39 fullscreen_control_set, 40 charstr, 41 clipboard_is_supported, 42 clipboard_set_text, 43 commit_app_config, 44 ContextRef, 45 displaytime, 46 DisplayTime, 47 displaytimer, 48 DisplayTimer, 49 do_once, 50 existing, 51 fade_screen, 52 get_display_resolution, 53 get_input_idle_time, 54 get_ip_address_type, 55 get_low_level_config_value, 56 get_max_graphics_quality, 57 get_remote_app_name, 58 get_replays_dir, 59 get_string_height, 60 get_string_width, 61 get_type_name, 62 get_virtual_safe_area_size, 63 get_virtual_screen_size, 64 getclass, 65 have_permission, 66 in_logic_thread, 67 in_main_menu, 68 increment_analytics_count, 69 is_browser_likely_available, 70 is_xcode_build, 71 lock_all_input, 72 LoginAdapter, 73 LoginInfo, 74 Lstr, 75 native_review_request, 76 native_review_request_supported, 77 NotFoundError, 78 open_file_externally, 79 open_url, 80 overlay_web_browser_close, 81 overlay_web_browser_is_open, 82 overlay_web_browser_is_supported, 83 overlay_web_browser_open_url, 84 Permission, 85 Plugin, 86 PluginSpec, 87 pushcall, 88 quit, 89 QuitType, 90 request_permission, 91 safecolor, 92 screenmessage, 93 set_analytics_screen, 94 set_low_level_config_value, 95 set_ui_input_device, 96 SpecialChar, 97 supports_max_fps, 98 supports_vsync, 99 supports_unicode_display, 100 timestring, 101 UIScale, 102 unlock_all_input, 103 utc_now_cloud, 104 WeakCall, 105 workspaces_in_use, 106) 107 108from _bauiv1 import ( 109 buttonwidget, 110 checkboxwidget, 111 columnwidget, 112 containerwidget, 113 get_qrcode_texture, 114 get_special_widget, 115 getmesh, 116 getsound, 117 gettexture, 118 hscrollwidget, 119 imagewidget, 120 Mesh, 121 root_ui_pause_updates, 122 root_ui_resume_updates, 123 rowwidget, 124 scrollwidget, 125 set_party_window_open, 126 spinnerwidget, 127 Sound, 128 Texture, 129 textwidget, 130 uibounds, 131 Widget, 132 widget, 133) 134from bauiv1._keyboard import Keyboard 135from bauiv1._uitypes import ( 136 Window, 137 MainWindowState, 138 BasicMainWindowState, 139 uicleanupcheck, 140 MainWindow, 141 RootUIUpdatePause, 142) 143from bauiv1._appsubsystem import UIV1AppSubsystem 144 145__all__ = [ 146 'add_clean_frame_callback', 147 'allows_ticket_sales', 148 'app', 149 'App', 150 'AppIntent', 151 'AppIntentDefault', 152 'AppIntentExec', 153 'AppMode', 154 'appname', 155 'appnameupper', 156 'appnameupper', 157 'apptime', 158 'AppTime', 159 'apptimer', 160 'AppTimer', 161 'BasicMainWindowState', 162 'buttonwidget', 163 'Call', 164 'fullscreen_control_available', 165 'fullscreen_control_get', 166 'fullscreen_control_key_shortcut', 167 'fullscreen_control_set', 168 'charstr', 169 'checkboxwidget', 170 'clipboard_is_supported', 171 'clipboard_set_text', 172 'columnwidget', 173 'commit_app_config', 174 'containerwidget', 175 'ContextRef', 176 'displaytime', 177 'DisplayTime', 178 'displaytimer', 179 'DisplayTimer', 180 'do_once', 181 'existing', 182 'fade_screen', 183 'get_display_resolution', 184 'get_input_idle_time', 185 'get_ip_address_type', 186 'get_low_level_config_value', 187 'get_max_graphics_quality', 188 'get_qrcode_texture', 189 'get_remote_app_name', 190 'get_replays_dir', 191 'get_special_widget', 192 'get_string_height', 193 'get_string_width', 194 'get_type_name', 195 'get_virtual_safe_area_size', 196 'get_virtual_screen_size', 197 'getclass', 198 'getmesh', 199 'getsound', 200 'gettexture', 201 'have_permission', 202 'hscrollwidget', 203 'imagewidget', 204 'in_logic_thread', 205 'in_main_menu', 206 'increment_analytics_count', 207 'is_browser_likely_available', 208 'is_xcode_build', 209 'Keyboard', 210 'lock_all_input', 211 'LoginAdapter', 212 'LoginInfo', 213 'Lstr', 214 'MainWindow', 215 'MainWindowState', 216 'Mesh', 217 'native_review_request', 218 'native_review_request_supported', 219 'NotFoundError', 220 'open_file_externally', 221 'open_url', 222 'overlay_web_browser_close', 223 'overlay_web_browser_is_open', 224 'overlay_web_browser_is_supported', 225 'overlay_web_browser_open_url', 226 'Permission', 227 'Plugin', 228 'PluginSpec', 229 'pushcall', 230 'quit', 231 'QuitType', 232 'request_permission', 233 'root_ui_pause_updates', 234 'root_ui_resume_updates', 235 'RootUIUpdatePause', 236 'rowwidget', 237 'safecolor', 238 'screenmessage', 239 'scrollwidget', 240 'set_analytics_screen', 241 'set_low_level_config_value', 242 'set_party_window_open', 243 'set_ui_input_device', 244 'Sound', 245 'SpecialChar', 246 'spinnerwidget', 247 'supports_max_fps', 248 'supports_vsync', 249 'supports_unicode_display', 250 'Texture', 251 'textwidget', 252 'timestring', 253 'uibounds', 254 'uicleanupcheck', 255 'UIScale', 256 'UIV1AppSubsystem', 257 'unlock_all_input', 258 'utc_now_cloud', 259 'WeakCall', 260 'widget', 261 'Widget', 262 'Window', 263 'workspaces_in_use', 264] 265 266# We want stuff to show up as bauiv1.Foo instead of bauiv1._sub.Foo. 267set_canonical_module_names(globals()) 268 269# Sanity check: we want to keep ballistica's dependencies and 270# bootstrapping order clearly defined; let's check a few particular 271# modules to make sure they never directly or indirectly import us 272# before their own execs complete. 273if __debug__: 274 for _mdl in 'babase', '_babase': 275 if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): 276 logging.warning( 277 '%s was imported before %s finished importing;' 278 ' should not happen.', 279 __name__, 280 _mdl, 281 )
:meta private:
52class App: 53 """High level Ballistica app functionality and state. 54 55 Access the single shared instance of this class via the "app" attr 56 available on various high level modules such as :mod:`bauiv1` and 57 :mod:`bascenev1`. 58 """ 59 60 # pylint: disable=too-many-public-methods 61 62 class State(Enum): 63 """High level state the app can be in.""" 64 65 #: The app has not yet begun starting and should not be used in 66 #: any way. 67 NOT_STARTED = 0 68 69 #: The native layer is spinning up its machinery (screens, 70 #: renderers, etc.). Nothing should happen in the Python layer 71 #: until this completes. 72 NATIVE_BOOTSTRAPPING = 1 73 74 #: Python app subsystems are being inited but should not yet 75 #: interact or do any work. 76 INITING = 2 77 78 #: Python app subsystems are inited and interacting, but the app 79 #: has not yet embarked on a high level course of action. It is 80 #: doing initial account logins, workspace & asset downloads, 81 #: etc. 82 LOADING = 3 83 84 #: All pieces are in place and the app is now doing its thing. 85 RUNNING = 4 86 87 #: Used on platforms such as mobile where the app basically needs 88 #: to shut down while backgrounded. In this state, all event 89 #: loops are suspended and all graphics and audio must cease 90 #: completely. Be aware that the suspended state can be entered 91 #: from any other state including NATIVE_BOOTSTRAPPING and 92 #: SHUTTING_DOWN. 93 SUSPENDED = 5 94 95 #: The app is shutting down. This process may involve sending 96 #: network messages or other things that can take up to a few 97 #: seconds, so ideally graphics and audio should remain 98 #: functional (with fades or spinners or whatever to show 99 #: something is happening). 100 SHUTTING_DOWN = 6 101 102 #: The app has completed shutdown. Any code running here should 103 #: be basically immediate. 104 SHUTDOWN_COMPLETE = 7 105 106 class DefaultAppModeSelector(AppModeSelector): 107 """Decides which AppMode to use to handle AppIntents. 108 109 This default version is generated by the project updater based 110 on the 'default_app_modes' value in the projectconfig. 111 112 It is also possible to modify app mode selection behavior by 113 setting app.mode_selector to an instance of a custom 114 AppModeSelector subclass. This is a good way to go if you are 115 modifying app behavior dynamically via a plugin instead of 116 statically in a spinoff project. 117 """ 118 119 @override 120 def app_mode_for_intent( 121 self, intent: AppIntent 122 ) -> type[AppMode] | None: 123 # pylint: disable=cyclic-import 124 125 # __DEFAULT_APP_MODE_SELECTION_BEGIN__ 126 # This section generated by batools.appmodule; do not edit. 127 128 # Ask our default app modes to handle it. 129 # (generated from 'default_app_modes' in projectconfig). 130 import baclassic 131 import babase 132 133 for appmode in [ 134 baclassic.ClassicAppMode, 135 babase.EmptyAppMode, 136 ]: 137 if appmode.can_handle_intent(intent): 138 return appmode 139 140 return None 141 142 # __DEFAULT_APP_MODE_SELECTION_END__ 143 144 # A few things defined as non-optional values but not actually 145 # available until the app starts. 146 plugins: PluginSubsystem 147 lang: LanguageSubsystem 148 health_monitor: AppHealthMonitor 149 150 # Define some other types here in the class-def so docs-generators 151 # are more likely to know about them. 152 config: AppConfig 153 env: babase.Env 154 state: State 155 threadpool: ThreadPoolExecutorPlus 156 meta: MetadataSubsystem 157 net: NetworkSubsystem 158 workspaces: WorkspaceSubsystem 159 components: AppComponentSubsystem 160 stringedit: StringEditSubsystem 161 devconsole: DevConsoleSubsystem 162 fg_state: int 163 164 #: How long we allow shutdown tasks to run before killing them. 165 #: Currently the entire app hard-exits if shutdown takes 15 seconds, 166 #: so we need to keep it under that. Staying above 10 should allow 167 #: 10 second network timeouts to happen though. 168 SHUTDOWN_TASK_TIMEOUT_SECONDS = 12 169 170 def __init__(self) -> None: 171 """(internal) 172 173 Do not instantiate this class. You can access the single shared 174 instance of it through various high level packages: 'babase.app', 175 'bascenev1.app', 'bauiv1.app', etc. 176 """ 177 178 # Hack for docs-generation: we can be imported with dummy modules 179 # instead of our actual binary ones, but we don't function. 180 if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1': 181 return 182 183 # Wrap our raw app config in our special wrapper and pass it to 184 # the native layer. 185 self.config = AppConfig(_babase.get_initial_app_config()) 186 _babase.set_app_config(self.config) 187 188 self.env = _babase.Env() 189 self.state = self.State.NOT_STARTED 190 191 # Default executor which can be used for misc background 192 # processing. It should also be passed to any additional asyncio 193 # loops we create so that everything shares the same single set 194 # of worker threads. 195 self.threadpool = ThreadPoolExecutorPlus( 196 thread_name_prefix='baworker', 197 initializer=self._thread_pool_thread_init, 198 ) 199 self.meta = MetadataSubsystem() 200 self.net = NetworkSubsystem() 201 self.workspaces = WorkspaceSubsystem() 202 self.components = AppComponentSubsystem() 203 self.stringedit = StringEditSubsystem() 204 self.devconsole = DevConsoleSubsystem() 205 206 # This is incremented any time the app is backgrounded or 207 # foregrounded; can be a simple way to determine if network data 208 # should be refreshed/etc. 209 self.fg_state = 0 210 211 self._subsystems: list[AppSubsystem] = [] 212 self._native_bootstrapping_completed = False 213 self._init_completed = False 214 self._meta_scan_completed = False 215 self._native_start_called = False 216 self._native_suspended = False 217 self._native_shutdown_called = False 218 self._native_shutdown_complete_called = False 219 self._initial_sign_in_completed = False 220 self._called_on_initing = False 221 self._called_on_loading = False 222 self._called_on_running = False 223 self._subsystem_registration_ended = False 224 self._pending_apply_app_config = False 225 self._asyncio_loop: asyncio.AbstractEventLoop | None = None 226 self._asyncio_tasks: set[asyncio.Task] = set() 227 self._asyncio_timer: babase.AppTimer | None = None 228 self._pending_intent: AppIntent | None = None 229 self._intent: AppIntent | None = None 230 self._mode_selector: babase.AppModeSelector | None = None 231 self._mode_instances: dict[type[AppMode], AppMode] = {} 232 self._mode: AppMode | None = None 233 self._shutdown_task: asyncio.Task[None] | None = None 234 self._shutdown_tasks: list[Coroutine[None, None, None]] = [ 235 self._wait_for_shutdown_suppressions(), 236 self._fade_and_shutdown_graphics(), 237 self._fade_and_shutdown_audio(), 238 ] 239 self._pool_thread_count = 0 240 241 # We hold a lock while lazy-loading our subsystem properties so 242 # we don't spin up any subsystem more than once, but the lock is 243 # recursive so that the subsystems can instantiate other 244 # subsystems. 245 self._subsystem_property_lock = RLock() 246 self._subsystem_property_data: dict[str, AppSubsystem | bool] = {} 247 248 def postinit(self) -> None: 249 """Called after we've been inited and assigned to babase.app. 250 251 Anything that accesses babase.app as part of its init process 252 must go here instead of __init__. 253 """ 254 255 # Hack for docs-generation: We can be imported with dummy 256 # modules instead of our actual binary ones, but we don't 257 # function. 258 if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1': 259 return 260 261 self.lang = LanguageSubsystem() 262 self.plugins = PluginSubsystem() 263 264 @property 265 def active(self) -> bool: 266 """Whether the app is currently front and center. 267 268 This will be False when the app is hidden, other activities 269 are covering it, etc. (depending on the platform). 270 """ 271 return _babase.app_is_active() 272 273 @property 274 def mode(self) -> AppMode | None: 275 """The app's current mode.""" 276 assert _babase.in_logic_thread() 277 return self._mode 278 279 @property 280 def asyncio_loop(self) -> asyncio.AbstractEventLoop: 281 """The logic thread's asyncio event loop. 282 283 This allow async tasks to be run in the logic thread. 284 285 Generally you should call App.create_async_task() to schedule 286 async code to run instead of using this directly. That will 287 handle retaining the task and logging errors automatically. 288 Only schedule tasks onto asyncio_loop yourself when you intend 289 to hold on to the returned task and await its results. Releasing 290 the task reference can lead to subtle bugs such as unreported 291 errors and garbage-collected tasks disappearing before their 292 work is done. 293 294 Note that, at this time, the asyncio loop is encapsulated 295 and explicitly stepped by the engine's logic thread loop and 296 thus things like asyncio.get_running_loop() will unintuitively 297 *not* return this loop from most places in the logic thread; 298 only from within a task explicitly created in this loop. 299 Hopefully this situation will be improved in the future with a 300 unified event loop. 301 """ 302 assert _babase.in_logic_thread() 303 assert self._asyncio_loop is not None 304 return self._asyncio_loop 305 306 def create_async_task( 307 self, coro: Coroutine[Any, Any, T], *, name: str | None = None 308 ) -> None: 309 """Create a fully managed async task. 310 311 This will automatically retain and release a reference to the task 312 and log any exceptions that occur in it. If you need to await a task 313 or otherwise need more control, schedule a task directly using 314 App.asyncio_loop. 315 """ 316 assert _babase.in_logic_thread() 317 318 # We hold a strong reference to the task until it is done. 319 # Otherwise it is possible for it to be garbage collected and 320 # disappear midway if the caller does not hold on to the 321 # returned task, which seems like a great way to introduce 322 # hard-to-track bugs. 323 task = self.asyncio_loop.create_task(coro, name=name) 324 self._asyncio_tasks.add(task) 325 task.add_done_callback(self._on_task_done) 326 327 def _on_task_done(self, task: asyncio.Task) -> None: 328 # Report any errors that occurred. 329 try: 330 exc = task.exception() 331 if exc is not None: 332 logging.error( 333 "Error in async task '%s'.", task.get_name(), exc_info=exc 334 ) 335 except Exception: 336 logging.exception('Error reporting async task error.') 337 338 self._asyncio_tasks.remove(task) 339 340 @property 341 def mode_selector(self) -> babase.AppModeSelector: 342 """Controls which app-modes are used for handling given intents. 343 344 Plugins can override this to change high level app behavior and 345 spinoff projects can change the default implementation for the 346 same effect. 347 """ 348 if self._mode_selector is None: 349 raise RuntimeError( 350 'mode_selector cannot be used until the app reaches' 351 ' the running state.' 352 ) 353 return self._mode_selector 354 355 @mode_selector.setter 356 def mode_selector(self, selector: babase.AppModeSelector) -> None: 357 self._mode_selector = selector 358 359 def _get_subsystem_property( 360 self, ssname: str, create_call: Callable[[], AppSubsystem | None] 361 ) -> AppSubsystem | None: 362 363 # Quick-out: if a subsystem is present, just return it; no 364 # locking necessary. 365 val = self._subsystem_property_data.get(ssname) 366 if val is not None: 367 if val is False: 368 # False means subsystem is confirmed as unavailable. 369 return None 370 if val is not True: 371 # A subsystem has been set. Return it. 372 return val 373 374 # Anything else (no val present or val True) requires locking. 375 with self._subsystem_property_lock: 376 val = self._subsystem_property_data.get(ssname) 377 if val is not None: 378 if val is False: 379 # False means confirmed as not present. 380 return None 381 if val is True: 382 # True means this property is already being loaded, 383 # and the fact that we're holding the lock means 384 # we're doing the loading, so this is a dependency 385 # loop. Not good. 386 raise RuntimeError( 387 f'Subsystem dependency loop detected for {ssname}' 388 ) 389 # Must be an instantiated subsystem. Noice. 390 return val 391 392 # Ok, there's nothing here for it. Instantiate and set it 393 # while we hold the lock. Set a placeholder value of True 394 # while we load so we can error if something we're loading 395 # tries to recursively load us. 396 self._subsystem_property_data[ssname] = True 397 398 # Do our one attempt to create the singleton. 399 val = create_call() 400 self._subsystem_property_data[ssname] = ( 401 False if val is None else val 402 ) 403 404 return val 405 406 # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_BEGIN__ 407 # This section generated by batools.appmodule; do not edit. 408 409 @property 410 def classic(self) -> ClassicAppSubsystem | None: 411 """Our classic subsystem (if available).""" 412 return self._get_subsystem_property( 413 'classic', self._create_classic_subsystem 414 ) # type: ignore 415 416 @staticmethod 417 def _create_classic_subsystem() -> ClassicAppSubsystem | None: 418 # pylint: disable=cyclic-import 419 try: 420 from baclassic import ClassicAppSubsystem 421 422 return ClassicAppSubsystem() 423 except ImportError: 424 return None 425 except Exception: 426 logging.exception('Error importing baclassic.') 427 return None 428 429 @property 430 def plus(self) -> PlusAppSubsystem | None: 431 """Our plus subsystem (if available).""" 432 return self._get_subsystem_property( 433 'plus', self._create_plus_subsystem 434 ) # type: ignore 435 436 @staticmethod 437 def _create_plus_subsystem() -> PlusAppSubsystem | None: 438 # pylint: disable=cyclic-import 439 try: 440 from baplus import PlusAppSubsystem 441 442 return PlusAppSubsystem() 443 except ImportError: 444 return None 445 except Exception: 446 logging.exception('Error importing baplus.') 447 return None 448 449 @property 450 def ui_v1(self) -> UIV1AppSubsystem: 451 """Our ui_v1 subsystem (always available).""" 452 return self._get_subsystem_property( 453 'ui_v1', self._create_ui_v1_subsystem 454 ) # type: ignore 455 456 @staticmethod 457 def _create_ui_v1_subsystem() -> UIV1AppSubsystem: 458 # pylint: disable=cyclic-import 459 460 from bauiv1 import UIV1AppSubsystem 461 462 return UIV1AppSubsystem() 463 464 # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_END__ 465 466 def register_subsystem(self, subsystem: AppSubsystem) -> None: 467 """Called by the AppSubsystem class. Do not use directly.""" 468 469 # We only allow registering new subsystems if we've not yet 470 # reached the 'running' state. This ensures that all subsystems 471 # receive a consistent set of callbacks starting with 472 # on_app_running(). 473 474 if self._subsystem_registration_ended: 475 raise RuntimeError( 476 'Subsystems can no longer be registered at this point.' 477 ) 478 self._subsystems.append(subsystem) 479 480 def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None: 481 """Add a task to be run on app shutdown. 482 483 Note that shutdown tasks will be canceled after 484 :py:const:`SHUTDOWN_TASK_TIMEOUT_SECONDS` if they are still 485 running. 486 """ 487 if ( 488 self.state is self.State.SHUTTING_DOWN 489 or self.state is self.State.SHUTDOWN_COMPLETE 490 ): 491 stname = self.state.name 492 raise RuntimeError( 493 f'Cannot add shutdown tasks with current state {stname}.' 494 ) 495 self._shutdown_tasks.append(coro) 496 497 def run(self) -> None: 498 """Run the app to completion. 499 500 Note that this only works on builds where Ballistica manages 501 its own event loop. 502 """ 503 _babase.run_app() 504 505 def set_intent(self, intent: AppIntent) -> None: 506 """Set the intent for the app. 507 508 Intent defines what the app is trying to do at a given time. 509 This call is asynchronous; the intent switch will happen in the 510 logic thread in the near future. If set_intent is called 511 repeatedly before the change takes place, the final intent to be 512 set will be used. 513 """ 514 515 # Mark this one as pending. We do this synchronously so that the 516 # last one marked actually takes effect if there is overlap 517 # (doing this in the bg thread could result in race conditions). 518 self._pending_intent = intent 519 520 # Do the actual work of calcing our app-mode/etc. in a bg thread 521 # since it may block for a moment to load modules/etc. 522 self.threadpool.submit_no_wait(self._set_intent, intent) 523 524 def push_apply_app_config(self) -> None: 525 """Internal. Use app.config.apply() to apply app config changes.""" 526 # To be safe, let's run this by itself in the event loop. 527 # This avoids potential trouble if this gets called mid-draw or 528 # something like that. 529 self._pending_apply_app_config = True 530 _babase.pushcall(self._apply_app_config, raw=True) 531 532 def on_native_start(self) -> None: 533 """Called by the native layer when the app is being started.""" 534 assert _babase.in_logic_thread() 535 assert not self._native_start_called 536 self._native_start_called = True 537 self._update_state() 538 539 def on_native_bootstrapping_complete(self) -> None: 540 """Called by the native layer once its ready to rock.""" 541 assert _babase.in_logic_thread() 542 assert not self._native_bootstrapping_completed 543 self._native_bootstrapping_completed = True 544 self._update_state() 545 546 def on_native_suspend(self) -> None: 547 """Called by the native layer when the app is suspended.""" 548 assert _babase.in_logic_thread() 549 assert not self._native_suspended # Should avoid redundant calls. 550 self._native_suspended = True 551 self._update_state() 552 553 def on_native_unsuspend(self) -> None: 554 """Called by the native layer when the app suspension ends.""" 555 assert _babase.in_logic_thread() 556 assert self._native_suspended # Should avoid redundant calls. 557 self._native_suspended = False 558 self._update_state() 559 560 def on_native_shutdown(self) -> None: 561 """Called by the native layer when the app starts shutting down.""" 562 assert _babase.in_logic_thread() 563 self._native_shutdown_called = True 564 self._update_state() 565 566 def on_native_shutdown_complete(self) -> None: 567 """Called by the native layer when the app is done shutting down.""" 568 assert _babase.in_logic_thread() 569 self._native_shutdown_complete_called = True 570 self._update_state() 571 572 def on_native_active_changed(self) -> None: 573 """Called by the native layer when the app active state changes.""" 574 assert _babase.in_logic_thread() 575 if self._mode is not None: 576 self._mode.on_app_active_changed() 577 578 def handle_deep_link(self, url: str) -> None: 579 """Handle a deep link URL.""" 580 from babase._language import Lstr 581 582 assert _babase.in_logic_thread() 583 584 appname = _babase.appname() 585 if url.startswith(f'{appname}://code/'): 586 code = url.replace(f'{appname}://code/', '') 587 if self.classic is not None: 588 self.classic.accounts.add_pending_promo_code(code) 589 else: 590 try: 591 _babase.screenmessage( 592 Lstr(resource='errorText'), color=(1, 0, 0) 593 ) 594 _babase.getsimplesound('error').play() 595 except ImportError: 596 pass 597 598 def on_initial_sign_in_complete(self) -> None: 599 """Called when initial sign-in (or lack thereof) completes. 600 601 This normally gets called by the plus subsystem. The 602 initial-sign-in process may include tasks such as syncing 603 account workspaces or other data so it may take a substantial 604 amount of time. 605 """ 606 assert _babase.in_logic_thread() 607 assert not self._initial_sign_in_completed 608 609 # Tell meta it can start scanning extra stuff that just showed 610 # up (namely account workspaces). 611 self.meta.start_extra_scan() 612 613 self._initial_sign_in_completed = True 614 self._update_state() 615 616 def set_ui_scale(self, scale: babase.UIScale) -> None: 617 """Change ui-scale on the fly. 618 619 Currently this is mainly for debugging and will not be called as 620 part of normal app operation. 621 """ 622 assert _babase.in_logic_thread() 623 624 # Apply to the native layer. 625 _babase.set_ui_scale(scale.name.lower()) 626 627 # Inform all subsystems that something screen-related has 628 # changed. We assume subsystems won't be added at this point so 629 # we can use the list directly. 630 assert self._subsystem_registration_ended 631 for subsystem in self._subsystems: 632 try: 633 subsystem.on_ui_scale_change() 634 except Exception: 635 logging.exception( 636 'Error in on_ui_scale_change() for subsystem %s.', subsystem 637 ) 638 639 def on_screen_size_change(self) -> None: 640 """Screen size has changed.""" 641 642 # Inform all app subsystems in the same order they were inited. 643 # Operate on a copy of the list here because this can be called 644 # while subsystems are still being added. 645 for subsystem in self._subsystems.copy(): 646 try: 647 subsystem.on_screen_size_change() 648 except Exception: 649 logging.exception( 650 'Error in on_screen_size_change() for subsystem %s.', 651 subsystem, 652 ) 653 654 def _set_intent(self, intent: AppIntent) -> None: 655 from babase._appmode import AppMode 656 657 # This should be happening in a bg thread. 658 assert not _babase.in_logic_thread() 659 try: 660 # Ask the selector what app-mode to use for this intent. 661 if self.mode_selector is None: 662 raise RuntimeError('No AppModeSelector set.') 663 664 modetype: type[AppMode] | None 665 666 # Special case - for testing we may force a specific 667 # app-mode to handle this intent instead of going through our 668 # usual selector. 669 forced_mode_type = getattr(intent, '_force_app_mode_handler', None) 670 if isinstance(forced_mode_type, type) and issubclass( 671 forced_mode_type, AppMode 672 ): 673 modetype = forced_mode_type 674 else: 675 modetype = self.mode_selector.app_mode_for_intent(intent) 676 677 # NOTE: Since intents are somewhat high level things, 678 # perhaps we should do some universal thing like a 679 # screenmessage saying 'The app cannot handle the request' 680 # on failure. 681 682 if modetype is None: 683 raise RuntimeError( 684 f'No app-mode found to handle app-intent' 685 f' type {type(intent)}.' 686 ) 687 688 # Make sure the app-mode the selector gave us *actually* 689 # supports the intent. 690 if not modetype.can_handle_intent(intent): 691 raise RuntimeError( 692 f'Intent {intent} cannot be handled by AppMode type' 693 f' {modetype} (selector {self.mode_selector}' 694 f' incorrectly thinks that it can be).' 695 ) 696 697 # Ok; seems legit. Now instantiate the mode if necessary and 698 # kick back to the logic thread to apply. 699 mode = self._mode_instances.get(modetype) 700 if mode is None: 701 self._mode_instances[modetype] = mode = modetype() 702 _babase.pushcall( 703 partial(self._apply_intent, intent, mode), 704 from_other_thread=True, 705 ) 706 except Exception: 707 logging.exception('Error setting app intent to %s.', intent) 708 _babase.pushcall( 709 partial(self._display_set_intent_error, intent), 710 from_other_thread=True, 711 ) 712 713 def _apply_intent(self, intent: AppIntent, mode: AppMode) -> None: 714 assert _babase.in_logic_thread() 715 716 # ONLY apply this intent if it is still the most recent one 717 # submitted. 718 if intent is not self._pending_intent: 719 return 720 721 # If the app-mode for this intent is different than the active 722 # one, switch modes. 723 if type(mode) is not type(self._mode): 724 if self._mode is None: 725 is_initial_mode = True 726 else: 727 is_initial_mode = False 728 try: 729 self._mode.on_deactivate() 730 except Exception: 731 logging.exception( 732 'Error deactivating app-mode %s.', self._mode 733 ) 734 735 # Reset all subsystems. We assume subsystems won't be added 736 # at this point so we can use the list directly. 737 assert self._subsystem_registration_ended 738 for subsystem in self._subsystems: 739 try: 740 subsystem.reset() 741 except Exception: 742 logging.exception( 743 'Error in reset() for subsystem %s.', subsystem 744 ) 745 746 self._mode = mode 747 try: 748 mode.on_activate() 749 except Exception: 750 # Hmm; what should we do in this case?... 751 logging.exception('Error activating app-mode %s.', mode) 752 753 # Let the world know when we first have an app-mode; certain 754 # app stuff such as input processing can proceed at that 755 # point. 756 if is_initial_mode: 757 _babase.on_initial_app_mode_set() 758 759 try: 760 mode.handle_intent(intent) 761 except Exception: 762 logging.exception( 763 'Error handling intent %s in app-mode %s.', intent, mode 764 ) 765 766 def _display_set_intent_error(self, intent: AppIntent) -> None: 767 """Show the *user* something went wrong setting an intent.""" 768 from babase._language import Lstr 769 770 del intent 771 _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) 772 _babase.getsimplesound('error').play() 773 774 def _on_initing(self) -> None: 775 """Called when the app enters the initing state. 776 777 Here we can put together subsystems and other pieces for the 778 app, but most things should not be doing any work yet. 779 """ 780 # pylint: disable=cyclic-import 781 from babase import _asyncio 782 from babase import _appconfig 783 from babase._apputils import AppHealthMonitor 784 from babase import _env 785 786 assert _babase.in_logic_thread() 787 788 _env.on_app_state_initing() 789 790 self._asyncio_loop = _asyncio.setup_asyncio() 791 self.health_monitor = AppHealthMonitor() 792 793 # __FEATURESET_APP_SUBSYSTEM_CREATE_BEGIN__ 794 # This section generated by batools.appmodule; do not edit. 795 796 # Poke these attrs to create all our subsystems. 797 _ = self.plus 798 _ = self.classic 799 _ = self.ui_v1 800 801 # __FEATURESET_APP_SUBSYSTEM_CREATE_END__ 802 803 # We're a pretty short-lived state. This should flip us to 804 # 'loading'. 805 self._init_completed = True 806 self._update_state() 807 808 def _on_loading(self) -> None: 809 """Called when we enter the loading state. 810 811 At this point, all built-in pieces of the app should be in place 812 and can start talking to each other and doing work. Though at a 813 high level, the goal of the app at this point is only to sign in 814 to initial accounts, download workspaces, and otherwise prepare 815 itself to really 'run'. 816 """ 817 assert _babase.in_logic_thread() 818 819 # Get meta-system scanning built-in stuff in the bg. 820 self.meta.start_scan(scan_complete_cb=self._on_meta_scan_complete) 821 822 # Inform all app subsystems in the same order they were inited. 823 # Operate on a copy of the list here because subsystems can 824 # still be added at this point. 825 for subsystem in self._subsystems.copy(): 826 try: 827 subsystem.on_app_loading() 828 except Exception: 829 logging.exception( 830 'Error in on_app_loading() for subsystem %s.', subsystem 831 ) 832 833 # Normally plus tells us when initial sign-in is done. If plus 834 # is not present, however, we just do it ourself so we can 835 # proceed on to the running state. 836 if self.plus is None: 837 _babase.pushcall(self.on_initial_sign_in_complete) 838 839 def _on_meta_scan_complete(self) -> None: 840 """Called when meta-scan is done doing its thing.""" 841 assert _babase.in_logic_thread() 842 843 # Now that we know what's out there, build our final plugin set. 844 self.plugins.on_meta_scan_complete() 845 846 assert not self._meta_scan_completed 847 self._meta_scan_completed = True 848 self._update_state() 849 850 def _on_running(self) -> None: 851 """Called when we enter the running state. 852 853 At this point, all workspaces, initial accounts, etc. are in place 854 and we can actually get started doing whatever we're gonna do. 855 """ 856 assert _babase.in_logic_thread() 857 858 # Let our native layer know. 859 _babase.on_app_running() 860 861 # Set a default app-mode-selector if none has been set yet 862 # by a plugin or whatnot. 863 if self._mode_selector is None: 864 self._mode_selector = self.DefaultAppModeSelector() 865 866 # Inform all app subsystems in the same order they were 867 # registered. Operate on a copy here because subsystems can 868 # still be added at this point. 869 # 870 # NOTE: Do we need to allow registering still at this point? If 871 # something gets registered here, it won't have its 872 # on_app_running callback called. Hmm; I suppose that's the only 873 # way that plugins can register subsystems though. 874 for subsystem in self._subsystems.copy(): 875 try: 876 subsystem.on_app_running() 877 except Exception: 878 logging.exception( 879 'Error in on_app_running() for subsystem %s.', subsystem 880 ) 881 882 # Cut off new subsystem additions at this point. 883 self._subsystem_registration_ended = True 884 885 # If 'exec' code was provided to the app, always kick that off 886 # here as an intent. 887 exec_cmd = _babase.exec_arg() 888 if exec_cmd is not None: 889 self.set_intent(AppIntentExec(exec_cmd)) 890 elif self._pending_intent is None: 891 # Otherwise tell the app to do its default thing *only* if a 892 # plugin hasn't already told it to do something. 893 self.set_intent(AppIntentDefault()) 894 895 def _apply_app_config(self) -> None: 896 assert _babase.in_logic_thread() 897 898 lifecyclelog.info('apply-app-config') 899 900 # If multiple apply calls have been made, only actually apply 901 # once. 902 if not self._pending_apply_app_config: 903 return 904 905 _pending_apply_app_config = False 906 907 # Inform all app subsystems in the same order they were inited. 908 # Operate on a copy here because subsystems may still be able to 909 # be added at this point. 910 for subsystem in self._subsystems.copy(): 911 try: 912 subsystem.do_apply_app_config() 913 except Exception: 914 logging.exception( 915 'Error in do_apply_app_config() for subsystem %s.', 916 subsystem, 917 ) 918 919 # Let the native layer do its thing. 920 _babase.do_apply_app_config() 921 922 def _update_state(self) -> None: 923 # pylint: disable=too-many-branches 924 assert _babase.in_logic_thread() 925 926 # Shutdown-complete trumps absolutely all. 927 if self._native_shutdown_complete_called: 928 if self.state is not self.State.SHUTDOWN_COMPLETE: 929 self.state = self.State.SHUTDOWN_COMPLETE 930 lifecyclelog.info('app-state is now %s', self.state.name) 931 self._on_shutdown_complete() 932 933 # Shutdown trumps all. Though we can't start shutting down until 934 # init is completed since we need our asyncio stuff to exist for 935 # the shutdown process. 936 elif self._native_shutdown_called and self._init_completed: 937 # Entering shutdown state: 938 if self.state is not self.State.SHUTTING_DOWN: 939 self.state = self.State.SHUTTING_DOWN 940 applog.info('Shutting down...') 941 lifecyclelog.info('app-state is now %s', self.state.name) 942 self._on_shutting_down() 943 944 elif self._native_suspended: 945 # Entering suspended state: 946 if self.state is not self.State.SUSPENDED: 947 self.state = self.State.SUSPENDED 948 self._on_suspend() 949 else: 950 # Leaving suspended state: 951 if self.state is self.State.SUSPENDED: 952 self._on_unsuspend() 953 954 # Entering or returning to running state 955 if self._initial_sign_in_completed and self._meta_scan_completed: 956 if self.state != self.State.RUNNING: 957 self.state = self.State.RUNNING 958 lifecyclelog.info('app-state is now %s', self.state.name) 959 if not self._called_on_running: 960 self._called_on_running = True 961 self._on_running() 962 963 # Entering or returning to loading state: 964 elif self._init_completed: 965 if self.state is not self.State.LOADING: 966 self.state = self.State.LOADING 967 lifecyclelog.info('app-state is now %s', self.state.name) 968 if not self._called_on_loading: 969 self._called_on_loading = True 970 self._on_loading() 971 972 # Entering or returning to initing state: 973 elif self._native_bootstrapping_completed: 974 if self.state is not self.State.INITING: 975 self.state = self.State.INITING 976 lifecyclelog.info('app-state is now %s', self.state.name) 977 if not self._called_on_initing: 978 self._called_on_initing = True 979 self._on_initing() 980 981 # Entering or returning to native bootstrapping: 982 elif self._native_start_called: 983 if self.state is not self.State.NATIVE_BOOTSTRAPPING: 984 self.state = self.State.NATIVE_BOOTSTRAPPING 985 lifecyclelog.info('app-state is now %s', self.state.name) 986 else: 987 # Only logical possibility left is NOT_STARTED, in which 988 # case we should not be getting called. 989 logging.warning( 990 'App._update_state called while in %s state;' 991 ' should not happen.', 992 self.state.value, 993 stack_info=True, 994 ) 995 996 async def _shutdown(self) -> None: 997 import asyncio 998 999 _babase.lock_all_input() 1000 try: 1001 async with asyncio.TaskGroup() as task_group: 1002 for task_coro in self._shutdown_tasks: 1003 # Note: Mypy currently complains if we don't take 1004 # this return value, but we don't actually need to. 1005 # https://github.com/python/mypy/issues/15036 1006 _ = task_group.create_task( 1007 self._run_shutdown_task(task_coro) 1008 ) 1009 except* Exception: 1010 logging.exception('Unexpected error(s) in shutdown.') 1011 1012 # Note: ideally we should run this directly here, but currently 1013 # it does some legacy stuff which blocks, so running it here 1014 # gives us asyncio task-took-too-long warnings. If we can 1015 # convert those to nice graceful async tasks we should revert 1016 # this to a direct call. 1017 _babase.pushcall(_babase.complete_shutdown) 1018 1019 async def _run_shutdown_task( 1020 self, coro: Coroutine[None, None, None] 1021 ) -> None: 1022 """Run a shutdown task; report errors and abort if taking too long.""" 1023 import asyncio 1024 1025 task = asyncio.create_task(coro) 1026 try: 1027 await asyncio.wait_for(task, self.SHUTDOWN_TASK_TIMEOUT_SECONDS) 1028 except Exception: 1029 logging.exception('Error in shutdown task (%s).', coro) 1030 1031 def _on_suspend(self) -> None: 1032 """Called when the app goes to a suspended state.""" 1033 assert _babase.in_logic_thread() 1034 1035 # Suspend all app subsystems in the opposite order they were inited. 1036 for subsystem in reversed(self._subsystems): 1037 try: 1038 subsystem.on_app_suspend() 1039 except Exception: 1040 logging.exception( 1041 'Error in on_app_suspend() for subsystem %s.', subsystem 1042 ) 1043 1044 def _on_unsuspend(self) -> None: 1045 """Called when unsuspending.""" 1046 assert _babase.in_logic_thread() 1047 self.fg_state += 1 1048 1049 # Unsuspend all app subsystems in the same order they were inited. 1050 for subsystem in self._subsystems: 1051 try: 1052 subsystem.on_app_unsuspend() 1053 except Exception: 1054 logging.exception( 1055 'Error in on_app_unsuspend() for subsystem %s.', subsystem 1056 ) 1057 1058 def _on_shutting_down(self) -> None: 1059 """(internal)""" 1060 assert _babase.in_logic_thread() 1061 1062 # Inform app subsystems that we're shutting down in the opposite 1063 # order they were inited. 1064 for subsystem in reversed(self._subsystems): 1065 try: 1066 subsystem.on_app_shutdown() 1067 except Exception: 1068 logging.exception( 1069 'Error in on_app_shutdown() for subsystem %s.', subsystem 1070 ) 1071 1072 # Now kick off any async shutdown task(s). 1073 assert self._asyncio_loop is not None 1074 self._shutdown_task = self._asyncio_loop.create_task(self._shutdown()) 1075 1076 def _on_shutdown_complete(self) -> None: 1077 """(internal)""" 1078 assert _babase.in_logic_thread() 1079 1080 # Deactivate any active app-mode. This allows things like saving 1081 # state to happen naturally without needing to handle 1082 # app-shutdown as a special case. 1083 if self._mode is not None: 1084 try: 1085 self._mode.on_deactivate() 1086 except Exception: 1087 logging.exception( 1088 'Error deactivating app-mode %s at app shutdown.', 1089 self._mode, 1090 ) 1091 self._mode = None 1092 1093 # Inform app subsystems that we're done shutting down in the opposite 1094 # order they were inited. 1095 for subsystem in reversed(self._subsystems): 1096 try: 1097 subsystem.on_app_shutdown_complete() 1098 except Exception: 1099 logging.exception( 1100 'Error in on_app_shutdown_complete() for subsystem %s.', 1101 subsystem, 1102 ) 1103 1104 async def _wait_for_shutdown_suppressions(self) -> None: 1105 import asyncio 1106 1107 # Spin and wait for anything blocking shutdown to complete. 1108 starttime = _babase.apptime() 1109 lifecyclelog.info('shutdown-suppress-wait begin') 1110 while _babase.shutdown_suppress_count() > 0: 1111 await asyncio.sleep(0.001) 1112 lifecyclelog.info('shutdown-suppress-wait end') 1113 duration = _babase.apptime() - starttime 1114 if duration > 1.0: 1115 logging.warning( 1116 'Shutdown-suppressions lasted longer than ideal ' 1117 '(%.2f seconds).', 1118 duration, 1119 ) 1120 1121 async def _fade_and_shutdown_graphics(self) -> None: 1122 import asyncio 1123 1124 # Kick off a short fade and give it time to complete. 1125 lifecyclelog.info('fade-and-shutdown-graphics begin') 1126 _babase.fade_screen(False, time=0.15) 1127 await asyncio.sleep(0.15) 1128 1129 # Now tell the graphics system to go down and wait until 1130 # it has done so. 1131 _babase.graphics_shutdown_begin() 1132 while not _babase.graphics_shutdown_is_complete(): 1133 await asyncio.sleep(0.01) 1134 lifecyclelog.info('fade-and-shutdown-graphics end') 1135 1136 async def _fade_and_shutdown_audio(self) -> None: 1137 import asyncio 1138 1139 # Tell the audio system to go down and give it a bit of 1140 # time to do so gracefully. 1141 lifecyclelog.info('fade-and-shutdown-audio begin') 1142 _babase.audio_shutdown_begin() 1143 await asyncio.sleep(0.15) 1144 while not _babase.audio_shutdown_is_complete(): 1145 await asyncio.sleep(0.01) 1146 lifecyclelog.info('fade-and-shutdown-audio end') 1147 1148 def _thread_pool_thread_init(self) -> None: 1149 # Help keep things clear in profiling tools/etc. 1150 self._pool_thread_count += 1 1151 _babase.set_thread_name(f'ballistica worker-{self._pool_thread_count}')
High level Ballistica app functionality and state.
Access the single shared instance of this class via the "app" attr
available on various high level modules such as bauiv1
and
bascenev1
.
248 def postinit(self) -> None: 249 """Called after we've been inited and assigned to babase.app. 250 251 Anything that accesses babase.app as part of its init process 252 must go here instead of __init__. 253 """ 254 255 # Hack for docs-generation: We can be imported with dummy 256 # modules instead of our actual binary ones, but we don't 257 # function. 258 if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1': 259 return 260 261 self.lang = LanguageSubsystem() 262 self.plugins = PluginSubsystem()
Called after we've been inited and assigned to babase.app.
Anything that accesses babase.app as part of its init process must go here instead of __init__.
264 @property 265 def active(self) -> bool: 266 """Whether the app is currently front and center. 267 268 This will be False when the app is hidden, other activities 269 are covering it, etc. (depending on the platform). 270 """ 271 return _babase.app_is_active()
Whether the app is currently front and center.
This will be False when the app is hidden, other activities are covering it, etc. (depending on the platform).
273 @property 274 def mode(self) -> AppMode | None: 275 """The app's current mode.""" 276 assert _babase.in_logic_thread() 277 return self._mode
The app's current mode.
279 @property 280 def asyncio_loop(self) -> asyncio.AbstractEventLoop: 281 """The logic thread's asyncio event loop. 282 283 This allow async tasks to be run in the logic thread. 284 285 Generally you should call App.create_async_task() to schedule 286 async code to run instead of using this directly. That will 287 handle retaining the task and logging errors automatically. 288 Only schedule tasks onto asyncio_loop yourself when you intend 289 to hold on to the returned task and await its results. Releasing 290 the task reference can lead to subtle bugs such as unreported 291 errors and garbage-collected tasks disappearing before their 292 work is done. 293 294 Note that, at this time, the asyncio loop is encapsulated 295 and explicitly stepped by the engine's logic thread loop and 296 thus things like asyncio.get_running_loop() will unintuitively 297 *not* return this loop from most places in the logic thread; 298 only from within a task explicitly created in this loop. 299 Hopefully this situation will be improved in the future with a 300 unified event loop. 301 """ 302 assert _babase.in_logic_thread() 303 assert self._asyncio_loop is not None 304 return self._asyncio_loop
The logic thread's asyncio event loop.
This allow async tasks to be run in the logic thread.
Generally you should call App.create_async_task() to schedule async code to run instead of using this directly. That will handle retaining the task and logging errors automatically. Only schedule tasks onto asyncio_loop yourself when you intend to hold on to the returned task and await its results. Releasing the task reference can lead to subtle bugs such as unreported errors and garbage-collected tasks disappearing before their work is done.
Note that, at this time, the asyncio loop is encapsulated and explicitly stepped by the engine's logic thread loop and thus things like asyncio.get_running_loop() will unintuitively not return this loop from most places in the logic thread; only from within a task explicitly created in this loop. Hopefully this situation will be improved in the future with a unified event loop.
306 def create_async_task( 307 self, coro: Coroutine[Any, Any, T], *, name: str | None = None 308 ) -> None: 309 """Create a fully managed async task. 310 311 This will automatically retain and release a reference to the task 312 and log any exceptions that occur in it. If you need to await a task 313 or otherwise need more control, schedule a task directly using 314 App.asyncio_loop. 315 """ 316 assert _babase.in_logic_thread() 317 318 # We hold a strong reference to the task until it is done. 319 # Otherwise it is possible for it to be garbage collected and 320 # disappear midway if the caller does not hold on to the 321 # returned task, which seems like a great way to introduce 322 # hard-to-track bugs. 323 task = self.asyncio_loop.create_task(coro, name=name) 324 self._asyncio_tasks.add(task) 325 task.add_done_callback(self._on_task_done)
Create a fully managed async task.
This will automatically retain and release a reference to the task and log any exceptions that occur in it. If you need to await a task or otherwise need more control, schedule a task directly using App.asyncio_loop.
340 @property 341 def mode_selector(self) -> babase.AppModeSelector: 342 """Controls which app-modes are used for handling given intents. 343 344 Plugins can override this to change high level app behavior and 345 spinoff projects can change the default implementation for the 346 same effect. 347 """ 348 if self._mode_selector is None: 349 raise RuntimeError( 350 'mode_selector cannot be used until the app reaches' 351 ' the running state.' 352 ) 353 return self._mode_selector
Controls which app-modes are used for handling given intents.
Plugins can override this to change high level app behavior and spinoff projects can change the default implementation for the same effect.
409 @property 410 def classic(self) -> ClassicAppSubsystem | None: 411 """Our classic subsystem (if available).""" 412 return self._get_subsystem_property( 413 'classic', self._create_classic_subsystem 414 ) # type: ignore
Our classic subsystem (if available).
429 @property 430 def plus(self) -> PlusAppSubsystem | None: 431 """Our plus subsystem (if available).""" 432 return self._get_subsystem_property( 433 'plus', self._create_plus_subsystem 434 ) # type: ignore
Our plus subsystem (if available).
449 @property 450 def ui_v1(self) -> UIV1AppSubsystem: 451 """Our ui_v1 subsystem (always available).""" 452 return self._get_subsystem_property( 453 'ui_v1', self._create_ui_v1_subsystem 454 ) # type: ignore
Our ui_v1 subsystem (always available).
466 def register_subsystem(self, subsystem: AppSubsystem) -> None: 467 """Called by the AppSubsystem class. Do not use directly.""" 468 469 # We only allow registering new subsystems if we've not yet 470 # reached the 'running' state. This ensures that all subsystems 471 # receive a consistent set of callbacks starting with 472 # on_app_running(). 473 474 if self._subsystem_registration_ended: 475 raise RuntimeError( 476 'Subsystems can no longer be registered at this point.' 477 ) 478 self._subsystems.append(subsystem)
Called by the AppSubsystem class. Do not use directly.
480 def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None: 481 """Add a task to be run on app shutdown. 482 483 Note that shutdown tasks will be canceled after 484 :py:const:`SHUTDOWN_TASK_TIMEOUT_SECONDS` if they are still 485 running. 486 """ 487 if ( 488 self.state is self.State.SHUTTING_DOWN 489 or self.state is self.State.SHUTDOWN_COMPLETE 490 ): 491 stname = self.state.name 492 raise RuntimeError( 493 f'Cannot add shutdown tasks with current state {stname}.' 494 ) 495 self._shutdown_tasks.append(coro)
Add a task to be run on app shutdown.
Note that shutdown tasks will be canceled after
SHUTDOWN_TASK_TIMEOUT_SECONDS
if they are still
running.
497 def run(self) -> None: 498 """Run the app to completion. 499 500 Note that this only works on builds where Ballistica manages 501 its own event loop. 502 """ 503 _babase.run_app()
Run the app to completion.
Note that this only works on builds where Ballistica manages its own event loop.
505 def set_intent(self, intent: AppIntent) -> None: 506 """Set the intent for the app. 507 508 Intent defines what the app is trying to do at a given time. 509 This call is asynchronous; the intent switch will happen in the 510 logic thread in the near future. If set_intent is called 511 repeatedly before the change takes place, the final intent to be 512 set will be used. 513 """ 514 515 # Mark this one as pending. We do this synchronously so that the 516 # last one marked actually takes effect if there is overlap 517 # (doing this in the bg thread could result in race conditions). 518 self._pending_intent = intent 519 520 # Do the actual work of calcing our app-mode/etc. in a bg thread 521 # since it may block for a moment to load modules/etc. 522 self.threadpool.submit_no_wait(self._set_intent, intent)
Set the intent for the app.
Intent defines what the app is trying to do at a given time. This call is asynchronous; the intent switch will happen in the logic thread in the near future. If set_intent is called repeatedly before the change takes place, the final intent to be set will be used.
524 def push_apply_app_config(self) -> None: 525 """Internal. Use app.config.apply() to apply app config changes.""" 526 # To be safe, let's run this by itself in the event loop. 527 # This avoids potential trouble if this gets called mid-draw or 528 # something like that. 529 self._pending_apply_app_config = True 530 _babase.pushcall(self._apply_app_config, raw=True)
Internal. Use app.config.apply() to apply app config changes.
532 def on_native_start(self) -> None: 533 """Called by the native layer when the app is being started.""" 534 assert _babase.in_logic_thread() 535 assert not self._native_start_called 536 self._native_start_called = True 537 self._update_state()
Called by the native layer when the app is being started.
539 def on_native_bootstrapping_complete(self) -> None: 540 """Called by the native layer once its ready to rock.""" 541 assert _babase.in_logic_thread() 542 assert not self._native_bootstrapping_completed 543 self._native_bootstrapping_completed = True 544 self._update_state()
Called by the native layer once its ready to rock.
546 def on_native_suspend(self) -> None: 547 """Called by the native layer when the app is suspended.""" 548 assert _babase.in_logic_thread() 549 assert not self._native_suspended # Should avoid redundant calls. 550 self._native_suspended = True 551 self._update_state()
Called by the native layer when the app is suspended.
553 def on_native_unsuspend(self) -> None: 554 """Called by the native layer when the app suspension ends.""" 555 assert _babase.in_logic_thread() 556 assert self._native_suspended # Should avoid redundant calls. 557 self._native_suspended = False 558 self._update_state()
Called by the native layer when the app suspension ends.
560 def on_native_shutdown(self) -> None: 561 """Called by the native layer when the app starts shutting down.""" 562 assert _babase.in_logic_thread() 563 self._native_shutdown_called = True 564 self._update_state()
Called by the native layer when the app starts shutting down.
566 def on_native_shutdown_complete(self) -> None: 567 """Called by the native layer when the app is done shutting down.""" 568 assert _babase.in_logic_thread() 569 self._native_shutdown_complete_called = True 570 self._update_state()
Called by the native layer when the app is done shutting down.
572 def on_native_active_changed(self) -> None: 573 """Called by the native layer when the app active state changes.""" 574 assert _babase.in_logic_thread() 575 if self._mode is not None: 576 self._mode.on_app_active_changed()
Called by the native layer when the app active state changes.
578 def handle_deep_link(self, url: str) -> None: 579 """Handle a deep link URL.""" 580 from babase._language import Lstr 581 582 assert _babase.in_logic_thread() 583 584 appname = _babase.appname() 585 if url.startswith(f'{appname}://code/'): 586 code = url.replace(f'{appname}://code/', '') 587 if self.classic is not None: 588 self.classic.accounts.add_pending_promo_code(code) 589 else: 590 try: 591 _babase.screenmessage( 592 Lstr(resource='errorText'), color=(1, 0, 0) 593 ) 594 _babase.getsimplesound('error').play() 595 except ImportError: 596 pass
Handle a deep link URL.
598 def on_initial_sign_in_complete(self) -> None: 599 """Called when initial sign-in (or lack thereof) completes. 600 601 This normally gets called by the plus subsystem. The 602 initial-sign-in process may include tasks such as syncing 603 account workspaces or other data so it may take a substantial 604 amount of time. 605 """ 606 assert _babase.in_logic_thread() 607 assert not self._initial_sign_in_completed 608 609 # Tell meta it can start scanning extra stuff that just showed 610 # up (namely account workspaces). 611 self.meta.start_extra_scan() 612 613 self._initial_sign_in_completed = True 614 self._update_state()
Called when initial sign-in (or lack thereof) completes.
This normally gets called by the plus subsystem. The initial-sign-in process may include tasks such as syncing account workspaces or other data so it may take a substantial amount of time.
616 def set_ui_scale(self, scale: babase.UIScale) -> None: 617 """Change ui-scale on the fly. 618 619 Currently this is mainly for debugging and will not be called as 620 part of normal app operation. 621 """ 622 assert _babase.in_logic_thread() 623 624 # Apply to the native layer. 625 _babase.set_ui_scale(scale.name.lower()) 626 627 # Inform all subsystems that something screen-related has 628 # changed. We assume subsystems won't be added at this point so 629 # we can use the list directly. 630 assert self._subsystem_registration_ended 631 for subsystem in self._subsystems: 632 try: 633 subsystem.on_ui_scale_change() 634 except Exception: 635 logging.exception( 636 'Error in on_ui_scale_change() for subsystem %s.', subsystem 637 )
Change ui-scale on the fly.
Currently this is mainly for debugging and will not be called as part of normal app operation.
639 def on_screen_size_change(self) -> None: 640 """Screen size has changed.""" 641 642 # Inform all app subsystems in the same order they were inited. 643 # Operate on a copy of the list here because this can be called 644 # while subsystems are still being added. 645 for subsystem in self._subsystems.copy(): 646 try: 647 subsystem.on_screen_size_change() 648 except Exception: 649 logging.exception( 650 'Error in on_screen_size_change() for subsystem %s.', 651 subsystem, 652 )
Screen size has changed.
62 class State(Enum): 63 """High level state the app can be in.""" 64 65 #: The app has not yet begun starting and should not be used in 66 #: any way. 67 NOT_STARTED = 0 68 69 #: The native layer is spinning up its machinery (screens, 70 #: renderers, etc.). Nothing should happen in the Python layer 71 #: until this completes. 72 NATIVE_BOOTSTRAPPING = 1 73 74 #: Python app subsystems are being inited but should not yet 75 #: interact or do any work. 76 INITING = 2 77 78 #: Python app subsystems are inited and interacting, but the app 79 #: has not yet embarked on a high level course of action. It is 80 #: doing initial account logins, workspace & asset downloads, 81 #: etc. 82 LOADING = 3 83 84 #: All pieces are in place and the app is now doing its thing. 85 RUNNING = 4 86 87 #: Used on platforms such as mobile where the app basically needs 88 #: to shut down while backgrounded. In this state, all event 89 #: loops are suspended and all graphics and audio must cease 90 #: completely. Be aware that the suspended state can be entered 91 #: from any other state including NATIVE_BOOTSTRAPPING and 92 #: SHUTTING_DOWN. 93 SUSPENDED = 5 94 95 #: The app is shutting down. This process may involve sending 96 #: network messages or other things that can take up to a few 97 #: seconds, so ideally graphics and audio should remain 98 #: functional (with fades or spinners or whatever to show 99 #: something is happening). 100 SHUTTING_DOWN = 6 101 102 #: The app has completed shutdown. Any code running here should 103 #: be basically immediate. 104 SHUTDOWN_COMPLETE = 7
High level state the app can be in.
106 class DefaultAppModeSelector(AppModeSelector): 107 """Decides which AppMode to use to handle AppIntents. 108 109 This default version is generated by the project updater based 110 on the 'default_app_modes' value in the projectconfig. 111 112 It is also possible to modify app mode selection behavior by 113 setting app.mode_selector to an instance of a custom 114 AppModeSelector subclass. This is a good way to go if you are 115 modifying app behavior dynamically via a plugin instead of 116 statically in a spinoff project. 117 """ 118 119 @override 120 def app_mode_for_intent( 121 self, intent: AppIntent 122 ) -> type[AppMode] | None: 123 # pylint: disable=cyclic-import 124 125 # __DEFAULT_APP_MODE_SELECTION_BEGIN__ 126 # This section generated by batools.appmodule; do not edit. 127 128 # Ask our default app modes to handle it. 129 # (generated from 'default_app_modes' in projectconfig). 130 import baclassic 131 import babase 132 133 for appmode in [ 134 baclassic.ClassicAppMode, 135 babase.EmptyAppMode, 136 ]: 137 if appmode.can_handle_intent(intent): 138 return appmode 139 140 return None 141 142 # __DEFAULT_APP_MODE_SELECTION_END__
Decides which AppMode to use to handle AppIntents.
This default version is generated by the project updater based on the 'default_app_modes' value in the projectconfig.
It is also possible to modify app mode selection behavior by setting app.mode_selector to an instance of a custom AppModeSelector subclass. This is a good way to go if you are modifying app behavior dynamically via a plugin instead of statically in a spinoff project.
119 @override 120 def app_mode_for_intent( 121 self, intent: AppIntent 122 ) -> type[AppMode] | None: 123 # pylint: disable=cyclic-import 124 125 # __DEFAULT_APP_MODE_SELECTION_BEGIN__ 126 # This section generated by batools.appmodule; do not edit. 127 128 # Ask our default app modes to handle it. 129 # (generated from 'default_app_modes' in projectconfig). 130 import baclassic 131 import babase 132 133 for appmode in [ 134 baclassic.ClassicAppMode, 135 babase.EmptyAppMode, 136 ]: 137 if appmode.can_handle_intent(intent): 138 return appmode 139 140 return None 141 142 # __DEFAULT_APP_MODE_SELECTION_END__
Given an AppIntent, return the AppMode that should handle it.
If None is returned, the AppIntent will be ignored.
This may be called in a background thread, so avoid any calls limited to logic thread use/etc.
A high level directive given to the app.
Tells the app to simply run in its default mode.
21class AppIntentExec(AppIntent): 22 """Tells the app to exec some Python code.""" 23 24 def __init__(self, code: str): 25 self.code = code
Tells the app to exec some Python code.
14class AppMode: 15 """A high level mode for the app.""" 16 17 @classmethod 18 def get_app_experience(cls) -> AppExperience: 19 """Return the overall experience provided by this mode.""" 20 raise NotImplementedError('AppMode subclasses must override this.') 21 22 @classmethod 23 def can_handle_intent(cls, intent: AppIntent) -> bool: 24 """Return whether this mode can handle the provided intent. 25 26 For this to return True, the AppMode must claim to support the 27 provided intent (via its _can_handle_intent() method) AND the 28 AppExperience associated with the AppMode must be supported by 29 the current app and runtime environment. 30 """ 31 # TODO: check AppExperience against current environment. 32 return cls._can_handle_intent(intent) 33 34 @classmethod 35 def _can_handle_intent(cls, intent: AppIntent) -> bool: 36 """Return whether our mode can handle the provided intent. 37 38 AppModes should override this to communicate what they can 39 handle. Note that AppExperience does not have to be considered 40 here; that is handled automatically by the can_handle_intent() 41 call. 42 """ 43 raise NotImplementedError('AppMode subclasses must override this.') 44 45 def handle_intent(self, intent: AppIntent) -> None: 46 """Handle an intent.""" 47 raise NotImplementedError('AppMode subclasses must override this.') 48 49 def on_activate(self) -> None: 50 """Called when the mode is becoming the active one fro the app.""" 51 52 def on_deactivate(self) -> None: 53 """Called when the mode stops being the active one for the app. 54 55 On platforms where the app is explicitly exited (such as desktop 56 PC) this will also be called at app shutdown. 57 58 To best cover both mobile and desktop style platforms, actions 59 such as saving state should generally happen in response to both 60 on_deactivate() and on_app_active_changed() (when active is 61 False). 62 """ 63 64 def on_app_active_changed(self) -> None: 65 """Called when app active state changes while in this app-mode. 66 67 This corresponds to :attr:`babase.App.active`. App-active state 68 becomes false when the app is hidden, minimized, backgrounded, 69 etc. The app-mode may want to take action such as pausing a 70 running game or saving state when this occurs. 71 72 On platforms such as mobile where apps get suspended and later 73 silently terminated by the OS, this is likely to be the last 74 reliable place to save state/etc. 75 76 To best cover both mobile and desktop style platforms, actions 77 such as saving state should generally happen in response to both 78 on_deactivate() and on_app_active_changed() (when active is 79 False). 80 """ 81 82 def on_purchase_process_begin( 83 self, item_id: str, user_initiated: bool 84 ) -> None: 85 """Called when in-app-purchase processing is beginning. 86 87 This call happens after a purchase has been completed locally 88 but before its receipt/info is sent to the master-server to 89 apply to the account. 90 """ 91 # pylint: disable=cyclic-import 92 import babase 93 94 del item_id # Unused. 95 96 # Show nothing for stuff not directly kicked off by the user. 97 if not user_initiated: 98 return 99 100 babase.screenmessage( 101 babase.Lstr(resource='updatingAccountText'), 102 color=(0, 1, 0), 103 ) 104 # Ick; we can be called early in the bootstrapping process 105 # before we're allowed to load assets. Guard against that. 106 if babase.asset_loads_allowed(): 107 babase.getsimplesound('click01').play() 108 109 def on_purchase_process_end( 110 self, item_id: str, user_initiated: bool, applied: bool 111 ) -> None: 112 """Called when in-app-purchase processing completes. 113 114 Each call to on_purchase_process_begin will be followed up by a 115 call to this method. If the purchase was found to be valid and 116 was applied to the account, applied will be True. In the case of 117 redundant or invalid purchases or communication failures it will 118 be False. 119 """ 120 # pylint: disable=cyclic-import 121 import babase 122 123 # Ignore this; we want to announce newly applied stuff even if 124 # it was from a different launch or client or whatever. 125 del user_initiated 126 127 # If the purchase wasn't applied, do nothing. This likely means it 128 # was redundant or something else harmless. 129 if not applied: 130 return 131 132 # By default just announce the item id we got. Real app-modes 133 # probably want to do something more specific based on item-id. 134 babase.screenmessage( 135 babase.Lstr( 136 translate=('serverResponses', 'You got a ${ITEM}!'), 137 subs=[('${ITEM}', item_id)], 138 ), 139 color=(0, 1, 0), 140 ) 141 if babase.asset_loads_allowed(): 142 babase.getsimplesound('cashRegister').play()
A high level mode for the app.
17 @classmethod 18 def get_app_experience(cls) -> AppExperience: 19 """Return the overall experience provided by this mode.""" 20 raise NotImplementedError('AppMode subclasses must override this.')
Return the overall experience provided by this mode.
22 @classmethod 23 def can_handle_intent(cls, intent: AppIntent) -> bool: 24 """Return whether this mode can handle the provided intent. 25 26 For this to return True, the AppMode must claim to support the 27 provided intent (via its _can_handle_intent() method) AND the 28 AppExperience associated with the AppMode must be supported by 29 the current app and runtime environment. 30 """ 31 # TODO: check AppExperience against current environment. 32 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.
45 def handle_intent(self, intent: AppIntent) -> None: 46 """Handle an intent.""" 47 raise NotImplementedError('AppMode subclasses must override this.')
Handle an intent.
49 def on_activate(self) -> None: 50 """Called when the mode is becoming the active one fro the app."""
Called when the mode is becoming the active one fro the app.
52 def on_deactivate(self) -> None: 53 """Called when the mode stops being the active one for the app. 54 55 On platforms where the app is explicitly exited (such as desktop 56 PC) this will also be called at app shutdown. 57 58 To best cover both mobile and desktop style platforms, actions 59 such as saving state should generally happen in response to both 60 on_deactivate() and on_app_active_changed() (when active is 61 False). 62 """
Called when the mode stops being the active one for the app.
On platforms where the app is explicitly exited (such as desktop PC) this will also be called at app shutdown.
To best cover both mobile and desktop style platforms, actions such as saving state should generally happen in response to both on_deactivate() and on_app_active_changed() (when active is False).
64 def on_app_active_changed(self) -> None: 65 """Called when app active state changes while in this app-mode. 66 67 This corresponds to :attr:`babase.App.active`. App-active state 68 becomes false when the app is hidden, minimized, backgrounded, 69 etc. The app-mode may want to take action such as pausing a 70 running game or saving state when this occurs. 71 72 On platforms such as mobile where apps get suspended and later 73 silently terminated by the OS, this is likely to be the last 74 reliable place to save state/etc. 75 76 To best cover both mobile and desktop style platforms, actions 77 such as saving state should generally happen in response to both 78 on_deactivate() and on_app_active_changed() (when active is 79 False). 80 """
Called when app active state changes while in this app-mode.
This corresponds to babase.App.active
. App-active state
becomes false when the app is hidden, minimized, backgrounded,
etc. The app-mode may want to take action such as pausing a
running game or saving state when this occurs.
On platforms such as mobile where apps get suspended and later silently terminated by the OS, this is likely to be the last reliable place to save state/etc.
To best cover both mobile and desktop style platforms, actions such as saving state should generally happen in response to both on_deactivate() and on_app_active_changed() (when active is False).
82 def on_purchase_process_begin( 83 self, item_id: str, user_initiated: bool 84 ) -> None: 85 """Called when in-app-purchase processing is beginning. 86 87 This call happens after a purchase has been completed locally 88 but before its receipt/info is sent to the master-server to 89 apply to the account. 90 """ 91 # pylint: disable=cyclic-import 92 import babase 93 94 del item_id # Unused. 95 96 # Show nothing for stuff not directly kicked off by the user. 97 if not user_initiated: 98 return 99 100 babase.screenmessage( 101 babase.Lstr(resource='updatingAccountText'), 102 color=(0, 1, 0), 103 ) 104 # Ick; we can be called early in the bootstrapping process 105 # before we're allowed to load assets. Guard against that. 106 if babase.asset_loads_allowed(): 107 babase.getsimplesound('click01').play()
Called when in-app-purchase processing is beginning.
This call happens after a purchase has been completed locally but before its receipt/info is sent to the master-server to apply to the account.
109 def on_purchase_process_end( 110 self, item_id: str, user_initiated: bool, applied: bool 111 ) -> None: 112 """Called when in-app-purchase processing completes. 113 114 Each call to on_purchase_process_begin will be followed up by a 115 call to this method. If the purchase was found to be valid and 116 was applied to the account, applied will be True. In the case of 117 redundant or invalid purchases or communication failures it will 118 be False. 119 """ 120 # pylint: disable=cyclic-import 121 import babase 122 123 # Ignore this; we want to announce newly applied stuff even if 124 # it was from a different launch or client or whatever. 125 del user_initiated 126 127 # If the purchase wasn't applied, do nothing. This likely means it 128 # was redundant or something else harmless. 129 if not applied: 130 return 131 132 # By default just announce the item id we got. Real app-modes 133 # probably want to do something more specific based on item-id. 134 babase.screenmessage( 135 babase.Lstr( 136 translate=('serverResponses', 'You got a ${ITEM}!'), 137 subs=[('${ITEM}', item_id)], 138 ), 139 color=(0, 1, 0), 140 ) 141 if babase.asset_loads_allowed(): 142 babase.getsimplesound('cashRegister').play()
Called when in-app-purchase processing completes.
Each call to on_purchase_process_begin will be followed up by a call to this method. If the purchase was found to be valid and was applied to the account, applied will be True. In the case of redundant or invalid purchases or communication failures it will be False.
Return current app name (all lowercase).
535def appnameupper() -> str: 536 """Return current app name with capitalized characters.""" 537 return str()
Return current app name with capitalized characters.
540def apptime() -> babase.AppTime: 541 """Return the current app-time in seconds. 542 543 App-time is a monotonic time value; it starts at 0.0 when the app 544 launches and will never jump by large amounts or go backwards, even if 545 the system time changes. Its progression will pause when the app is in 546 a suspended state. 547 548 Note that the AppTime returned here is simply float; it just has a 549 unique type in the type-checker's eyes to help prevent it from being 550 accidentally used with time functionality expecting other time types. 551 """ 552 import babase # pylint: disable=cyclic-import 553 554 return babase.AppTime(0.0)
Return the current app-time in seconds.
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.
557def apptimer(time: float, call: Callable[[], Any]) -> None: 558 """Schedule a callable object to run based on app-time. 559 560 This function creates a one-off timer which cannot be canceled or 561 modified once created. If you require the ability to do so, or need 562 a repeating timer, use the babase.AppTimer class instead. 563 564 Args: 565 time: Length of time in seconds that the timer will wait before 566 firing. 567 568 call: A callable Python object. Note that the timer will retain a 569 strong reference to the callable for as long as the timer 570 exists, so you may want to look into concepts such as 571 babase.WeakCall if that is not desired. 572 573 Example: Print some stuff through time: 574 >>> babase.screenmessage('hello from now!') 575 >>> babase.apptimer(1.0, babase.Call(babase.screenmessage, 576 ... 'hello from the future!')) 577 >>> babase.apptimer(2.0, babase.Call(babase.screenmessage, 578 ... 'hello from the future 2!')) 579 """ 580 return None
Schedule a callable object to run based on app-time.
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.
Args: time: Length of time in seconds that the timer will wait before firing.
call: 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.
Example: 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 This class encapsulates a timer based on app-time. 59 The underlying timer will be destroyed when this object is no longer 60 referenced. If you do not want to worry about keeping a reference to 61 your timer around, use the babase.apptimer() function instead to get a 62 one-off timer. 63 64 ##### Arguments 65 ###### time 66 > Length of time in seconds that the timer will wait before firing. 67 68 ###### call 69 > A callable Python object. Remember that the timer will retain a 70 strong reference to the callable for as long as it exists, so you 71 may want to look into concepts such as babase.WeakCall if that is not 72 desired. 73 74 ###### repeat 75 > If True, the timer will fire repeatedly, with each successive 76 firing having the same delay as the first. 77 78 ##### Example 79 80 Use a Timer object to print repeatedly for a few seconds: 81 ... def say_it(): 82 ... babase.screenmessage('BADGER!') 83 ... def stop_saying_it(): 84 ... global g_timer 85 ... g_timer = None 86 ... babase.screenmessage('MUSHROOM MUSHROOM!') 87 ... # Create our timer; it will run as long as we have the self.t ref. 88 ... g_timer = babase.AppTimer(0.3, say_it, repeat=True) 89 ... # Now fire off a one-shot timer to kill it. 90 ... babase.apptimer(3.89, stop_saying_it) 91 """ 92 93 def __init__( 94 self, time: float, call: Callable[[], Any], repeat: bool = False 95 ) -> None: 96 pass
Timers are used to run code at later points in time.
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)
293class BasicMainWindowState(MainWindowState): 294 """A basic MainWindowState holding a lambda to recreate a MainWindow.""" 295 296 def __init__( 297 self, 298 create_call: Callable[ 299 [ 300 Literal['in_right', 'in_left', 'in_scale'] | None, 301 bauiv1.Widget | None, 302 ], 303 bauiv1.MainWindow, 304 ], 305 ) -> None: 306 super().__init__() 307 self.create_call = create_call 308 309 @override 310 def create_window( 311 self, 312 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 313 origin_widget: bauiv1.Widget | None = None, 314 ) -> bauiv1.MainWindow: 315 return self.create_call(transition, origin_widget)
A basic MainWindowState holding a lambda to recreate a MainWindow.
309 @override 310 def create_window( 311 self, 312 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 313 origin_widget: bauiv1.Widget | None = None, 314 ) -> bauiv1.MainWindow: 315 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.
598def charstr(char_id: babase.SpecialChar) -> str: 599 """Get a unicode string representing a special character. 600 601 Note that these utilize the private-use block of unicode characters 602 (U+E000-U+F8FF) and are specific to the game; exporting or rendering 603 them elsewhere will be meaningless. 604 605 See babase.SpecialChar for the list of available characters. 606 """ 607 return str()
Get a unicode string representing a special character.
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.
199def checkboxwidget( 200 *, 201 edit: bauiv1.Widget | None = None, 202 parent: bauiv1.Widget | None = None, 203 size: Sequence[float] | None = None, 204 position: Sequence[float] | None = None, 205 text: str | bauiv1.Lstr | None = None, 206 value: bool | None = None, 207 on_value_change_call: Callable[[bool], None] | None = None, 208 on_select_call: Callable[[], None] | None = None, 209 text_scale: float | None = None, 210 textcolor: Sequence[float] | None = None, 211 scale: float | None = None, 212 is_radio_button: bool | None = None, 213 maxwidth: float | None = None, 214 autoselect: bool | None = None, 215 color: Sequence[float] | None = None, 216) -> bauiv1.Widget: 217 """Create or edit a check-box widget. 218 219 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 220 a new one is created and returned. Arguments that are not set to None 221 are applied to the Widget. 222 """ 223 import bauiv1 # pylint: disable=cyclic-import 224 225 return bauiv1.Widget()
Create or edit a check-box widget.
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.
628def clipboard_is_supported() -> bool: 629 """Return whether this platform supports clipboard operations at all. 630 631 If this returns False, UIs should not show 'copy to clipboard' 632 buttons, etc. 633 """ 634 return bool()
Return whether this platform supports clipboard operations at all.
If this returns False, UIs should not show 'copy to clipboard' buttons, etc.
637def clipboard_set_text(value: str) -> None: 638 """Copy a string to the system clipboard. 639 640 Ensure that babase.clipboard_is_supported() returns True before adding 641 buttons/etc. that make use of this functionality. 642 """ 643 return None
Copy a string to the system clipboard.
Ensure that babase.clipboard_is_supported() returns True before adding buttons/etc. that make use of this functionality.
228def columnwidget( 229 *, 230 edit: bauiv1.Widget | None = None, 231 parent: bauiv1.Widget | None = None, 232 size: Sequence[float] | None = None, 233 position: Sequence[float] | None = None, 234 background: bool | None = None, 235 selected_child: bauiv1.Widget | None = None, 236 visible_child: bauiv1.Widget | None = None, 237 single_depth: bool | None = None, 238 print_list_exit_instructions: bool | None = None, 239 left_border: float | None = None, 240 top_border: float | None = None, 241 bottom_border: float | None = None, 242 selection_loops_to_parent: bool | None = None, 243 border: float | None = None, 244 margin: float | None = None, 245 claims_left_right: bool | None = None, 246) -> bauiv1.Widget: 247 """Create or edit a column widget. 248 249 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 250 a new one is created and returned. Arguments that are not set to None 251 are applied to the Widget. 252 """ 253 import bauiv1 # pylint: disable=cyclic-import 254 255 return bauiv1.Widget()
Create or edit a column widget.
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.
258def containerwidget( 259 *, 260 edit: bauiv1.Widget | None = None, 261 parent: bauiv1.Widget | None = None, 262 id: str | None = None, 263 size: Sequence[float] | None = None, 264 position: Sequence[float] | None = None, 265 background: bool | None = None, 266 selected_child: bauiv1.Widget | None = None, 267 transition: str | None = None, 268 cancel_button: bauiv1.Widget | None = None, 269 start_button: bauiv1.Widget | None = None, 270 root_selectable: bool | None = None, 271 on_activate_call: Callable[[], None] | None = None, 272 claims_left_right: bool | None = None, 273 selection_loops: bool | None = None, 274 selection_loops_to_parent: bool | None = None, 275 scale: float | None = None, 276 on_outside_click_call: Callable[[], None] | None = None, 277 single_depth: bool | None = None, 278 visible_child: bauiv1.Widget | None = None, 279 stack_offset: Sequence[float] | None = None, 280 color: Sequence[float] | None = None, 281 on_cancel_call: Callable[[], None] | None = None, 282 print_list_exit_instructions: bool | None = None, 283 click_activate: bool | None = None, 284 always_highlight: bool | None = None, 285 selectable: bool | None = None, 286 scale_origin_stack_offset: Sequence[float] | None = None, 287 toolbar_visibility: ( 288 Literal[ 289 'menu_minimal', 290 'menu_minimal_no_back', 291 'menu_full', 292 'menu_full_no_back', 293 'menu_store', 294 'menu_store_no_back', 295 'menu_in_game', 296 'menu_tokens', 297 'get_tokens', 298 'no_menu_minimal', 299 'inherit', 300 ] 301 | None 302 ) = None, 303 on_select_call: Callable[[], None] | None = None, 304 claim_outside_clicks: bool | None = None, 305 claims_up_down: bool | None = None, 306) -> bauiv1.Widget: 307 """Create or edit a container widget. 308 309 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 310 a new one is created and returned. Arguments that are not set to None 311 are applied to the Widget. 312 """ 313 import bauiv1 # pylint: disable=cyclic-import 314 315 return bauiv1.Widget()
Create or edit a container widget.
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.
146class ContextRef: 147 """Store or use a ballistica context. 148 149 Many operations such as bascenev1.newnode() or bascenev1.gettexture() 150 operate implicitly on a current 'context'. A context is some sort of 151 state that functionality can implicitly use. Context determines, for 152 example, which scene nodes or textures get added to without having to 153 specify it explicitly in the newnode()/gettexture() call. Contexts can 154 also affect object lifecycles; for example a babase.ContextCall will 155 become a no-op when the context it was created in is destroyed. 156 157 In general, if you are a modder, you should not need to worry about 158 contexts; mod code should mostly be getting run in the correct 159 context and timers and other callbacks will take care of saving 160 and restoring contexts automatically. There may be rare cases, 161 however, where you need to deal directly with contexts, and that is 162 where this class comes in. 163 164 Creating a babase.ContextRef() will capture a reference to the current 165 context. Other modules may provide ways to access their contexts; for 166 example a bascenev1.Activity instance has a 'context' attribute. You 167 can also use babase.ContextRef.empty() to create a reference to *no* 168 context. Some code such as UI calls may expect this and may complain 169 if you try to use them within a context. 170 171 ##### Usage 172 ContextRefs are generally used with the Python 'with' statement, which 173 sets the context they point to as current on entry and resets it to 174 the previous value on exit. 175 176 ##### Example 177 Explicitly create a few UI bits with no context set. 178 (UI stuff may complain if called within a context): 179 >>> with bui.ContextRef.empty(): 180 ... my_container = bui.containerwidget() 181 """ 182 183 def __init__( 184 self, 185 ) -> None: 186 pass 187 188 def __enter__(self) -> None: 189 """Support for "with" statement.""" 190 pass 191 192 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: 193 """Support for "with" statement.""" 194 pass 195 196 @classmethod 197 def empty(cls) -> ContextRef: 198 """Return a ContextRef pointing to no context. 199 200 This is useful when code should be run free of a context. 201 For example, UI code generally insists on being run this way. 202 Otherwise, callbacks set on the UI could inadvertently stop working 203 due to a game activity ending, which would be unintuitive behavior. 204 """ 205 return ContextRef() 206 207 def is_empty(self) -> bool: 208 """Whether the context was created as empty.""" 209 return bool() 210 211 def is_expired(self) -> bool: 212 """Whether the context has expired.""" 213 return bool()
Store or use a ballistica context.
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()
196 @classmethod 197 def empty(cls) -> ContextRef: 198 """Return a ContextRef pointing to no context. 199 200 This is useful when code should be run free of a context. 201 For example, UI code generally insists on being run this way. 202 Otherwise, callbacks set on the UI could inadvertently stop working 203 due to a game activity ending, which would be unintuitive behavior. 204 """ 205 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.
729def displaytime() -> babase.DisplayTime: 730 """Return the current display-time in seconds. 731 732 Display-time is a time value intended to be used for animation and other 733 visual purposes. It will generally increment by a consistent amount each 734 frame. It will pass at an overall similar rate to AppTime, but trades 735 accuracy for smoothness. 736 737 Note that the value returned here is simply a float; it just has a 738 unique type in the type-checker's eyes to help prevent it from being 739 accidentally used with time functionality expecting other time types. 740 """ 741 import babase # pylint: disable=cyclic-import 742 743 return babase.DisplayTime(0.0)
Return the current display-time in seconds.
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.
746def displaytimer(time: float, call: Callable[[], Any]) -> None: 747 """Schedule a callable object to run based on display-time. 748 749 This function creates a one-off timer which cannot be canceled or 750 modified once created. If you require the ability to do so, or need 751 a repeating timer, use the babase.DisplayTimer class instead. 752 753 Display-time is a time value intended to be used for animation and other 754 visual purposes. It will generally increment by a consistent amount each 755 frame. It will pass at an overall similar rate to AppTime, but trades 756 accuracy for smoothness. 757 758 ##### Arguments 759 ###### time (float) 760 > Length of time in seconds that the timer will wait before firing. 761 762 ###### call (Callable[[], Any]) 763 > A callable Python object. Note that the timer will retain a 764 strong reference to the callable for as long as the timer exists, so you 765 may want to look into concepts such as babase.WeakCall if that is not 766 desired. 767 768 ##### Examples 769 Print some stuff through time: 770 >>> babase.screenmessage('hello from now!') 771 >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage, 772 ... 'hello from the future!')) 773 >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage, 774 ... 'hello from the future 2!')) 775 """ 776 return None
Schedule a callable object to run based on display-time.
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!'))
216class DisplayTimer: 217 """Timers are used to run code at later points in time. 218 219 This class encapsulates a timer based on display-time. 220 The underlying timer will be destroyed when this object is no longer 221 referenced. If you do not want to worry about keeping a reference to 222 your timer around, use the babase.displaytimer() function instead to get a 223 one-off timer. 224 225 Display-time is a time value intended to be used for animation and 226 other visual purposes. It will generally increment by a consistent 227 amount each frame. It will pass at an overall similar rate to AppTime, 228 but trades accuracy for smoothness. 229 230 ##### Arguments 231 ###### time 232 > Length of time in seconds that the timer will wait before firing. 233 234 ###### call 235 > A callable Python object. Remember that the timer will retain a 236 strong reference to the callable for as long as it exists, so you 237 may want to look into concepts such as babase.WeakCall if that is not 238 desired. 239 240 ###### repeat 241 > If True, the timer will fire repeatedly, with each successive 242 firing having the same delay as the first. 243 244 ##### Example 245 246 Use a Timer object to print repeatedly for a few seconds: 247 ... def say_it(): 248 ... babase.screenmessage('BADGER!') 249 ... def stop_saying_it(): 250 ... global g_timer 251 ... g_timer = None 252 ... babase.screenmessage('MUSHROOM MUSHROOM!') 253 ... # Create our timer; it will run as long as we have the self.t ref. 254 ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) 255 ... # Now fire off a one-shot timer to kill it. 256 ... babase.displaytimer(3.89, stop_saying_it) 257 """ 258 259 def __init__( 260 self, time: float, call: Callable[[], Any], repeat: bool = False 261 ) -> None: 262 pass
Timers are used to run code at later points in time.
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)
784def do_once() -> bool: 785 """Return whether this is the first time running a line of code. 786 787 This is used by 'print_once()' type calls to keep from overflowing 788 logs. The call functions by registering the filename and line where 789 The call is made from. Returns True if this location has not been 790 registered already, and False if it has. 791 792 ##### Example 793 This print will only fire for the first loop iteration: 794 >>> for i in range(10): 795 ... if babase.do_once(): 796 ... print('HelloWorld once from loop!') 797 """ 798 return bool()
Return whether this is the first time running a line of code.
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!')
48def existing(obj: ExistableT | None) -> ExistableT | None: 49 """Convert invalid references to None for any babase.Existable object. 50 51 To best support type checking, it is important that invalid references 52 not be passed around and instead get converted to values of None. 53 That way the type checker can properly flag attempts to pass possibly-dead 54 objects (FooType | None) into functions expecting only live ones 55 (FooType), etc. This call can be used on any 'existable' object 56 (one with an exists() method) and will convert it to a None value 57 if it does not exist. 58 59 For more info, see notes on 'existables' here: 60 https://ballistica.net/wiki/Coding-Style-Guide 61 """ 62 assert obj is None or hasattr(obj, 'exists'), f'No "exists" attr on {obj}.' 63 return obj if obj is not None and obj.exists() else None
Convert invalid references to None for any babase.Existable object.
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
967def get_input_idle_time() -> float: 968 """Return seconds since any local input occurred (touch, keypress, etc.).""" 969 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.
318def get_qrcode_texture(url: str) -> bauiv1.Texture: 319 """Return a QR code texture. 320 321 The provided url must be 64 bytes or less. 322 """ 323 import bauiv1 # pylint: disable=cyclic-import 324 325 return bauiv1.Texture()
Return a QR code texture.
The provided url must be 64 bytes or less.
105def get_type_name(cls: type) -> str: 106 """Return a full type name including module for a class.""" 107 return f'{cls.__module__}.{cls.__name__}'
Return a full type name including module for a class.
66def getclass( 67 name: str, subclassof: type[T], check_sdlib_modulename_clash: bool = False 68) -> type[T]: 69 """Given a full class name such as foo.bar.MyClass, return the class. 70 71 The class will be checked to make sure it is a subclass of the provided 72 'subclassof' class, and a TypeError will be raised if not. 73 """ 74 import importlib 75 76 splits = name.split('.') 77 modulename = '.'.join(splits[:-1]) 78 classname = splits[-1] 79 if modulename in sys.stdlib_module_names and check_sdlib_modulename_clash: 80 raise Exception(f'{modulename} is an inbuilt module.') 81 module = importlib.import_module(modulename) 82 cls: type = getattr(module, classname) 83 84 if not issubclass(cls, subclassof): 85 raise TypeError(f'{name} is not a subclass of {subclassof}.') 86 return cls
Given a full class name such as foo.bar.MyClass, return the class.
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.
356def getmesh(name: str) -> bauiv1.Mesh: 357 """Load a mesh for use solely in the local user interface.""" 358 import bauiv1 # pylint: disable=cyclic-import 359 360 return bauiv1.Mesh()
Load a mesh for use solely in the local user interface.
363def getsound(name: str) -> bauiv1.Sound: 364 """Load a sound for use in the ui.""" 365 import bauiv1 # pylint: disable=cyclic-import 366 367 return bauiv1.Sound()
Load a sound for use in the ui.
370def gettexture(name: str) -> bauiv1.Texture: 371 """Load a texture for use in the ui.""" 372 import bauiv1 # pylint: disable=cyclic-import 373 374 return bauiv1.Texture()
Load a texture for use in the ui.
377def hscrollwidget( 378 *, 379 edit: bauiv1.Widget | None = None, 380 parent: bauiv1.Widget | None = None, 381 size: Sequence[float] | None = None, 382 position: Sequence[float] | None = None, 383 background: bool | None = None, 384 selected_child: bauiv1.Widget | None = None, 385 capture_arrows: bool | None = None, 386 on_select_call: Callable[[], None] | None = None, 387 center_small_content: bool | None = None, 388 color: Sequence[float] | None = None, 389 highlight: bool | None = None, 390 border_opacity: float | None = None, 391 simple_culling_h: float | None = None, 392 claims_left_right: bool | None = None, 393 claims_up_down: bool | None = None, 394) -> bauiv1.Widget: 395 """Create or edit a horizontal scroll widget. 396 397 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 398 a new one is created and returned. Arguments that are not set to None 399 are applied to the Widget. 400 """ 401 import bauiv1 # pylint: disable=cyclic-import 402 403 return bauiv1.Widget()
Create or edit a horizontal scroll widget.
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.
406def imagewidget( 407 *, 408 edit: bauiv1.Widget | None = None, 409 parent: bauiv1.Widget | None = None, 410 size: Sequence[float] | None = None, 411 position: Sequence[float] | None = None, 412 color: Sequence[float] | None = None, 413 texture: bauiv1.Texture | None = None, 414 opacity: float | None = None, 415 mesh_transparent: bauiv1.Mesh | None = None, 416 mesh_opaque: bauiv1.Mesh | None = None, 417 has_alpha_channel: bool = True, 418 tint_texture: bauiv1.Texture | None = None, 419 tint_color: Sequence[float] | None = None, 420 transition_delay: float | None = None, 421 draw_controller: bauiv1.Widget | None = None, 422 tint2_color: Sequence[float] | None = None, 423 tilt_scale: float | None = None, 424 mask_texture: bauiv1.Texture | None = None, 425 radial_amount: float | None = None, 426 draw_controller_mult: float | None = None, 427) -> bauiv1.Widget: 428 """Create or edit an image widget. 429 430 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 431 a new one is created and returned. Arguments that are not set to None 432 are applied to the Widget. 433 """ 434 import bauiv1 # pylint: disable=cyclic-import 435 436 return bauiv1.Widget()
Create or edit an image widget.
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 Keyboards are discoverable by the meta-tag system 18 and the user can select which one they want to use. 19 On-screen keyboard uses chars from active babase.Keyboard. 20 """ 21 22 name: str 23 """Displays when user selecting this keyboard.""" 24 25 chars: list[tuple[str, ...]] 26 """Used for row/column lengths.""" 27 28 pages: dict[str, tuple[str, ...]] 29 """Extra chars like emojis.""" 30 31 nums: tuple[str, ...] 32 """The 'num' page."""
Chars definitions for on-screen keyboard.
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.
494class Lstr: 495 """Used to define strings in a language-independent way. 496 497 These should be used whenever possible in place of hard-coded 498 strings so that in-game or UI elements show up correctly on all 499 clients in their currently active language. 500 501 To see available resource keys, look at any of the 502 ``bs_language_*.py`` files in the game or the translations pages at 503 `legacy.ballistica.net/translate 504 <https://legacy.ballistica.net/translate>`. 505 506 Examples 507 -------- 508 509 **Example 1: Specify a String from a Resource Path**:: 510 511 mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText') 512 513 **Example 2: Specify a Translated String via a Category and English Value** 514 515 If a translated value is available, it will be used; otherwise, the 516 English value will be. To see available translation categories, look 517 under the ``translations`` resource section:: 518 519 mynode.text = babase.Lstr(translate=('gameDescriptions', 520 'Defeat all enemies')) 521 522 **Example 3: Specify a Raw Value with Substitutions** 523 524 Substitutions can be used with ``resource`` and ``translate`` modes 525 as well:: 526 527 mynode.text = babase.Lstr(value='${A} / ${B}', 528 subs=[('${A}', str(score)), 529 ('${B}', str(total))]) 530 531 **Example 4: Nesting** 532 533 :class:`~babase.Lstr` instances can be nested. This example would display 534 the resource at ``res_a`` but replace ``${NAME}`` with the value of 535 the resource at ``res_b``:: 536 537 mytextnode.text = babase.Lstr( 538 resource='res_a', 539 subs=[('${NAME}', babase.Lstr(resource='res_b'))]) 540 """ 541 542 # This class is used a lot in UI stuff and doesn't need to be 543 # flexible, so let's optimize its performance a bit. 544 __slots__ = ['args'] 545 546 @overload 547 def __init__( 548 self, 549 *, 550 resource: str, 551 fallback_resource: str = '', 552 fallback_value: str = '', 553 subs: Sequence[tuple[str, str | Lstr]] | None = None, 554 ) -> None: 555 """Create an Lstr from a string resource.""" 556 557 @overload 558 def __init__( 559 self, 560 *, 561 translate: tuple[str, str], 562 subs: Sequence[tuple[str, str | Lstr]] | None = None, 563 ) -> None: 564 """Create an Lstr by translating a string in a category.""" 565 566 @overload 567 def __init__( 568 self, 569 *, 570 value: str, 571 subs: Sequence[tuple[str, str | Lstr]] | None = None, 572 ) -> None: 573 """Create an Lstr from a raw string value.""" 574 575 def __init__(self, *args: Any, **keywds: Any) -> None: 576 """Instantiate a Lstr. 577 578 Pass a value for either 'resource', 'translate', 579 or 'value'. (see Lstr help for examples). 580 'subs' can be a sequence of 2-member sequences consisting of values 581 and replacements. 582 'fallback_resource' can be a resource key that will be used if the 583 main one is not present for 584 the current language in place of falling back to the english value 585 ('resource' mode only). 586 'fallback_value' can be a literal string that will be used if neither 587 the resource nor the fallback resource is found ('resource' mode only). 588 """ 589 # pylint: disable=too-many-branches 590 if args: 591 raise TypeError('Lstr accepts only keyword arguments') 592 593 # Basically just store the exact args they passed. However if 594 # they passed any Lstr values for subs, replace them with that 595 # Lstr's dict. 596 self.args = keywds 597 our_type = type(self) 598 599 if isinstance(self.args.get('value'), our_type): 600 raise TypeError("'value' must be a regular string; not an Lstr") 601 602 if 'subs' in keywds: 603 subs = keywds.get('subs') 604 subs_filtered = [] 605 if subs is not None: 606 for key, value in keywds['subs']: 607 if isinstance(value, our_type): 608 subs_filtered.append((key, value.args)) 609 else: 610 subs_filtered.append((key, value)) 611 self.args['subs'] = subs_filtered 612 613 # As of protocol 31 we support compact key names ('t' instead of 614 # 'translate', etc). Convert as needed. 615 if 'translate' in keywds: 616 keywds['t'] = keywds['translate'] 617 del keywds['translate'] 618 if 'resource' in keywds: 619 keywds['r'] = keywds['resource'] 620 del keywds['resource'] 621 if 'value' in keywds: 622 keywds['v'] = keywds['value'] 623 del keywds['value'] 624 if 'fallback' in keywds: 625 from babase import _error 626 627 _error.print_error( 628 'deprecated "fallback" arg passed to Lstr(); use ' 629 'either "fallback_resource" or "fallback_value"', 630 once=True, 631 ) 632 keywds['f'] = keywds['fallback'] 633 del keywds['fallback'] 634 if 'fallback_resource' in keywds: 635 keywds['f'] = keywds['fallback_resource'] 636 del keywds['fallback_resource'] 637 if 'subs' in keywds: 638 keywds['s'] = keywds['subs'] 639 del keywds['subs'] 640 if 'fallback_value' in keywds: 641 keywds['fv'] = keywds['fallback_value'] 642 del keywds['fallback_value'] 643 644 def evaluate(self) -> str: 645 """Evaluate the Lstr and returns a flat string in the current language. 646 647 You should avoid doing this as much as possible and instead pass 648 and store Lstr values. 649 """ 650 return _babase.evaluate_lstr(self._get_json()) 651 652 def is_flat_value(self) -> bool: 653 """Return whether the Lstr is a 'flat' value. 654 655 This is defined as a simple string value incorporating no 656 translations, resources, or substitutions. In this case it may 657 be reasonable to replace it with a raw string value, perform 658 string manipulation on it, etc. 659 """ 660 return bool('v' in self.args and not self.args.get('s', [])) 661 662 def _get_json(self) -> str: 663 try: 664 return json.dumps(self.args, separators=(',', ':')) 665 except Exception: 666 from babase import _error 667 668 _error.print_exception('_get_json failed for', self.args) 669 return 'JSON_ERR' 670 671 @override 672 def __str__(self) -> str: 673 return '<ba.Lstr: ' + self._get_json() + '>' 674 675 @override 676 def __repr__(self) -> str: 677 return '<ba.Lstr: ' + self._get_json() + '>' 678 679 @staticmethod 680 def from_json(json_string: str) -> babase.Lstr: 681 """Given a json string, returns a babase.Lstr. Does no validation.""" 682 lstr = Lstr(value='') 683 lstr.args = json.loads(json_string) 684 return lstr
Used to define strings in a language-independent way.
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
<https://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 with 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: Nesting
~babase.Lstr
instances 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'))])
575 def __init__(self, *args: Any, **keywds: Any) -> None: 576 """Instantiate a Lstr. 577 578 Pass a value for either 'resource', 'translate', 579 or 'value'. (see Lstr help for examples). 580 'subs' can be a sequence of 2-member sequences consisting of values 581 and replacements. 582 'fallback_resource' can be a resource key that will be used if the 583 main one is not present for 584 the current language in place of falling back to the english value 585 ('resource' mode only). 586 'fallback_value' can be a literal string that will be used if neither 587 the resource nor the fallback resource is found ('resource' mode only). 588 """ 589 # pylint: disable=too-many-branches 590 if args: 591 raise TypeError('Lstr accepts only keyword arguments') 592 593 # Basically just store the exact args they passed. However if 594 # they passed any Lstr values for subs, replace them with that 595 # Lstr's dict. 596 self.args = keywds 597 our_type = type(self) 598 599 if isinstance(self.args.get('value'), our_type): 600 raise TypeError("'value' must be a regular string; not an Lstr") 601 602 if 'subs' in keywds: 603 subs = keywds.get('subs') 604 subs_filtered = [] 605 if subs is not None: 606 for key, value in keywds['subs']: 607 if isinstance(value, our_type): 608 subs_filtered.append((key, value.args)) 609 else: 610 subs_filtered.append((key, value)) 611 self.args['subs'] = subs_filtered 612 613 # As of protocol 31 we support compact key names ('t' instead of 614 # 'translate', etc). Convert as needed. 615 if 'translate' in keywds: 616 keywds['t'] = keywds['translate'] 617 del keywds['translate'] 618 if 'resource' in keywds: 619 keywds['r'] = keywds['resource'] 620 del keywds['resource'] 621 if 'value' in keywds: 622 keywds['v'] = keywds['value'] 623 del keywds['value'] 624 if 'fallback' in keywds: 625 from babase import _error 626 627 _error.print_error( 628 'deprecated "fallback" arg passed to Lstr(); use ' 629 'either "fallback_resource" or "fallback_value"', 630 once=True, 631 ) 632 keywds['f'] = keywds['fallback'] 633 del keywds['fallback'] 634 if 'fallback_resource' in keywds: 635 keywds['f'] = keywds['fallback_resource'] 636 del keywds['fallback_resource'] 637 if 'subs' in keywds: 638 keywds['s'] = keywds['subs'] 639 del keywds['subs'] 640 if 'fallback_value' in keywds: 641 keywds['fv'] = keywds['fallback_value'] 642 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).
644 def evaluate(self) -> str: 645 """Evaluate the Lstr and returns a flat string in the current language. 646 647 You should avoid doing this as much as possible and instead pass 648 and store Lstr values. 649 """ 650 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.
652 def is_flat_value(self) -> bool: 653 """Return whether the Lstr is a 'flat' value. 654 655 This is defined as a simple string value incorporating no 656 translations, resources, or substitutions. In this case it may 657 be reasonable to replace it with a raw string value, perform 658 string manipulation on it, etc. 659 """ 660 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.
67class MainWindow(Window): 68 """A special type of window that can be set as 'main'. 69 70 The UI system has at most one main window at any given time. 71 MainWindows support high level functionality such as saving and 72 restoring states, allowing them to be automatically recreated when 73 navigating back from other locations or when something like ui-scale 74 changes. 75 """ 76 77 def __init__( 78 self, 79 root_widget: bauiv1.Widget, 80 *, 81 transition: str | None, 82 origin_widget: bauiv1.Widget | None, 83 cleanupcheck: bool = True, 84 refresh_on_screen_size_changes: bool = False, 85 ): 86 """Create a MainWindow given a root widget and transition info. 87 88 Automatically handles in and out transitions on the provided 89 widget, so there is no need to set transitions when creating it. 90 """ 91 # A back-state supplied by the ui system. 92 self.main_window_back_state: MainWindowState | None = None 93 94 self.main_window_is_top_level: bool = False 95 96 # Windows that size tailor themselves to exact screen dimensions 97 # can pass True for this. Generally this only applies to small 98 # ui scale and at larger scales windows simply fit in the 99 # virtual safe area. 100 self.refreshes_on_screen_size_changes = refresh_on_screen_size_changes 101 102 # Windows can be flagged as auxiliary when not related to the 103 # main UI task at hand. UI code may choose to handle auxiliary 104 # windows in special ways, such as by implicitly replacing 105 # existing auxiliary windows with new ones instead of keeping 106 # old ones as back targets. 107 self.main_window_is_auxiliary: bool = False 108 109 self._main_window_transition = transition 110 self._main_window_origin_widget = origin_widget 111 super().__init__( 112 root_widget, 113 cleanupcheck=cleanupcheck, 114 prevent_main_window_auto_recreate=False, 115 ) 116 117 scale_origin: tuple[float, float] | None 118 if origin_widget is not None: 119 self._main_window_transition_out = 'out_scale' 120 scale_origin = origin_widget.get_screen_space_center() 121 transition = 'in_scale' 122 else: 123 self._main_window_transition_out = 'out_right' 124 scale_origin = None 125 _bauiv1.containerwidget( 126 edit=root_widget, 127 transition=transition, 128 scale_origin_stack_offset=scale_origin, 129 ) 130 131 def main_window_close(self, transition: str | None = None) -> None: 132 """Get window transitioning out if still alive.""" 133 134 # no-op if our underlying widget is dead or on its way out. 135 if not self._root_widget or self._root_widget.transitioning_out: 136 return 137 138 # Transition ourself out. 139 try: 140 self.on_main_window_close() 141 except Exception: 142 logging.exception('Error in on_main_window_close() for %s.', self) 143 144 # Note: normally transition of None means instant, but we use 145 # that to mean 'do the default' so we support a special 146 # 'instant' string. 147 if transition == 'instant': 148 self._root_widget.delete() 149 else: 150 _bauiv1.containerwidget( 151 edit=self._root_widget, 152 transition=( 153 self._main_window_transition_out 154 if transition is None 155 else transition 156 ), 157 ) 158 159 def main_window_has_control(self) -> bool: 160 """Is this MainWindow allowed to change the global main window? 161 162 It is a good idea to make sure this is True before calling 163 main_window_replace(). This prevents fluke UI breakage such as 164 multiple simultaneous events causing a MainWindow to spawn 165 multiple replacements for itself. 166 """ 167 # We are allowed to change main windows if we are the current one 168 # AND our underlying widget is still alive and not transitioning out. 169 return ( 170 babase.app.ui_v1.get_main_window() is self 171 and bool(self._root_widget) 172 and not self._root_widget.transitioning_out 173 ) 174 175 def main_window_back(self) -> None: 176 """Move back in the main window stack. 177 178 Is a no-op if the main window does not have control; 179 no need to check main_window_has_control() first. 180 """ 181 182 # Users should always check main_window_has_control() before 183 # calling us. Error if it seems they did not. 184 if not self.main_window_has_control(): 185 return 186 187 uiv1 = babase.app.ui_v1 188 189 # Get the 'back' window coming in. 190 if not self.main_window_is_top_level: 191 192 back_state = self.main_window_back_state 193 if back_state is None: 194 raise RuntimeError( 195 f'Main window {self} provides no back-state.' 196 ) 197 198 # Valid states should have values here. 199 assert back_state.is_top_level is not None 200 assert back_state.is_auxiliary is not None 201 assert back_state.window_type is not None 202 203 backwin = back_state.create_window(transition='in_left') 204 205 uiv1.set_main_window( 206 backwin, 207 from_window=self, 208 is_back=True, 209 back_state=back_state, 210 suppress_warning=True, 211 ) 212 213 # Transition ourself out. 214 self.main_window_close() 215 216 def main_window_replace( 217 self, 218 new_window: MainWindow, 219 back_state: MainWindowState | None = None, 220 is_auxiliary: bool = False, 221 ) -> None: 222 """Replace ourself with a new MainWindow.""" 223 224 # Users should always check main_window_has_control() *before* 225 # creating new MainWindows and passing them in here. Kill the 226 # passed window and Error if it seems they did not. 227 if not self.main_window_has_control(): 228 new_window.get_root_widget().delete() 229 raise RuntimeError( 230 f'main_window_replace() called on a not-in-control window' 231 f' ({self}); always check main_window_has_control() before' 232 f' calling main_window_replace().' 233 ) 234 235 # Just shove the old out the left to give the feel that we're 236 # adding to the nav stack. 237 transition = 'out_left' 238 239 # Transition ourself out. 240 try: 241 self.on_main_window_close() 242 except Exception: 243 logging.exception('Error in on_main_window_close() for %s.', self) 244 245 _bauiv1.containerwidget(edit=self._root_widget, transition=transition) 246 babase.app.ui_v1.set_main_window( 247 new_window, 248 from_window=self, 249 back_state=back_state, 250 is_auxiliary=is_auxiliary, 251 suppress_warning=True, 252 ) 253 254 def on_main_window_close(self) -> None: 255 """Called before transitioning out a main window. 256 257 A good opportunity to save window state/etc. 258 """ 259 260 def get_main_window_state(self) -> MainWindowState: 261 """Return a WindowState to recreate this window, if supported.""" 262 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.
77 def __init__( 78 self, 79 root_widget: bauiv1.Widget, 80 *, 81 transition: str | None, 82 origin_widget: bauiv1.Widget | None, 83 cleanupcheck: bool = True, 84 refresh_on_screen_size_changes: bool = False, 85 ): 86 """Create a MainWindow given a root widget and transition info. 87 88 Automatically handles in and out transitions on the provided 89 widget, so there is no need to set transitions when creating it. 90 """ 91 # A back-state supplied by the ui system. 92 self.main_window_back_state: MainWindowState | None = None 93 94 self.main_window_is_top_level: bool = False 95 96 # Windows that size tailor themselves to exact screen dimensions 97 # can pass True for this. Generally this only applies to small 98 # ui scale and at larger scales windows simply fit in the 99 # virtual safe area. 100 self.refreshes_on_screen_size_changes = refresh_on_screen_size_changes 101 102 # Windows can be flagged as auxiliary when not related to the 103 # main UI task at hand. UI code may choose to handle auxiliary 104 # windows in special ways, such as by implicitly replacing 105 # existing auxiliary windows with new ones instead of keeping 106 # old ones as back targets. 107 self.main_window_is_auxiliary: bool = False 108 109 self._main_window_transition = transition 110 self._main_window_origin_widget = origin_widget 111 super().__init__( 112 root_widget, 113 cleanupcheck=cleanupcheck, 114 prevent_main_window_auto_recreate=False, 115 ) 116 117 scale_origin: tuple[float, float] | None 118 if origin_widget is not None: 119 self._main_window_transition_out = 'out_scale' 120 scale_origin = origin_widget.get_screen_space_center() 121 transition = 'in_scale' 122 else: 123 self._main_window_transition_out = 'out_right' 124 scale_origin = None 125 _bauiv1.containerwidget( 126 edit=root_widget, 127 transition=transition, 128 scale_origin_stack_offset=scale_origin, 129 )
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.
131 def main_window_close(self, transition: str | None = None) -> None: 132 """Get window transitioning out if still alive.""" 133 134 # no-op if our underlying widget is dead or on its way out. 135 if not self._root_widget or self._root_widget.transitioning_out: 136 return 137 138 # Transition ourself out. 139 try: 140 self.on_main_window_close() 141 except Exception: 142 logging.exception('Error in on_main_window_close() for %s.', self) 143 144 # Note: normally transition of None means instant, but we use 145 # that to mean 'do the default' so we support a special 146 # 'instant' string. 147 if transition == 'instant': 148 self._root_widget.delete() 149 else: 150 _bauiv1.containerwidget( 151 edit=self._root_widget, 152 transition=( 153 self._main_window_transition_out 154 if transition is None 155 else transition 156 ), 157 )
Get window transitioning out if still alive.
159 def main_window_has_control(self) -> bool: 160 """Is this MainWindow allowed to change the global main window? 161 162 It is a good idea to make sure this is True before calling 163 main_window_replace(). This prevents fluke UI breakage such as 164 multiple simultaneous events causing a MainWindow to spawn 165 multiple replacements for itself. 166 """ 167 # We are allowed to change main windows if we are the current one 168 # AND our underlying widget is still alive and not transitioning out. 169 return ( 170 babase.app.ui_v1.get_main_window() is self 171 and bool(self._root_widget) 172 and not self._root_widget.transitioning_out 173 )
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.
175 def main_window_back(self) -> None: 176 """Move back in the main window stack. 177 178 Is a no-op if the main window does not have control; 179 no need to check main_window_has_control() first. 180 """ 181 182 # Users should always check main_window_has_control() before 183 # calling us. Error if it seems they did not. 184 if not self.main_window_has_control(): 185 return 186 187 uiv1 = babase.app.ui_v1 188 189 # Get the 'back' window coming in. 190 if not self.main_window_is_top_level: 191 192 back_state = self.main_window_back_state 193 if back_state is None: 194 raise RuntimeError( 195 f'Main window {self} provides no back-state.' 196 ) 197 198 # Valid states should have values here. 199 assert back_state.is_top_level is not None 200 assert back_state.is_auxiliary is not None 201 assert back_state.window_type is not None 202 203 backwin = back_state.create_window(transition='in_left') 204 205 uiv1.set_main_window( 206 backwin, 207 from_window=self, 208 is_back=True, 209 back_state=back_state, 210 suppress_warning=True, 211 ) 212 213 # Transition ourself out. 214 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.
216 def main_window_replace( 217 self, 218 new_window: MainWindow, 219 back_state: MainWindowState | None = None, 220 is_auxiliary: bool = False, 221 ) -> None: 222 """Replace ourself with a new MainWindow.""" 223 224 # Users should always check main_window_has_control() *before* 225 # creating new MainWindows and passing them in here. Kill the 226 # passed window and Error if it seems they did not. 227 if not self.main_window_has_control(): 228 new_window.get_root_widget().delete() 229 raise RuntimeError( 230 f'main_window_replace() called on a not-in-control window' 231 f' ({self}); always check main_window_has_control() before' 232 f' calling main_window_replace().' 233 ) 234 235 # Just shove the old out the left to give the feel that we're 236 # adding to the nav stack. 237 transition = 'out_left' 238 239 # Transition ourself out. 240 try: 241 self.on_main_window_close() 242 except Exception: 243 logging.exception('Error in on_main_window_close() for %s.', self) 244 245 _bauiv1.containerwidget(edit=self._root_widget, transition=transition) 246 babase.app.ui_v1.set_main_window( 247 new_window, 248 from_window=self, 249 back_state=back_state, 250 is_auxiliary=is_auxiliary, 251 suppress_warning=True, 252 )
Replace ourself with a new MainWindow.
254 def on_main_window_close(self) -> None: 255 """Called before transitioning out a main window. 256 257 A good opportunity to save window state/etc. 258 """
Called before transitioning out a main window.
A good opportunity to save window state/etc.
265class MainWindowState: 266 """Persistent state for a specific MainWindow. 267 268 This allows MainWindows to be automatically recreated for back-button 269 purposes, when switching app-modes, etc. 270 """ 271 272 def __init__(self) -> None: 273 # The window that back/cancel navigation should take us to. 274 self.parent: MainWindowState | None = None 275 self.is_top_level: bool | None = None 276 self.is_auxiliary: bool | None = None 277 self.window_type: type[MainWindow] | None = None 278 self.selection: str | None = None 279 280 def create_window( 281 self, 282 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 283 origin_widget: bauiv1.Widget | None = None, 284 ) -> MainWindow: 285 """Create a window based on this state. 286 287 WindowState child classes should override this to recreate their 288 particular type of window. 289 """ 290 raise NotImplementedError()
Persistent state for a specific MainWindow.
This allows MainWindows to be automatically recreated for back-button purposes, when switching app-modes, etc.
280 def create_window( 281 self, 282 transition: Literal['in_right', 'in_left', 'in_scale'] | None = None, 283 origin_widget: bauiv1.Widget | None = None, 284 ) -> MainWindow: 285 """Create a window based on this state. 286 287 WindowState child classes should override this to recreate their 288 particular type of window. 289 """ 290 raise NotImplementedError()
Create a window based on this state.
WindowState child classes should override this to recreate their particular type of window.
Mesh asset for local user interface purposes.
25class NotFoundError(Exception): 26 """Exception raised when a referenced object does not exist."""
Exception raised when a referenced object does not exist.
1299def open_url(address: str, force_fallback: bool = False) -> None: 1300 """Open the provided URL. 1301 1302 Attempts to open the provided url in a web-browser. If that is not 1303 possible (or force_fallback is True), instead displays the url as 1304 a string and/or qrcode. 1305 """ 1306 return None
Open the provided URL.
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.
1309def overlay_web_browser_close() -> bool: 1310 """Close any open overlay web browser.""" 1311 return bool()
Close any open overlay web browser.
1314def overlay_web_browser_is_open() -> bool: 1315 """Return whether an overlay web browser is open currently.""" 1316 return bool()
Return whether an overlay web browser is open currently.
1319def overlay_web_browser_is_supported() -> bool: 1320 """Return whether an overlay web browser is supported here. 1321 1322 An overlay web browser is a small dialog that pops up over the top 1323 of the main engine window. It can be used for performing simple 1324 tasks such as sign-ins. 1325 """ 1326 return bool()
Return whether an overlay web browser is supported here.
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.
1329def overlay_web_browser_open_url(address: str) -> None: 1330 """Open the provided URL in an overlayw web browser. 1331 1332 An overlay web browser is a small dialog that pops up over the top 1333 of the main engine window. It can be used for performing simple 1334 tasks such as sign-ins. 1335 """ 1336 return None
Open the provided URL in an overlayw web browser.
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.
Permissions that can be requested from the OS.
317class Plugin: 318 """A plugin to alter app behavior in some way. 319 320 Plugins are discoverable by the meta-tag system 321 and the user can select which ones they want to enable. 322 Enabled plugins are then called at specific times as the 323 app is running in order to modify its behavior in some way. 324 """ 325 326 def on_app_running(self) -> None: 327 """Called when the app reaches the running state.""" 328 329 def on_app_suspend(self) -> None: 330 """Called when the app enters the suspended state.""" 331 332 def on_app_unsuspend(self) -> None: 333 """Called when the app exits the suspended state.""" 334 335 def on_app_shutdown(self) -> None: 336 """Called when the app is beginning the shutdown process.""" 337 338 def on_app_shutdown_complete(self) -> None: 339 """Called when the app has completed the shutdown process.""" 340 341 def has_settings_ui(self) -> bool: 342 """Called to ask if we have settings UI we can show.""" 343 return False 344 345 def show_settings_ui(self, source_widget: Any | None) -> None: 346 """Called to show our settings UI."""
A plugin to alter app behavior in some way.
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.
335 def on_app_shutdown(self) -> None: 336 """Called when the app is beginning the shutdown process."""
Called when the app is beginning the shutdown process.
338 def on_app_shutdown_complete(self) -> None: 339 """Called when the app has completed the shutdown process."""
Called when the app has completed the shutdown process.
224class PluginSpec: 225 """Represents a plugin the engine knows about. 226 227 The 'enabled' attr represents whether this plugin is set to load. 228 Getting or setting that attr affects the corresponding app-config 229 key. Remember to commit the app-config after making any changes. 230 231 The 'attempted_load' attr will be True if the engine has attempted 232 to load the plugin. If 'attempted_load' is True for a PluginSpec but 233 the 'plugin' attr is None, it means there was an error loading the 234 plugin. If a plugin's api-version does not match the running app, if 235 a new plugin is detected with auto-enable-plugins disabled, or if 236 the user has explicitly disabled a plugin, the engine will not even 237 attempt to load it. 238 """ 239 240 def __init__(self, class_path: str, loadable: bool): 241 self.class_path = class_path 242 self.loadable = loadable 243 self.attempted_load = False 244 self.plugin: Plugin | None = None 245 246 @property 247 def enabled(self) -> bool: 248 """Whether the user wants this plugin to load.""" 249 plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {}) 250 assert isinstance(plugstates, dict) 251 val = plugstates.get(self.class_path, {}).get('enabled', False) is True 252 return val 253 254 @enabled.setter 255 def enabled(self, val: bool) -> None: 256 plugstates: dict[str, dict] = _babase.app.config.setdefault( 257 'Plugins', {} 258 ) 259 assert isinstance(plugstates, dict) 260 plugstate = plugstates.setdefault(self.class_path, {}) 261 plugstate['enabled'] = val 262 263 def attempt_load_if_enabled(self) -> Plugin | None: 264 """Possibly load the plugin and log any errors.""" 265 from babase._general import getclass 266 from babase._language import Lstr 267 268 assert not self.attempted_load 269 assert self.plugin is None 270 271 if not self.enabled: 272 return None 273 self.attempted_load = True 274 if not self.loadable: 275 return None 276 try: 277 cls = getclass(self.class_path, Plugin, True) 278 except Exception as exc: 279 _babase.getsimplesound('error').play() 280 _babase.screenmessage( 281 Lstr( 282 resource='pluginClassLoadErrorText', 283 subs=[ 284 ('${PLUGIN}', self.class_path), 285 ('${ERROR}', str(exc)), 286 ], 287 ), 288 color=(1, 0, 0), 289 ) 290 logging.exception( 291 "Error loading plugin class '%s'.", self.class_path 292 ) 293 return None 294 try: 295 self.plugin = cls() 296 return self.plugin 297 except Exception as exc: 298 from babase import _error 299 300 _babase.getsimplesound('error').play() 301 _babase.screenmessage( 302 Lstr( 303 resource='pluginInitErrorText', 304 subs=[ 305 ('${PLUGIN}', self.class_path), 306 ('${ERROR}', str(exc)), 307 ], 308 ), 309 color=(1, 0, 0), 310 ) 311 logging.exception( 312 "Error initing plugin class: '%s'.", self.class_path 313 ) 314 return None
Represents a plugin the engine knows about.
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.
246 @property 247 def enabled(self) -> bool: 248 """Whether the user wants this plugin to load.""" 249 plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {}) 250 assert isinstance(plugstates, dict) 251 val = plugstates.get(self.class_path, {}).get('enabled', False) is True 252 return val
Whether the user wants this plugin to load.
263 def attempt_load_if_enabled(self) -> Plugin | None: 264 """Possibly load the plugin and log any errors.""" 265 from babase._general import getclass 266 from babase._language import Lstr 267 268 assert not self.attempted_load 269 assert self.plugin is None 270 271 if not self.enabled: 272 return None 273 self.attempted_load = True 274 if not self.loadable: 275 return None 276 try: 277 cls = getclass(self.class_path, Plugin, True) 278 except Exception as exc: 279 _babase.getsimplesound('error').play() 280 _babase.screenmessage( 281 Lstr( 282 resource='pluginClassLoadErrorText', 283 subs=[ 284 ('${PLUGIN}', self.class_path), 285 ('${ERROR}', str(exc)), 286 ], 287 ), 288 color=(1, 0, 0), 289 ) 290 logging.exception( 291 "Error loading plugin class '%s'.", self.class_path 292 ) 293 return None 294 try: 295 self.plugin = cls() 296 return self.plugin 297 except Exception as exc: 298 from babase import _error 299 300 _babase.getsimplesound('error').play() 301 _babase.screenmessage( 302 Lstr( 303 resource='pluginInitErrorText', 304 subs=[ 305 ('${PLUGIN}', self.class_path), 306 ('${ERROR}', str(exc)), 307 ], 308 ), 309 color=(1, 0, 0), 310 ) 311 logging.exception( 312 "Error initing plugin class: '%s'.", self.class_path 313 ) 314 return None
Possibly load the plugin and log any errors.
1368def pushcall( 1369 call: Callable, 1370 from_other_thread: bool = False, 1371 suppress_other_thread_warning: bool = False, 1372 other_thread_use_fg_context: bool = False, 1373 raw: bool = False, 1374) -> None: 1375 """Push a call to the logic event-loop. 1376 1377 This call expects to be used in the logic thread, and will automatically 1378 save and restore the babase.Context to behave seamlessly. 1379 1380 If you want to push a call from outside of the logic thread, 1381 however, you can pass 'from_other_thread' as True. In this case 1382 the call will always run in the UI context_ref on the logic thread 1383 or whichever context_ref is in the foreground if 1384 other_thread_use_fg_context is True. 1385 Passing raw=True will disable thread checks and context_ref sets/restores. 1386 """ 1387 return None
Push a call to the logic event-loop.
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.
1391def quit( 1392 confirm: bool = False, quit_type: babase.QuitType | None = None 1393) -> None: 1394 """Quit the app. 1395 1396 If 'confirm' is True, a confirm dialog will be presented if conditions 1397 allow; otherwise the quit will still be immediate. 1398 See docs for babase.QuitType for explanations of the optional 1399 'quit_type' arg. 1400 """ 1401 return None
Quit the app.
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.
40class QuitType(Enum): 41 """Types of input a controller can send to the game. 42 43 'soft' may hide/reset the app but keep the process running, depending 44 on the platform. 45 46 'back' is a variant of 'soft' which may give 'back-button-pressed' 47 behavior depending on the platform. (returning to some previous 48 activity instead of dumping to the home screen, etc.) 49 50 'hard' leads to the process exiting. This generally should be avoided 51 on platforms such as mobile. 52 """ 53 54 SOFT = 0 55 BACK = 1 56 HARD = 2
Types of input a controller can send to the game.
'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.
454def root_ui_pause_updates() -> None: 455 """Temporarily pause updates to the root ui for animation purposes. 456 Make sure that each call to this is matched by a call to 457 root_ui_resume_updates(). 458 """ 459 return None
Temporarily pause updates to the root ui for animation purposes. Make sure that each call to this is matched by a call to root_ui_resume_updates().
462def root_ui_resume_updates() -> None: 463 """Resume paused updates to the root ui for animation purposes.""" 464 return None
Resume paused updates to the root ui for animation purposes.
433class RootUIUpdatePause: 434 """Pauses updates to the root-ui while in existence.""" 435 436 def __init__(self) -> None: 437 _bauiv1.root_ui_pause_updates() 438 439 def __del__(self) -> None: 440 _bauiv1.root_ui_resume_updates()
Pauses updates to the root-ui while in existence.
467def rowwidget( 468 edit: bauiv1.Widget | None = None, 469 parent: bauiv1.Widget | None = None, 470 size: Sequence[float] | None = None, 471 position: Sequence[float] | None = None, 472 background: bool | None = None, 473 selected_child: bauiv1.Widget | None = None, 474 visible_child: bauiv1.Widget | None = None, 475 claims_left_right: bool | None = None, 476 selection_loops_to_parent: bool | None = None, 477) -> bauiv1.Widget: 478 """Create or edit a row widget. 479 480 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 481 a new one is created and returned. Arguments that are not set to None 482 are applied to the Widget. 483 """ 484 import bauiv1 # pylint: disable=cyclic-import 485 486 return bauiv1.Widget()
Create or edit a row widget.
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.
1439def safecolor( 1440 color: Sequence[float], target_intensity: float = 0.6 1441) -> tuple[float, ...]: 1442 """Given a color tuple, return a color safe to display as text. 1443 1444 Accepts tuples of length 3 or 4. This will slightly brighten very 1445 dark colors, etc. 1446 """ 1447 return (0.0, 0.0, 0.0)
Given a color tuple, return a color safe to display as text.
Accepts tuples of length 3 or 4. This will slightly brighten very dark colors, etc.
1450def screenmessage( 1451 message: str | babase.Lstr, 1452 color: Sequence[float] | None = None, 1453 log: bool = False, 1454) -> None: 1455 """Print a message to the local client's screen, in a given color. 1456 1457 Note that this version of the function is purely for local display. 1458 To broadcast screen messages in network play, look for methods such as 1459 broadcastmessage() provided by the scene-version packages. 1460 """ 1461 return None
Print a message to the local client's screen, in a given color.
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.
489def scrollwidget( 490 *, 491 edit: bauiv1.Widget | None = None, 492 parent: bauiv1.Widget | None = None, 493 size: Sequence[float] | None = None, 494 position: Sequence[float] | None = None, 495 background: bool | None = None, 496 selected_child: bauiv1.Widget | None = None, 497 capture_arrows: bool = False, 498 on_select_call: Callable | None = None, 499 center_small_content: bool | None = None, 500 center_small_content_horizontally: bool | None = None, 501 color: Sequence[float] | None = None, 502 highlight: bool | None = None, 503 border_opacity: float | None = None, 504 simple_culling_v: float | None = None, 505 selection_loops_to_parent: bool | None = None, 506 claims_left_right: bool | None = None, 507 claims_up_down: bool | None = None, 508 autoselect: bool | None = None, 509) -> bauiv1.Widget: 510 """Create or edit a scroll widget. 511 512 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 513 a new one is created and returned. Arguments that are not set to None 514 are applied to the Widget. 515 """ 516 import bauiv1 # pylint: disable=cyclic-import 517 518 return bauiv1.Widget()
Create or edit a scroll widget.
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.
1464def set_analytics_screen(screen: str) -> None: 1465 """Used for analytics to see where in the app players spend their time. 1466 1467 Generally called when opening a new window or entering some UI. 1468 'screen' should be a string description of an app location 1469 ('Main Menu', etc.) 1470 """ 1471 return None
Used for analytics to see where in the app players spend their time.
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 """Sound asset for local user interface purposes.""" 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
Sound asset for local user interface purposes.
88class SpecialChar(Enum): 89 """Special characters the game can print.""" 90 DOWN_ARROW = 0 91 UP_ARROW = 1 92 LEFT_ARROW = 2 93 RIGHT_ARROW = 3 94 TOP_BUTTON = 4 95 LEFT_BUTTON = 5 96 RIGHT_BUTTON = 6 97 BOTTOM_BUTTON = 7 98 DELETE = 8 99 SHIFT = 9 100 BACK = 10 101 LOGO_FLAT = 11 102 REWIND_BUTTON = 12 103 PLAY_PAUSE_BUTTON = 13 104 FAST_FORWARD_BUTTON = 14 105 DPAD_CENTER_BUTTON = 15 106 PLAY_STATION_CROSS_BUTTON = 16 107 PLAY_STATION_CIRCLE_BUTTON = 17 108 PLAY_STATION_TRIANGLE_BUTTON = 18 109 PLAY_STATION_SQUARE_BUTTON = 19 110 PLAY_BUTTON = 20 111 PAUSE_BUTTON = 21 112 OUYA_BUTTON_O = 22 113 OUYA_BUTTON_U = 23 114 OUYA_BUTTON_Y = 24 115 OUYA_BUTTON_A = 25 116 TOKEN = 26 117 LOGO = 27 118 TICKET = 28 119 GOOGLE_PLAY_GAMES_LOGO = 29 120 GAME_CENTER_LOGO = 30 121 DICE_BUTTON1 = 31 122 DICE_BUTTON2 = 32 123 DICE_BUTTON3 = 33 124 DICE_BUTTON4 = 34 125 GAME_CIRCLE_LOGO = 35 126 PARTY_ICON = 36 127 TEST_ACCOUNT = 37 128 TICKET_BACKING = 38 129 TROPHY1 = 39 130 TROPHY2 = 40 131 TROPHY3 = 41 132 TROPHY0A = 42 133 TROPHY0B = 43 134 TROPHY4 = 44 135 LOCAL_ACCOUNT = 45 136 EXPLODINARY_LOGO = 46 137 FLAG_UNITED_STATES = 47 138 FLAG_MEXICO = 48 139 FLAG_GERMANY = 49 140 FLAG_BRAZIL = 50 141 FLAG_RUSSIA = 51 142 FLAG_CHINA = 52 143 FLAG_UNITED_KINGDOM = 53 144 FLAG_CANADA = 54 145 FLAG_INDIA = 55 146 FLAG_JAPAN = 56 147 FLAG_FRANCE = 57 148 FLAG_INDONESIA = 58 149 FLAG_ITALY = 59 150 FLAG_SOUTH_KOREA = 60 151 FLAG_NETHERLANDS = 61 152 FEDORA = 62 153 HAL = 63 154 CROWN = 64 155 YIN_YANG = 65 156 EYE_BALL = 66 157 SKULL = 67 158 HEART = 68 159 DRAGON = 69 160 HELMET = 70 161 MUSHROOM = 71 162 NINJA_STAR = 72 163 VIKING_HELMET = 73 164 MOON = 74 165 SPIDER = 75 166 FIREBALL = 76 167 FLAG_UNITED_ARAB_EMIRATES = 77 168 FLAG_QATAR = 78 169 FLAG_EGYPT = 79 170 FLAG_KUWAIT = 80 171 FLAG_ALGERIA = 81 172 FLAG_SAUDI_ARABIA = 82 173 FLAG_MALAYSIA = 83 174 FLAG_CZECH_REPUBLIC = 84 175 FLAG_AUSTRALIA = 85 176 FLAG_SINGAPORE = 86 177 OCULUS_LOGO = 87 178 STEAM_LOGO = 88 179 NVIDIA_LOGO = 89 180 FLAG_IRAN = 90 181 FLAG_POLAND = 91 182 FLAG_ARGENTINA = 92 183 FLAG_PHILIPPINES = 93 184 FLAG_CHILE = 94 185 MIKIROG = 95 186 V2_LOGO = 96
Special characters the game can print.
526def spinnerwidget( 527 *, 528 edit: bauiv1.Widget | None = None, 529 parent: bauiv1.Widget | None = None, 530 size: float | None = None, 531 position: Sequence[float] | None = None, 532 style: Literal['bomb', 'simple'] | None = None, 533 visible: bool | None = None, 534) -> bauiv1.Widget: 535 """Create or edit a spinner widget. 536 537 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 538 a new one is created and returned. Arguments that are not set to None 539 are applied to the Widget. 540 """ 541 import bauiv1 # pylint: disable=cyclic-import 542 543 return bauiv1.Widget()
Create or edit a spinner widget.
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.
1608def supports_unicode_display() -> bool: 1609 """Return whether we can display all unicode characters in the gui.""" 1610 return bool()
Return whether we can display all unicode characters in the gui.
Texture asset for local user interface purposes.
546def textwidget( 547 *, 548 edit: bauiv1.Widget | None = None, 549 parent: bauiv1.Widget | None = None, 550 size: Sequence[float] | None = None, 551 position: Sequence[float] | None = None, 552 text: str | bauiv1.Lstr | None = None, 553 v_align: str | None = None, 554 h_align: str | None = None, 555 editable: bool | None = None, 556 padding: float | None = None, 557 on_return_press_call: Callable[[], None] | None = None, 558 on_activate_call: Callable[[], None] | None = None, 559 selectable: bool | None = None, 560 query: bauiv1.Widget | None = None, 561 max_chars: int | None = None, 562 color: Sequence[float] | None = None, 563 click_activate: bool | None = None, 564 on_select_call: Callable[[], None] | None = None, 565 always_highlight: bool | None = None, 566 draw_controller: bauiv1.Widget | None = None, 567 scale: float | None = None, 568 corner_scale: float | None = None, 569 description: str | bauiv1.Lstr | None = None, 570 transition_delay: float | None = None, 571 maxwidth: float | None = None, 572 max_height: float | None = None, 573 flatness: float | None = None, 574 shadow: float | None = None, 575 autoselect: bool | None = None, 576 rotate: float | None = None, 577 enabled: bool | None = None, 578 force_internal_editing: bool | None = None, 579 always_show_carat: bool | None = None, 580 big: bool | None = None, 581 extra_touch_border_scale: float | None = None, 582 res_scale: float | None = None, 583 query_max_chars: bauiv1.Widget | None = None, 584 query_description: bauiv1.Widget | None = None, 585 adapter_finished: bool | None = None, 586 glow_type: str | None = None, 587 allow_clear_button: bool | None = None, 588) -> bauiv1.Widget: 589 """Create or edit a text widget. 590 591 Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise 592 a new one is created and returned. Arguments that are not set to None 593 are applied to the Widget. 594 """ 595 import bauiv1 # pylint: disable=cyclic-import 596 597 return bauiv1.Widget()
Create or edit a text widget.
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 Given a time value, returns a babase.Lstr with: 22 (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True). 23 24 WARNING: the underlying Lstr value is somewhat large so don't use this 25 to rapidly update Node text values for an onscreen timer or you may 26 consume significant network bandwidth. For that purpose you should 27 use a 'timedisplay' Node and attribute connections. 28 29 """ 30 from babase._language import Lstr 31 32 # We take float seconds but operate on int milliseconds internally. 33 timeval = int(1000 * timeval) 34 bits = [] 35 subs = [] 36 hval = (timeval // 1000) // (60 * 60) 37 if hval != 0: 38 bits.append('${H}') 39 subs.append( 40 ( 41 '${H}', 42 Lstr( 43 resource='timeSuffixHoursText', 44 subs=[('${COUNT}', str(hval))], 45 ), 46 ) 47 ) 48 mval = ((timeval // 1000) // 60) % 60 49 if mval != 0: 50 bits.append('${M}') 51 subs.append( 52 ( 53 '${M}', 54 Lstr( 55 resource='timeSuffixMinutesText', 56 subs=[('${COUNT}', str(mval))], 57 ), 58 ) 59 ) 60 61 # We add seconds if its non-zero *or* we haven't added anything else. 62 if centi: 63 # pylint: disable=consider-using-f-string 64 sval = timeval / 1000.0 % 60.0 65 if sval >= 0.005 or not bits: 66 bits.append('${S}') 67 subs.append( 68 ( 69 '${S}', 70 Lstr( 71 resource='timeSuffixSecondsText', 72 subs=[('${COUNT}', ('%.2f' % sval))], 73 ), 74 ) 75 ) 76 else: 77 sval = timeval // 1000 % 60 78 if sval != 0 or not bits: 79 bits.append('${S}') 80 subs.append( 81 ( 82 '${S}', 83 Lstr( 84 resource='timeSuffixSecondsText', 85 subs=[('${COUNT}', str(sval))], 86 ), 87 ) 88 ) 89 return Lstr(value=' '.join(bits), subs=subs)
Generate a babase.Lstr for displaying a time value.
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.
327def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None: 328 """Checks to ensure a widget-owning object gets cleaned up properly. 329 330 This adds a check which will print an error message if the provided 331 object still exists ~5 seconds after the provided bauiv1.Widget dies. 332 333 This is a good sanity check for any sort of object that wraps or 334 controls a bauiv1.Widget. For instance, a 'Window' class instance has 335 no reason to still exist once its root container bauiv1.Widget has fully 336 transitioned out and been destroyed. Circular references or careless 337 strong referencing can lead to such objects never getting destroyed, 338 however, and this helps detect such cases to avoid memory leaks. 339 """ 340 if DEBUG_UI_CLEANUP_CHECKS: 341 print(f'adding uicleanup to {obj}') 342 if not isinstance(widget, _bauiv1.Widget): 343 raise TypeError('widget arg is not a bauiv1.Widget') 344 345 if bool(False): 346 347 def foobar() -> None: 348 """Just testing.""" 349 if DEBUG_UI_CLEANUP_CHECKS: 350 print('uicleanupcheck widget dying...') 351 352 widget.add_delete_callback(foobar) 353 354 assert babase.app.classic is not None 355 babase.app.ui_v1.cleanupchecks.append( 356 UICleanupCheck( 357 obj=weakref.ref(obj), widget=widget, widget_death_time=None 358 ) 359 )
Checks to ensure a widget-owning object gets cleaned up properly.
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.
59class UIScale(Enum): 60 """The overall scale the UI is being rendered for. Note that this is 61 independent of pixel resolution. For example, a phone and a desktop PC 62 might render the game at similar pixel resolutions but the size they 63 display content at will vary significantly. 64 65 'large' is used for devices such as desktop PCs where fine details can 66 be clearly seen. UI elements are generally smaller on the screen 67 and more content can be seen at once. 68 69 'medium' is used for devices such as tablets, TVs, or VR headsets. 70 This mode strikes a balance between clean readability and amount of 71 content visible. 72 73 'small' is used primarily for phones or other small devices where 74 content needs to be presented as large and clear in order to remain 75 readable from an average distance. 76 """ 77 78 SMALL = 0 79 MEDIUM = 1 80 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.
'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 To use this class, access the single instance of it at 'ba.app.ui'. 37 """ 38 39 class RootUIElement(Enum): 40 """Stuff provided by the root ui.""" 41 42 MENU_BUTTON = 'menu_button' 43 SQUAD_BUTTON = 'squad_button' 44 ACCOUNT_BUTTON = 'account_button' 45 SETTINGS_BUTTON = 'settings_button' 46 INBOX_BUTTON = 'inbox_button' 47 STORE_BUTTON = 'store_button' 48 INVENTORY_BUTTON = 'inventory_button' 49 ACHIEVEMENTS_BUTTON = 'achievements_button' 50 GET_TOKENS_BUTTON = 'get_tokens_button' 51 TICKETS_METER = 'tickets_meter' 52 TOKENS_METER = 'tokens_meter' 53 TROPHY_METER = 'trophy_meter' 54 LEVEL_METER = 'level_meter' 55 CHEST_SLOT_0 = 'chest_slot_0' 56 CHEST_SLOT_1 = 'chest_slot_1' 57 CHEST_SLOT_2 = 'chest_slot_2' 58 CHEST_SLOT_3 = 'chest_slot_3' 59 60 def __init__(self) -> None: 61 from bauiv1._uitypes import MainWindow 62 63 super().__init__() 64 65 # We hold only a weak ref to the current main Window; we want it 66 # to be able to disappear on its own. That being said, we do 67 # expect MainWindows to keep themselves alive until replaced by 68 # another MainWindow and we complain if they don't. 69 self._main_window = empty_weakref(MainWindow) 70 self._main_window_widget: bauiv1.Widget | None = None 71 72 self.quit_window: bauiv1.Widget | None = None 73 74 # For storing arbitrary class-level state data for Windows or 75 # other UI related classes. 76 self.window_states: dict[type, Any] = {} 77 78 self._uiscale: babase.UIScale 79 self._update_ui_scale() 80 81 self.cleanupchecks: list[UICleanupCheck] = [] 82 self.upkeeptimer: babase.AppTimer | None = None 83 84 self.title_color = (0.72, 0.7, 0.75) 85 self.heading_color = (0.72, 0.7, 0.75) 86 self.infotextcolor = (0.7, 0.9, 0.7) 87 88 self.window_auto_recreate_suppress_count = 0 89 90 self._last_win_recreate_screen_size: tuple[float, float] | None = None 91 self._last_win_recreate_uiscale: bauiv1.UIScale | None = None 92 self._last_win_recreate_time: float | None = None 93 self._win_recreate_timer: babase.AppTimer | None = None 94 95 # Elements in our root UI will call anything here when 96 # activated. 97 self.root_ui_calls: dict[ 98 UIV1AppSubsystem.RootUIElement, Callable[[], None] 99 ] = {} 100 101 def _update_ui_scale(self) -> None: 102 uiscalestr = babase.get_ui_scale() 103 if uiscalestr == 'large': 104 self._uiscale = babase.UIScale.LARGE 105 elif uiscalestr == 'medium': 106 self._uiscale = babase.UIScale.MEDIUM 107 elif uiscalestr == 'small': 108 self._uiscale = babase.UIScale.SMALL 109 else: 110 logging.error("Invalid UIScale '%s'.", uiscalestr) 111 self._uiscale = babase.UIScale.MEDIUM 112 113 @property 114 def available(self) -> bool: 115 """Can uiv1 currently be used? 116 117 Code that may run in headless mode, before the UI has been spun up, 118 while other ui systems are active, etc. can check this to avoid 119 likely erroring. 120 """ 121 return _bauiv1.is_available() 122 123 @override 124 def reset(self) -> None: 125 from bauiv1._uitypes import MainWindow 126 127 self.root_ui_calls.clear() 128 self._main_window = empty_weakref(MainWindow) 129 self._main_window_widget = None 130 131 @property 132 def uiscale(self) -> babase.UIScale: 133 """Current ui scale for the app.""" 134 return self._uiscale 135 136 @override 137 def on_app_loading(self) -> None: 138 from bauiv1._uitypes import ui_upkeep 139 140 # IMPORTANT: If tweaking UI stuff, make sure it behaves for 141 # small, medium, and large UI modes. (doesn't run off screen, 142 # etc). The overrides below can be used to test with different 143 # sizes. Generally small is used on phones, medium is used on 144 # tablets/tvs, and large is on desktop computers or perhaps 145 # large tablets. When possible, run in windowed mode and resize 146 # the window to assure this holds true at all aspect ratios. 147 148 # UPDATE: A better way to test this is now by setting the 149 # environment variable BA_UI_SCALE to "small", "medium", or 150 # "large". This will affect system UIs not covered by the values 151 # below such as screen-messages. The below values remain 152 # functional, however, for cases such as Android where 153 # environment variables can't be set easily. 154 155 if bool(False): # force-test ui scale 156 self._uiscale = babase.UIScale.SMALL 157 with babase.ContextRef.empty(): 158 babase.pushcall( 159 lambda: babase.screenmessage( 160 f'FORCING UISCALE {self._uiscale.name} FOR TESTING', 161 color=(1, 0, 1), 162 log=True, 163 ) 164 ) 165 166 # Kick off our periodic UI upkeep. 167 168 # FIXME: Can probably kill this if we do immediate UI death 169 # checks. 170 self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True) 171 172 def get_main_window(self) -> bauiv1.MainWindow | None: 173 """Return main window, if any.""" 174 return self._main_window() 175 176 def set_main_window( 177 self, 178 window: bauiv1.MainWindow, 179 *, 180 from_window: bauiv1.MainWindow | None | bool = True, 181 is_back: bool = False, 182 is_top_level: bool = False, 183 is_auxiliary: bool = False, 184 back_state: MainWindowState | None = None, 185 suppress_warning: bool = False, 186 ) -> None: 187 """Set the current 'main' window. 188 189 Generally this should not be called directly; The high level 190 MainWindow methods main_window_replace() and main_window_back() 191 should be used whenever possible to implement navigation. 192 193 The caller is responsible for cleaning up any previous main 194 window. 195 """ 196 # pylint: disable=too-many-locals 197 # pylint: disable=too-many-branches 198 # pylint: disable=too-many-statements 199 from bauiv1._uitypes import MainWindow 200 201 # If we haven't grabbed initial uiscale or screen size for recreate 202 # comparision purposes, this is a good time to do so. 203 if self._last_win_recreate_screen_size is None: 204 self._last_win_recreate_screen_size = ( 205 babase.get_virtual_screen_size() 206 ) 207 if self._last_win_recreate_uiscale is None: 208 self._last_win_recreate_uiscale = babase.app.ui_v1.uiscale 209 210 # Encourage migration to the new higher level nav calls. 211 if not suppress_warning: 212 warnings.warn( 213 'set_main_window() should usually not be called directly;' 214 ' use the main_window_replace() or main_window_back()' 215 ' methods on MainWindow objects for navigation instead.' 216 ' If you truly need to use set_main_window(),' 217 ' pass suppress_warning=True to silence this warning.', 218 DeprecationWarning, 219 stacklevel=2, 220 ) 221 222 # We used to accept Widgets but now want MainWindows. 223 if not isinstance(window, MainWindow): 224 raise RuntimeError( 225 f'set_main_window() now takes a MainWindow as its "window" arg.' 226 f' You passed a {type(window)}.', 227 ) 228 window_weakref = weakref.ref(window) 229 window_widget = window.get_root_widget() 230 231 if not isinstance(from_window, MainWindow): 232 if from_window is not None and not isinstance(from_window, bool): 233 raise RuntimeError( 234 f'set_main_window() now takes a MainWindow or bool or None' 235 f'as its "from_window" arg.' 236 f' You passed a {type(from_window)}.', 237 ) 238 239 existing = self._main_window() 240 241 # If they passed a back-state, make sure it is fully filled out. 242 if back_state is not None: 243 if ( 244 back_state.is_top_level is None 245 or back_state.is_auxiliary is None 246 or back_state.window_type is None 247 ): 248 raise RuntimeError( 249 'Provided back_state is incomplete.' 250 ' Make sure to only pass fully-filled-out MainWindowStates.' 251 ) 252 253 # If a top-level main-window is being set, complain if there already 254 # is a main-window. 255 if is_top_level: 256 if existing: 257 logging.warning( 258 'set_main_window() called with top-level window %s' 259 ' but found existing main-window %s.', 260 window, 261 existing, 262 ) 263 else: 264 # In other cases, sanity-check that the window asking for 265 # this switch is the one we're switching away from. 266 try: 267 if isinstance(from_window, bool): 268 # For default val True we warn that the arg wasn't 269 # passed. False can be explicitly passed to disable 270 # this check. 271 if from_window is True: 272 caller_frame = inspect.stack()[1] 273 caller_filename = caller_frame.filename 274 caller_line_number = caller_frame.lineno 275 logging.warning( 276 'set_main_window() should be passed a' 277 " 'from_window' value to help ensure proper" 278 ' UI behavior (%s line %i).', 279 caller_filename, 280 caller_line_number, 281 ) 282 else: 283 # For everything else, warn if what they passed 284 # wasn't the previous main menu widget. 285 if from_window is not existing: 286 caller_frame = inspect.stack()[1] 287 caller_filename = caller_frame.filename 288 caller_line_number = caller_frame.lineno 289 logging.warning( 290 "set_main_window() was passed 'from_window' %s" 291 ' but existing main-menu-window is %s.' 292 ' (%s line %i).', 293 from_window, 294 existing, 295 caller_filename, 296 caller_line_number, 297 ) 298 except Exception: 299 # Prevent any bugs in these checks from causing problems. 300 logging.exception('Error checking from_window') 301 302 if is_back: 303 # These values should only be passed for forward navigation. 304 assert not is_top_level 305 assert not is_auxiliary 306 # Make sure back state is complete. 307 assert back_state is not None 308 assert back_state.is_top_level is not None 309 assert back_state.is_auxiliary is not None 310 assert back_state.window_type is type(window) 311 window.main_window_back_state = back_state.parent 312 window.main_window_is_top_level = back_state.is_top_level 313 window.main_window_is_auxiliary = back_state.is_auxiliary 314 else: 315 # Store if the window is top-level so we won't complain later if 316 # we go back from it and there's nowhere to go to. 317 window.main_window_is_top_level = is_top_level 318 319 window.main_window_is_auxiliary = is_auxiliary 320 321 # When navigating forward, generate a back-window-state from 322 # the outgoing window. 323 if is_top_level: 324 # Top level windows don't have or expect anywhere to 325 # go back to. 326 window.main_window_back_state = None 327 elif back_state is not None: 328 window.main_window_back_state = back_state 329 else: 330 oldwin = self._main_window() 331 if oldwin is None: 332 # We currenty only hold weak refs to windows so that 333 # they are free to die on their own, but we expect 334 # the main menu window to keep itself alive as long 335 # as its the main one. Holler if that seems to not 336 # be happening. 337 logging.warning( 338 'set_main_window: No old MainWindow found' 339 ' and is_top_level is False;' 340 ' this should not happen.' 341 ) 342 window.main_window_back_state = None 343 else: 344 window.main_window_back_state = self.save_main_window_state( 345 oldwin 346 ) 347 348 self._main_window = window_weakref 349 self._main_window_widget = window_widget 350 351 def has_main_window(self) -> bool: 352 """Return whether a main menu window is present.""" 353 return bool(self._main_window_widget) 354 355 def clear_main_window(self, transition: str | None = None) -> None: 356 """Clear any existing main window.""" 357 from bauiv1._uitypes import MainWindow 358 359 main_window = self._main_window() 360 if main_window: 361 main_window.main_window_close(transition=transition) 362 else: 363 # Fallback; if we have a widget but no window, nuke the widget. 364 if self._main_window_widget: 365 logging.error( 366 'Have _main_window_widget but no main_window' 367 ' on clear_main_window; unexpected.' 368 ) 369 self._main_window_widget.delete() 370 371 self._main_window = empty_weakref(MainWindow) 372 self._main_window_widget = None 373 374 def save_main_window_state(self, window: MainWindow) -> MainWindowState: 375 """Fully initialize a window-state from a window. 376 377 Use this to get a complete state for later restoration purposes. 378 Calling the window's get_main_window_state() directly is 379 insufficient. 380 """ 381 winstate = window.get_main_window_state() 382 383 # Store some common window stuff on its state. 384 winstate.parent = window.main_window_back_state 385 winstate.is_top_level = window.main_window_is_top_level 386 winstate.is_auxiliary = window.main_window_is_auxiliary 387 winstate.window_type = type(window) 388 389 return winstate 390 391 def restore_main_window_state(self, state: MainWindowState) -> None: 392 """Restore UI to a saved state.""" 393 existing = self.get_main_window() 394 if existing is not None: 395 raise RuntimeError('There is already a MainWindow.') 396 397 # Valid states should have a value here. 398 assert state.is_top_level is not None 399 assert state.is_auxiliary is not None 400 assert state.window_type is not None 401 402 win = state.create_window(transition=None) 403 self.set_main_window( 404 win, 405 from_window=False, # disable check 406 is_top_level=state.is_top_level, 407 is_auxiliary=state.is_auxiliary, 408 back_state=state.parent, 409 suppress_warning=True, 410 ) 411 412 def should_suppress_window_recreates(self) -> bool: 413 """Should we avoid auto-recreating windows at the current time?""" 414 415 # This is slightly hack-ish and ideally we can get to the point 416 # where we never need this and can remove it. 417 418 # Currently string-edits grab a weak-ref to the exact text 419 # widget they're targeting. So we need to suppress recreates 420 # while edits are in progress. Ideally we should change that to 421 # use ids or something that would survive a recreate. 422 if babase.app.stringedit.active_adapter() is not None: 423 return True 424 425 # Suppress if anything else is requesting suppression (such as 426 # generic Windows that don't handle being recreated). 427 return babase.app.ui_v1.window_auto_recreate_suppress_count > 0 428 429 @override 430 def on_ui_scale_change(self) -> None: 431 # Update our stored UIScale. 432 self._update_ui_scale() 433 434 # Update native bits (allow root widget to rebuild itself/etc.) 435 _bauiv1.on_ui_scale_change() 436 437 self._schedule_main_win_recreate() 438 439 @override 440 def on_screen_size_change(self) -> None: 441 442 self._schedule_main_win_recreate() 443 444 def _schedule_main_win_recreate(self) -> None: 445 446 # If there is a timer set already, do nothing. 447 if self._win_recreate_timer is not None: 448 return 449 450 # Recreating a MainWindow is a kinda heavy thing and it doesn't 451 # seem like we should be doing it at 120hz during a live window 452 # resize, so let's limit the max rate we do it. We also use the 453 # same mechanism to defer window recreates while anything is 454 # suppressing them. 455 now = time.monotonic() 456 457 # Up to 4 refreshes per second seems reasonable. 458 interval = 0.25 459 460 # Ok; there's no timer. Schedule one. 461 till_update = ( 462 interval 463 if self.should_suppress_window_recreates() 464 else ( 465 0.0 466 if self._last_win_recreate_time is None 467 else max(0.0, self._last_win_recreate_time + interval - now) 468 ) 469 ) 470 self._win_recreate_timer = babase.AppTimer( 471 till_update, self._do_main_win_recreate 472 ) 473 474 def _do_main_win_recreate(self) -> None: 475 self._last_win_recreate_time = time.monotonic() 476 self._win_recreate_timer = None 477 478 # If win-recreates are currently suppressed, just kick off 479 # another timer. We'll do our actual thing once suppression 480 # finally ends. 481 if self.should_suppress_window_recreates(): 482 self._schedule_main_win_recreate() 483 return 484 485 mainwindow = self.get_main_window() 486 487 # Can't recreate what doesn't exist. 488 if mainwindow is None: 489 return 490 491 virtual_screen_size = babase.get_virtual_screen_size() 492 uiscale = babase.app.ui_v1.uiscale 493 494 # These should always get actual values when a main-window is 495 # assigned so should never still be None here. 496 assert self._last_win_recreate_uiscale is not None 497 assert self._last_win_recreate_screen_size is not None 498 499 # If uiscale hasn't changed and our screen-size hasn't either 500 # (or it has but we don't care) then we're done. 501 if uiscale is self._last_win_recreate_uiscale and ( 502 virtual_screen_size == self._last_win_recreate_screen_size 503 or not mainwindow.refreshes_on_screen_size_changes 504 ): 505 return 506 507 # Do the recreate. 508 winstate = self.save_main_window_state(mainwindow) 509 self.clear_main_window(transition='instant') 510 self.restore_main_window_state(winstate) 511 512 # Store the size we created this for to avoid redundant 513 # future recreates. 514 self._last_win_recreate_uiscale = uiscale 515 self._last_win_recreate_screen_size = virtual_screen_size
Consolidated UI functionality for the app.
To use this class, access the single instance of it at 'ba.app.ui'.
113 @property 114 def available(self) -> bool: 115 """Can uiv1 currently be used? 116 117 Code that may run in headless mode, before the UI has been spun up, 118 while other ui systems are active, etc. can check this to avoid 119 likely erroring. 120 """ 121 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.
123 @override 124 def reset(self) -> None: 125 from bauiv1._uitypes import MainWindow 126 127 self.root_ui_calls.clear() 128 self._main_window = empty_weakref(MainWindow) 129 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.
131 @property 132 def uiscale(self) -> babase.UIScale: 133 """Current ui scale for the app.""" 134 return self._uiscale
Current ui scale for the app.
136 @override 137 def on_app_loading(self) -> None: 138 from bauiv1._uitypes import ui_upkeep 139 140 # IMPORTANT: If tweaking UI stuff, make sure it behaves for 141 # small, medium, and large UI modes. (doesn't run off screen, 142 # etc). The overrides below can be used to test with different 143 # sizes. Generally small is used on phones, medium is used on 144 # tablets/tvs, and large is on desktop computers or perhaps 145 # large tablets. When possible, run in windowed mode and resize 146 # the window to assure this holds true at all aspect ratios. 147 148 # UPDATE: A better way to test this is now by setting the 149 # environment variable BA_UI_SCALE to "small", "medium", or 150 # "large". This will affect system UIs not covered by the values 151 # below such as screen-messages. The below values remain 152 # functional, however, for cases such as Android where 153 # environment variables can't be set easily. 154 155 if bool(False): # force-test ui scale 156 self._uiscale = babase.UIScale.SMALL 157 with babase.ContextRef.empty(): 158 babase.pushcall( 159 lambda: babase.screenmessage( 160 f'FORCING UISCALE {self._uiscale.name} FOR TESTING', 161 color=(1, 0, 1), 162 log=True, 163 ) 164 ) 165 166 # Kick off our periodic UI upkeep. 167 168 # FIXME: Can probably kill this if we do immediate UI death 169 # checks. 170 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.
172 def get_main_window(self) -> bauiv1.MainWindow | None: 173 """Return main window, if any.""" 174 return self._main_window()
Return main window, if any.
176 def set_main_window( 177 self, 178 window: bauiv1.MainWindow, 179 *, 180 from_window: bauiv1.MainWindow | None | bool = True, 181 is_back: bool = False, 182 is_top_level: bool = False, 183 is_auxiliary: bool = False, 184 back_state: MainWindowState | None = None, 185 suppress_warning: bool = False, 186 ) -> None: 187 """Set the current 'main' window. 188 189 Generally this should not be called directly; The high level 190 MainWindow methods main_window_replace() and main_window_back() 191 should be used whenever possible to implement navigation. 192 193 The caller is responsible for cleaning up any previous main 194 window. 195 """ 196 # pylint: disable=too-many-locals 197 # pylint: disable=too-many-branches 198 # pylint: disable=too-many-statements 199 from bauiv1._uitypes import MainWindow 200 201 # If we haven't grabbed initial uiscale or screen size for recreate 202 # comparision purposes, this is a good time to do so. 203 if self._last_win_recreate_screen_size is None: 204 self._last_win_recreate_screen_size = ( 205 babase.get_virtual_screen_size() 206 ) 207 if self._last_win_recreate_uiscale is None: 208 self._last_win_recreate_uiscale = babase.app.ui_v1.uiscale 209 210 # Encourage migration to the new higher level nav calls. 211 if not suppress_warning: 212 warnings.warn( 213 'set_main_window() should usually not be called directly;' 214 ' use the main_window_replace() or main_window_back()' 215 ' methods on MainWindow objects for navigation instead.' 216 ' If you truly need to use set_main_window(),' 217 ' pass suppress_warning=True to silence this warning.', 218 DeprecationWarning, 219 stacklevel=2, 220 ) 221 222 # We used to accept Widgets but now want MainWindows. 223 if not isinstance(window, MainWindow): 224 raise RuntimeError( 225 f'set_main_window() now takes a MainWindow as its "window" arg.' 226 f' You passed a {type(window)}.', 227 ) 228 window_weakref = weakref.ref(window) 229 window_widget = window.get_root_widget() 230 231 if not isinstance(from_window, MainWindow): 232 if from_window is not None and not isinstance(from_window, bool): 233 raise RuntimeError( 234 f'set_main_window() now takes a MainWindow or bool or None' 235 f'as its "from_window" arg.' 236 f' You passed a {type(from_window)}.', 237 ) 238 239 existing = self._main_window() 240 241 # If they passed a back-state, make sure it is fully filled out. 242 if back_state is not None: 243 if ( 244 back_state.is_top_level is None 245 or back_state.is_auxiliary is None 246 or back_state.window_type is None 247 ): 248 raise RuntimeError( 249 'Provided back_state is incomplete.' 250 ' Make sure to only pass fully-filled-out MainWindowStates.' 251 ) 252 253 # If a top-level main-window is being set, complain if there already 254 # is a main-window. 255 if is_top_level: 256 if existing: 257 logging.warning( 258 'set_main_window() called with top-level window %s' 259 ' but found existing main-window %s.', 260 window, 261 existing, 262 ) 263 else: 264 # In other cases, sanity-check that the window asking for 265 # this switch is the one we're switching away from. 266 try: 267 if isinstance(from_window, bool): 268 # For default val True we warn that the arg wasn't 269 # passed. False can be explicitly passed to disable 270 # this check. 271 if from_window is True: 272 caller_frame = inspect.stack()[1] 273 caller_filename = caller_frame.filename 274 caller_line_number = caller_frame.lineno 275 logging.warning( 276 'set_main_window() should be passed a' 277 " 'from_window' value to help ensure proper" 278 ' UI behavior (%s line %i).', 279 caller_filename, 280 caller_line_number, 281 ) 282 else: 283 # For everything else, warn if what they passed 284 # wasn't the previous main menu widget. 285 if from_window is not existing: 286 caller_frame = inspect.stack()[1] 287 caller_filename = caller_frame.filename 288 caller_line_number = caller_frame.lineno 289 logging.warning( 290 "set_main_window() was passed 'from_window' %s" 291 ' but existing main-menu-window is %s.' 292 ' (%s line %i).', 293 from_window, 294 existing, 295 caller_filename, 296 caller_line_number, 297 ) 298 except Exception: 299 # Prevent any bugs in these checks from causing problems. 300 logging.exception('Error checking from_window') 301 302 if is_back: 303 # These values should only be passed for forward navigation. 304 assert not is_top_level 305 assert not is_auxiliary 306 # Make sure back state is complete. 307 assert back_state is not None 308 assert back_state.is_top_level is not None 309 assert back_state.is_auxiliary is not None 310 assert back_state.window_type is type(window) 311 window.main_window_back_state = back_state.parent 312 window.main_window_is_top_level = back_state.is_top_level 313 window.main_window_is_auxiliary = back_state.is_auxiliary 314 else: 315 # Store if the window is top-level so we won't complain later if 316 # we go back from it and there's nowhere to go to. 317 window.main_window_is_top_level = is_top_level 318 319 window.main_window_is_auxiliary = is_auxiliary 320 321 # When navigating forward, generate a back-window-state from 322 # the outgoing window. 323 if is_top_level: 324 # Top level windows don't have or expect anywhere to 325 # go back to. 326 window.main_window_back_state = None 327 elif back_state is not None: 328 window.main_window_back_state = back_state 329 else: 330 oldwin = self._main_window() 331 if oldwin is None: 332 # We currenty only hold weak refs to windows so that 333 # they are free to die on their own, but we expect 334 # the main menu window to keep itself alive as long 335 # as its the main one. Holler if that seems to not 336 # be happening. 337 logging.warning( 338 'set_main_window: No old MainWindow found' 339 ' and is_top_level is False;' 340 ' this should not happen.' 341 ) 342 window.main_window_back_state = None 343 else: 344 window.main_window_back_state = self.save_main_window_state( 345 oldwin 346 ) 347 348 self._main_window = window_weakref 349 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.
351 def has_main_window(self) -> bool: 352 """Return whether a main menu window is present.""" 353 return bool(self._main_window_widget)
Return whether a main menu window is present.
355 def clear_main_window(self, transition: str | None = None) -> None: 356 """Clear any existing main window.""" 357 from bauiv1._uitypes import MainWindow 358 359 main_window = self._main_window() 360 if main_window: 361 main_window.main_window_close(transition=transition) 362 else: 363 # Fallback; if we have a widget but no window, nuke the widget. 364 if self._main_window_widget: 365 logging.error( 366 'Have _main_window_widget but no main_window' 367 ' on clear_main_window; unexpected.' 368 ) 369 self._main_window_widget.delete() 370 371 self._main_window = empty_weakref(MainWindow) 372 self._main_window_widget = None
Clear any existing main window.
374 def save_main_window_state(self, window: MainWindow) -> MainWindowState: 375 """Fully initialize a window-state from a window. 376 377 Use this to get a complete state for later restoration purposes. 378 Calling the window's get_main_window_state() directly is 379 insufficient. 380 """ 381 winstate = window.get_main_window_state() 382 383 # Store some common window stuff on its state. 384 winstate.parent = window.main_window_back_state 385 winstate.is_top_level = window.main_window_is_top_level 386 winstate.is_auxiliary = window.main_window_is_auxiliary 387 winstate.window_type = type(window) 388 389 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.
391 def restore_main_window_state(self, state: MainWindowState) -> None: 392 """Restore UI to a saved state.""" 393 existing = self.get_main_window() 394 if existing is not None: 395 raise RuntimeError('There is already a MainWindow.') 396 397 # Valid states should have a value here. 398 assert state.is_top_level is not None 399 assert state.is_auxiliary is not None 400 assert state.window_type is not None 401 402 win = state.create_window(transition=None) 403 self.set_main_window( 404 win, 405 from_window=False, # disable check 406 is_top_level=state.is_top_level, 407 is_auxiliary=state.is_auxiliary, 408 back_state=state.parent, 409 suppress_warning=True, 410 )
Restore UI to a saved state.
412 def should_suppress_window_recreates(self) -> bool: 413 """Should we avoid auto-recreating windows at the current time?""" 414 415 # This is slightly hack-ish and ideally we can get to the point 416 # where we never need this and can remove it. 417 418 # Currently string-edits grab a weak-ref to the exact text 419 # widget they're targeting. So we need to suppress recreates 420 # while edits are in progress. Ideally we should change that to 421 # use ids or something that would survive a recreate. 422 if babase.app.stringedit.active_adapter() is not None: 423 return True 424 425 # Suppress if anything else is requesting suppression (such as 426 # generic Windows that don't handle being recreated). 427 return babase.app.ui_v1.window_auto_recreate_suppress_count > 0
Should we avoid auto-recreating windows at the current time?
429 @override 430 def on_ui_scale_change(self) -> None: 431 # Update our stored UIScale. 432 self._update_ui_scale() 433 434 # Update native bits (allow root widget to rebuild itself/etc.) 435 _bauiv1.on_ui_scale_change() 436 437 self._schedule_main_win_recreate()
Called when screen ui-scale changes.
Will not be called for the initial ui scale.
39 class RootUIElement(Enum): 40 """Stuff provided by the root ui.""" 41 42 MENU_BUTTON = 'menu_button' 43 SQUAD_BUTTON = 'squad_button' 44 ACCOUNT_BUTTON = 'account_button' 45 SETTINGS_BUTTON = 'settings_button' 46 INBOX_BUTTON = 'inbox_button' 47 STORE_BUTTON = 'store_button' 48 INVENTORY_BUTTON = 'inventory_button' 49 ACHIEVEMENTS_BUTTON = 'achievements_button' 50 GET_TOKENS_BUTTON = 'get_tokens_button' 51 TICKETS_METER = 'tickets_meter' 52 TOKENS_METER = 'tokens_meter' 53 TROPHY_METER = 'trophy_meter' 54 LEVEL_METER = 'level_meter' 55 CHEST_SLOT_0 = 'chest_slot_0' 56 CHEST_SLOT_1 = 'chest_slot_1' 57 CHEST_SLOT_2 = 'chest_slot_2' 58 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.
611def widget( 612 *, 613 edit: bauiv1.Widget, 614 up_widget: bauiv1.Widget | None = None, 615 down_widget: bauiv1.Widget | None = None, 616 left_widget: bauiv1.Widget | None = None, 617 right_widget: bauiv1.Widget | None = None, 618 show_buffer_top: float | None = None, 619 show_buffer_bottom: float | None = None, 620 show_buffer_left: float | None = None, 621 show_buffer_right: float | None = None, 622 depth_range: tuple[float, float] | None = None, 623 autoselect: bool | None = None, 624) -> None: 625 """Edit common attributes of any widget. 626 627 Unlike other UI calls, this can only be used to edit, not to create. 628 """ 629 return None
Edit common attributes of any widget.
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 This class represents a weak reference to a widget object 81 in the internal C++ layer. Currently, functions such as 82 bauiv1.buttonwidget() must be used to instantiate or edit these. 83 """ 84 85 transitioning_out: bool 86 """Whether this widget is in the process of dying (read only). 87 88 It can be useful to check this on a window's root widget to 89 prevent multiple window actions from firing simultaneously, 90 potentially leaving the UI in a broken state.""" 91 92 def __bool__(self) -> bool: 93 """Support for bool evaluation.""" 94 return bool(True) # Slight obfuscation. 95 96 def activate(self) -> None: 97 """Activates a widget; the same as if it had been clicked.""" 98 return None 99 100 def add_delete_callback(self, call: Callable) -> None: 101 """Add a call to be run immediately after this widget is destroyed.""" 102 return None 103 104 def delete(self, ignore_missing: bool = True) -> None: 105 """Delete the Widget. Ignores already-deleted Widgets if ignore_missing 106 is True; otherwise an Exception is thrown. 107 """ 108 return None 109 110 def exists(self) -> bool: 111 """Returns whether the Widget still exists. 112 Most functionality will fail on a nonexistent widget. 113 114 Note that you can also use the boolean operator for this same 115 functionality, so a statement such as "if mywidget" will do 116 the right thing both for Widget objects and values of None. 117 """ 118 return bool() 119 120 def get_children(self) -> list[bauiv1.Widget]: 121 """Returns any child Widgets of this Widget.""" 122 import bauiv1 123 124 return [bauiv1.Widget()] 125 126 def get_screen_space_center(self) -> tuple[float, float]: 127 """Returns the coords of the bauiv1.Widget center relative to the center 128 of the screen. This can be useful for placing pop-up windows and other 129 special cases. 130 """ 131 return (0.0, 0.0) 132 133 def get_selected_child(self) -> bauiv1.Widget | None: 134 """Returns the selected child Widget or None if nothing is selected.""" 135 import bauiv1 136 137 return bauiv1.Widget() 138 139 def get_widget_type(self) -> str: 140 """Return the internal type of the Widget as a string. Note that this 141 is different from the Python bauiv1.Widget type, which is the same for 142 all widgets. 143 """ 144 return str()
Internal type for low level UI elements; buttons, windows, etc.
This class represents a weak reference to a widget object in the internal C++ layer. Currently, functions such as bauiv1.buttonwidget() must be used to instantiate or edit these.
Whether this widget is in the process of dying (read only).
It can be useful to check this on a window's root widget to prevent multiple window actions from firing simultaneously, potentially leaving the UI in a broken state.
96 def activate(self) -> None: 97 """Activates a widget; the same as if it had been clicked.""" 98 return None
Activates a widget; the same as if it had been clicked.
100 def add_delete_callback(self, call: Callable) -> None: 101 """Add a call to be run immediately after this widget is destroyed.""" 102 return None
Add a call to be run immediately after this widget is destroyed.
104 def delete(self, ignore_missing: bool = True) -> None: 105 """Delete the Widget. Ignores already-deleted Widgets if ignore_missing 106 is True; otherwise an Exception is thrown. 107 """ 108 return None
Delete the Widget. Ignores already-deleted Widgets if ignore_missing is True; otherwise an Exception is thrown.
110 def exists(self) -> bool: 111 """Returns whether the Widget still exists. 112 Most functionality will fail on a nonexistent widget. 113 114 Note that you can also use the boolean operator for this same 115 functionality, so a statement such as "if mywidget" will do 116 the right thing both for Widget objects and values of None. 117 """ 118 return bool()
Returns whether the Widget still exists. Most functionality will fail on a nonexistent widget.
Note that you can also use the boolean operator for this same functionality, so a statement such as "if mywidget" will do the right thing both for Widget objects and values of None.
120 def get_children(self) -> list[bauiv1.Widget]: 121 """Returns any child Widgets of this Widget.""" 122 import bauiv1 123 124 return [bauiv1.Widget()]
Returns any child Widgets of this Widget.
126 def get_screen_space_center(self) -> tuple[float, float]: 127 """Returns the coords of the bauiv1.Widget center relative to the center 128 of the screen. This can be useful for placing pop-up windows and other 129 special cases. 130 """ 131 return (0.0, 0.0)
Returns the coords of the bauiv1.Widget center relative to the center of the screen. This can be useful for placing pop-up windows and other special cases.
133 def get_selected_child(self) -> bauiv1.Widget | None: 134 """Returns the selected child Widget or None if nothing is selected.""" 135 import bauiv1 136 137 return bauiv1.Widget()
Returns the selected child Widget or None if nothing is selected.
139 def get_widget_type(self) -> str: 140 """Return the internal type of the Widget as a string. Note that this 141 is different from the Python bauiv1.Widget type, which is the same for 142 all widgets. 143 """ 144 return str()
Return the internal type of the Widget as a string. Note that this is different from the Python bauiv1.Widget type, which is the same for all widgets.
28class Window: 29 """A basic window. 30 31 Essentially wraps a ContainerWidget with some higher level 32 functionality. 33 """ 34 35 def __init__( 36 self, 37 root_widget: bauiv1.Widget, 38 cleanupcheck: bool = True, 39 prevent_main_window_auto_recreate: bool = True, 40 ): 41 self._root_widget = root_widget 42 43 # By default, the presence of any generic windows prevents the 44 # app from running its fancy main-window-auto-recreate mechanism 45 # on screen-resizes and whatnot. This avoids things like 46 # temporary popup windows getting stuck under auto-re-created 47 # main-windows. 48 self._prevent_main_window_auto_recreate = ( 49 prevent_main_window_auto_recreate 50 ) 51 if prevent_main_window_auto_recreate: 52 babase.app.ui_v1.window_auto_recreate_suppress_count += 1 53 54 # Generally we complain if we outlive our root widget. 55 if cleanupcheck: 56 uicleanupcheck(self, root_widget) 57 58 def __del__(self) -> None: 59 if self._prevent_main_window_auto_recreate: 60 babase.app.ui_v1.window_auto_recreate_suppress_count -= 1 61 62 def get_root_widget(self) -> bauiv1.Widget: 63 """Return the root widget.""" 64 return self._root_widget
A basic window.
Essentially wraps a ContainerWidget with some higher level functionality.
35 def __init__( 36 self, 37 root_widget: bauiv1.Widget, 38 cleanupcheck: bool = True, 39 prevent_main_window_auto_recreate: bool = True, 40 ): 41 self._root_widget = root_widget 42 43 # By default, the presence of any generic windows prevents the 44 # app from running its fancy main-window-auto-recreate mechanism 45 # on screen-resizes and whatnot. This avoids things like 46 # temporary popup windows getting stuck under auto-re-created 47 # main-windows. 48 self._prevent_main_window_auto_recreate = ( 49 prevent_main_window_auto_recreate 50 ) 51 if prevent_main_window_auto_recreate: 52 babase.app.ui_v1.window_auto_recreate_suppress_count += 1 53 54 # Generally we complain if we outlive our root widget. 55 if cleanupcheck: 56 uicleanupcheck(self, root_widget)