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            )
def allows_ticket_sales() -> bool:
506def allows_ticket_sales() -> bool:
507    """:meta private:"""
508    return bool()

:meta private:

app = <App object>
class App:
  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.

health_monitor: babase.AppHealthMonitor
env: _babase.Env
state: App.State
net: babase._net.NetworkSubsystem
workspaces: babase._workspace.WorkspaceSubsystem
components: babase._appcomponent.AppComponentSubsystem
fg_state: int
SHUTDOWN_TASK_TIMEOUT_SECONDS = 12
def postinit(self) -> None:
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__.

active: bool
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).

mode: AppMode | None
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.

asyncio_loop: asyncio.events.AbstractEventLoop
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.

def create_async_task(self, coro: Coroutine[Any, Any, ~T], *, name: str | None = None) -> None:
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.

mode_selector: babase.AppModeSelector
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.

classic: baclassic.ClassicAppSubsystem | None
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).

plus: baplus.PlusAppSubsystem | None
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).

ui_v1: UIV1AppSubsystem
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).

def register_subsystem(self, subsystem: babase.AppSubsystem) -> None:
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.

def add_shutdown_task(self, coro: Coroutine[NoneType, NoneType, NoneType]) -> None:
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.

def run(self) -> None:
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.

def set_intent(self, intent: AppIntent) -> None:
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.

def push_apply_app_config(self) -> None:
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.

def on_native_start(self) -> None:
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.

def on_native_bootstrapping_complete(self) -> None:
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.

def on_native_suspend(self) -> None:
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.

def on_native_unsuspend(self) -> None:
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.

def on_native_shutdown(self) -> None:
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.

def on_native_shutdown_complete(self) -> None:
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.

def on_native_active_changed(self) -> None:
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.

def on_initial_sign_in_complete(self) -> None:
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.

def set_ui_scale(self, scale: UIScale) -> None:
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.

def on_screen_size_change(self) -> None:
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.

class App.State(enum.Enum):
 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.

NOT_STARTED = <State.NOT_STARTED: 0>
NATIVE_BOOTSTRAPPING = <State.NATIVE_BOOTSTRAPPING: 1>
INITING = <State.INITING: 2>
LOADING = <State.LOADING: 3>
RUNNING = <State.RUNNING: 4>
SUSPENDED = <State.SUSPENDED: 5>
SHUTTING_DOWN = <State.SHUTTING_DOWN: 6>
SHUTDOWN_COMPLETE = <State.SHUTDOWN_COMPLETE: 7>
class App.DefaultAppModeSelector(babase._appmodeselector.AppModeSelector):
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.

@override
def app_mode_for_intent( self, intent: AppIntent) -> type[AppMode] | None:
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.

class AppIntent:
13class AppIntent:
14    """A high level directive given to the app."""

A high level directive given to the app.

class AppIntentDefault(bauiv1.AppIntent):
17class AppIntentDefault(AppIntent):
18    """Tells the app to simply run in its default mode."""

Tells the app to simply run in its default mode.

class AppIntentExec(bauiv1.AppIntent):
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.

AppIntentExec(code: str)
24    def __init__(self, code: str):
25        self.code = code
code
class AppMode:
 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.

@classmethod
def get_app_experience(cls) -> bacommon.app.AppExperience:
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.

@classmethod
def can_handle_intent(cls, intent: AppIntent) -> bool:
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.

def handle_intent(self, intent: AppIntent) -> None:
45    def handle_intent(self, intent: AppIntent) -> None:
46        """Handle an intent."""
47        raise NotImplementedError('AppMode subclasses must override this.')

Handle an intent.

def on_activate(self) -> None:
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.

def on_deactivate(self) -> None:
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).

def on_app_active_changed(self) -> None:
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).

def on_purchase_process_begin(self, item_id: str, user_initiated: bool) -> None:
 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.

def on_purchase_process_end(self, item_id: str, user_initiated: bool, applied: bool) -> None:
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.

def appname() -> str:
530def appname() -> str:
531    """Return current app name (all lowercase)."""
532    return str()

Return current app name (all lowercase).

def appnameupper() -> str:
535def appnameupper() -> str:
536    """Return current app name with capitalized characters."""
537    return str()

Return current app name with capitalized characters.

def apptime() -> AppTime:
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.

AppTime = AppTime
def apptimer(time: float, call: Callable[[], Any]) -> None:
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!'))
class AppTimer:
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)

AppTimer(time: float, call: Callable[[], Any], repeat: bool = False)
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass
class BasicMainWindowState(bauiv1.MainWindowState):
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.

BasicMainWindowState( create_call: Callable[[Optional[Literal['in_right', 'in_left', 'in_scale']], _bauiv1.Widget | None], MainWindow])
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
create_call
@override
def create_window( self, transition: Optional[Literal['in_right', 'in_left', 'in_scale']] = None, origin_widget: _bauiv1.Widget | None = None) -> 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.

def buttonwidget( *, edit: _bauiv1.Widget | None = None, parent: _bauiv1.Widget | None = None, id: str | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, on_activate_call: Optional[Callable] = None, label: str | Lstr | None = None, color: Optional[Sequence[float]] = None, down_widget: _bauiv1.Widget | None = None, up_widget: _bauiv1.Widget | None = None, left_widget: _bauiv1.Widget | None = None, right_widget: _bauiv1.Widget | None = None, texture: _bauiv1.Texture | None = None, text_scale: float | None = None, textcolor: Optional[Sequence[float]] = None, enable_sound: bool | None = None, mesh_transparent: _bauiv1.Mesh | None = None, mesh_opaque: _bauiv1.Mesh | None = None, repeat: bool | None = None, scale: float | None = None, transition_delay: float | None = None, on_select_call: Optional[Callable] = None, button_type: str | None = None, extra_touch_border_scale: float | None = None, selectable: bool | None = None, show_buffer_top: float | None = None, icon: _bauiv1.Texture | None = None, iconscale: float | None = None, icon_tint: float | None = None, icon_color: Optional[Sequence[float]] = None, autoselect: bool | None = None, mask_texture: _bauiv1.Texture | None = None, tint_texture: _bauiv1.Texture | None = None, tint_color: Optional[Sequence[float]] = None, tint2_color: Optional[Sequence[float]] = None, text_flatness: float | None = None, text_res_scale: float | None = None, enabled: bool | None = None) -> _bauiv1.Widget:
147def buttonwidget(
148    *,
149    edit: bauiv1.Widget | None = None,
150    parent: bauiv1.Widget | None = None,
151    id: str | None = None,
152    size: Sequence[float] | None = None,
153    position: Sequence[float] | None = None,
154    on_activate_call: Callable | None = None,
155    label: str | bauiv1.Lstr | None = None,
156    color: Sequence[float] | None = None,
157    down_widget: bauiv1.Widget | None = None,
158    up_widget: bauiv1.Widget | None = None,
159    left_widget: bauiv1.Widget | None = None,
160    right_widget: bauiv1.Widget | None = None,
161    texture: bauiv1.Texture | None = None,
162    text_scale: float | None = None,
163    textcolor: Sequence[float] | None = None,
164    enable_sound: bool | None = None,
165    mesh_transparent: bauiv1.Mesh | None = None,
166    mesh_opaque: bauiv1.Mesh | None = None,
167    repeat: bool | None = None,
168    scale: float | None = None,
169    transition_delay: float | None = None,
170    on_select_call: Callable | None = None,
171    button_type: str | None = None,
172    extra_touch_border_scale: float | None = None,
173    selectable: bool | None = None,
174    show_buffer_top: float | None = None,
175    icon: bauiv1.Texture | None = None,
176    iconscale: float | None = None,
177    icon_tint: float | None = None,
178    icon_color: Sequence[float] | None = None,
179    autoselect: bool | None = None,
180    mask_texture: bauiv1.Texture | None = None,
181    tint_texture: bauiv1.Texture | None = None,
182    tint_color: Sequence[float] | None = None,
183    tint2_color: Sequence[float] | None = None,
184    text_flatness: float | None = None,
185    text_res_scale: float | None = None,
186    enabled: bool | None = None,
187) -> bauiv1.Widget:
188    """Create or edit a button widget.
189
190    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
191    a new one is created and returned. Arguments that are not set to None
192    are applied to the Widget.
193    """
194    import bauiv1  # pylint: disable=cyclic-import
195
196    return bauiv1.Widget()

Create or edit a button 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.

Call = <class 'babase._general._Call'>
def charstr(char_id: SpecialChar) -> str:
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.

def checkboxwidget( *, edit: _bauiv1.Widget | None = None, parent: _bauiv1.Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, text: str | Lstr | None = None, value: bool | None = None, on_value_change_call: Optional[Callable[[bool], NoneType]] = None, on_select_call: Optional[Callable[[], NoneType]] = None, text_scale: float | None = None, textcolor: Optional[Sequence[float]] = None, scale: float | None = None, is_radio_button: bool | None = None, maxwidth: float | None = None, autoselect: bool | None = None, color: Optional[Sequence[float]] = None) -> _bauiv1.Widget:
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.

def clipboard_is_supported() -> bool:
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.

def clipboard_set_text(value: str) -> None:
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.

def columnwidget( *, edit: _bauiv1.Widget | None = None, parent: _bauiv1.Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, background: bool | None = None, selected_child: _bauiv1.Widget | None = None, visible_child: _bauiv1.Widget | None = None, single_depth: bool | None = None, print_list_exit_instructions: bool | None = None, left_border: float | None = None, top_border: float | None = None, bottom_border: float | None = None, selection_loops_to_parent: bool | None = None, border: float | None = None, margin: float | None = None, claims_left_right: bool | None = None) -> _bauiv1.Widget:
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.

def containerwidget( *, edit: _bauiv1.Widget | None = None, parent: _bauiv1.Widget | None = None, id: str | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, background: bool | None = None, selected_child: _bauiv1.Widget | None = None, transition: str | None = None, cancel_button: _bauiv1.Widget | None = None, start_button: _bauiv1.Widget | None = None, root_selectable: bool | None = None, on_activate_call: Optional[Callable[[], NoneType]] = None, claims_left_right: bool | None = None, selection_loops: bool | None = None, selection_loops_to_parent: bool | None = None, scale: float | None = None, on_outside_click_call: Optional[Callable[[], NoneType]] = None, single_depth: bool | None = None, visible_child: _bauiv1.Widget | None = None, stack_offset: Optional[Sequence[float]] = None, color: Optional[Sequence[float]] = None, on_cancel_call: Optional[Callable[[], NoneType]] = None, print_list_exit_instructions: bool | None = None, click_activate: bool | None = None, always_highlight: bool | None = None, selectable: bool | None = None, scale_origin_stack_offset: Optional[Sequence[float]] = None, toolbar_visibility: Optional[Literal['menu_minimal', 'menu_minimal_no_back', 'menu_full', 'menu_full_no_back', 'menu_store', 'menu_store_no_back', 'menu_in_game', 'menu_tokens', 'get_tokens', 'no_menu_minimal', 'inherit']] = None, on_select_call: Optional[Callable[[], NoneType]] = None, claim_outside_clicks: bool | None = None, claims_up_down: bool | None = None) -> _bauiv1.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.

class ContextRef:
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()
@classmethod
def empty(cls) -> _babase.ContextRef:
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.

def is_empty(self) -> bool:
207    def is_empty(self) -> bool:
208        """Whether the context was created as empty."""
209        return bool()

Whether the context was created as empty.

def is_expired(self) -> bool:
211    def is_expired(self) -> bool:
212        """Whether the context has expired."""
213        return bool()

Whether the context has expired.

def displaytime() -> DisplayTime:
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.

DisplayTime = DisplayTime
def displaytimer(time: float, call: Callable[[], Any]) -> None:
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!'))
class DisplayTimer:
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)

DisplayTimer(time: float, call: Callable[[], Any], repeat: bool = False)
259    def __init__(
260        self, time: float, call: Callable[[], Any], repeat: bool = False
261    ) -> None:
262        pass
def do_once() -> bool:
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!')
def existing(obj: Optional[~ExistableT]) -> Optional[~ExistableT]:
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

def get_input_idle_time() -> float:
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.).

def get_ip_address_type(addr: str) -> socket.AddressFamily:
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.

def get_qrcode_texture(url: str) -> _bauiv1.Texture:
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.

def get_type_name(cls: type) -> str:
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.

def getclass( name: str, subclassof: type[~T], check_sdlib_modulename_clash: bool = False) -> type[~T]:
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.

def getmesh(name: str) -> _bauiv1.Mesh:
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.

def getsound(name: str) -> _bauiv1.Sound:
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.

def gettexture(name: str) -> _bauiv1.Texture:
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.

def hscrollwidget( *, edit: _bauiv1.Widget | None = None, parent: _bauiv1.Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, background: bool | None = None, selected_child: _bauiv1.Widget | None = None, capture_arrows: bool | None = None, on_select_call: Optional[Callable[[], NoneType]] = None, center_small_content: bool | None = None, color: Optional[Sequence[float]] = None, highlight: bool | None = None, border_opacity: float | None = None, simple_culling_h: float | None = None, claims_left_right: bool | None = None, claims_up_down: bool | None = None) -> _bauiv1.Widget:
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.

def imagewidget( *, edit: _bauiv1.Widget | None = None, parent: _bauiv1.Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, color: Optional[Sequence[float]] = None, texture: _bauiv1.Texture | None = None, opacity: float | None = None, mesh_transparent: _bauiv1.Mesh | None = None, mesh_opaque: _bauiv1.Mesh | None = None, has_alpha_channel: bool = True, tint_texture: _bauiv1.Texture | None = None, tint_color: Optional[Sequence[float]] = None, transition_delay: float | None = None, draw_controller: _bauiv1.Widget | None = None, tint2_color: Optional[Sequence[float]] = None, tilt_scale: float | None = None, mask_texture: _bauiv1.Texture | None = None, radial_amount: float | None = None, draw_controller_mult: float | None = None) -> _bauiv1.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.

def is_browser_likely_available() -> bool:
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.)

class Keyboard:
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.

name: str

Displays when user selecting this keyboard.

chars: list[tuple[str, ...]]

Used for row/column lengths.

pages: dict[str, tuple[str, ...]]

Extra chars like emojis.

nums: tuple[str, ...]

The 'num' page.

class LoginAdapter:
 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.

LoginAdapter(login_type: bacommon.login.LoginType)
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
login_type
def on_app_loading(self) -> 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.

def set_implicit_login_state( self, state: LoginAdapter.ImplicitLoginState | None) -> None:
 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.

def set_active_logins(self, logins: dict[bacommon.login.LoginType, str]) -> None:
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.

def on_back_end_active_change(self, active: bool) -> None:
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.

@final
def sign_in( self, result_cb: Callable[[LoginAdapter, LoginAdapter.SignInResult | Exception], NoneType], description: str) -> None:
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.

def is_back_end_active(self) -> bool:
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?

def get_sign_in_token(self, completion_cb: Callable[[str | None], NoneType]) -> None:
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.

@dataclass
class LoginAdapter.SignInResult:
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.

LoginAdapter.SignInResult(credentials: str)
credentials: str
@dataclass
class LoginAdapter.ImplicitLoginState:
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.

LoginAdapter.ImplicitLoginState(login_id: str, display_name: str)
login_id: str
display_name: str
@dataclass
class LoginInfo:
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.

LoginInfo(name: str)
name: str
class Lstr:
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'))])
Lstr(*args: Any, **keywds: Any)
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).

args
def evaluate(self) -> str:
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.

def is_flat_value(self) -> bool:
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.

@staticmethod
def from_json(json_string: str) -> Lstr:
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

Given a json string, returns a babase.Lstr. Does no validation.

class MainWindow(bauiv1.Window):
 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.

MainWindow( root_widget: _bauiv1.Widget, *, transition: str | None, origin_widget: _bauiv1.Widget | None, cleanupcheck: bool = True, refresh_on_screen_size_changes: bool = False)
 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.

main_window_back_state: MainWindowState | None
main_window_is_top_level: bool
refreshes_on_screen_size_changes
main_window_is_auxiliary: bool
def main_window_close(self, transition: str | None = None) -> None:
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.

def main_window_has_control(self) -> bool:
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.

def main_window_back(self) -> None:
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.

def main_window_replace( self, new_window: MainWindow, back_state: MainWindowState | None = None, is_auxiliary: bool = False) -> None:
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.

def on_main_window_close(self) -> None:
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.

def get_main_window_state(self) -> MainWindowState:
260    def get_main_window_state(self) -> MainWindowState:
261        """Return a WindowState to recreate this window, if supported."""
262        raise NotImplementedError()

Return a WindowState to recreate this window, if supported.

class MainWindowState:
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.

parent: MainWindowState | None
is_top_level: bool | None
is_auxiliary: bool | None
window_type: type[MainWindow] | None
selection: str | None
def create_window( self, transition: Optional[Literal['in_right', 'in_left', 'in_scale']] = None, origin_widget: _bauiv1.Widget | None = None) -> MainWindow:
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.

class Mesh:
53class Mesh:
54    """Mesh asset for local user interface purposes."""
55
56    pass

Mesh asset for local user interface purposes.

class NotFoundError(builtins.Exception):
25class NotFoundError(Exception):
26    """Exception raised when a referenced object does not exist."""

Exception raised when a referenced object does not exist.

def open_url(address: str, force_fallback: bool = False) -> None:
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.

def overlay_web_browser_close() -> bool:
1309def overlay_web_browser_close() -> bool:
1310    """Close any open overlay web browser."""
1311    return bool()

Close any open overlay web browser.

def overlay_web_browser_is_open() -> bool:
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.

def overlay_web_browser_is_supported() -> bool:
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.

def overlay_web_browser_open_url(address: str) -> None:
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.

class Permission(enum.Enum):
83class Permission(Enum):
84    """Permissions that can be requested from the OS."""
85    STORAGE = 0

Permissions that can be requested from the OS.

STORAGE = <Permission.STORAGE: 0>
class Plugin:
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.

def on_app_running(self) -> None:
326    def on_app_running(self) -> None:
327        """Called when the app reaches the running state."""

Called when the app reaches the running state.

def on_app_suspend(self) -> None:
329    def on_app_suspend(self) -> None:
330        """Called when the app enters the suspended state."""

Called when the app enters the suspended state.

def on_app_unsuspend(self) -> None:
332    def on_app_unsuspend(self) -> None:
333        """Called when the app exits the suspended state."""

Called when the app exits the suspended state.

def on_app_shutdown(self) -> None:
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.

def on_app_shutdown_complete(self) -> None:
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.

def has_settings_ui(self) -> bool:
341    def has_settings_ui(self) -> bool:
342        """Called to ask if we have settings UI we can show."""
343        return False

Called to ask if we have settings UI we can show.

def show_settings_ui(self, source_widget: typing.Any | None) -> None:
345    def show_settings_ui(self, source_widget: Any | None) -> None:
346        """Called to show our settings UI."""

Called to show our settings UI.

class PluginSpec:
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.

PluginSpec(class_path: str, loadable: bool)
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
class_path
loadable
attempted_load
plugin: Plugin | None
enabled: bool
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.

def attempt_load_if_enabled(self) -> Plugin | None:
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.

def pushcall( call: Callable, from_other_thread: bool = False, suppress_other_thread_warning: bool = False, other_thread_use_fg_context: bool = False, raw: bool = False) -> None:
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.

def quit( confirm: bool = False, quit_type: QuitType | None = None) -> None:
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.

class QuitType(enum.Enum):
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.

SOFT = <QuitType.SOFT: 0>
BACK = <QuitType.BACK: 1>
HARD = <QuitType.HARD: 2>
def root_ui_pause_updates() -> None:
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().

def root_ui_resume_updates() -> None:
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.

class RootUIUpdatePause:
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.

def rowwidget( edit: _bauiv1.Widget | None = None, parent: _bauiv1.Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, background: bool | None = None, selected_child: _bauiv1.Widget | None = None, visible_child: _bauiv1.Widget | None = None, claims_left_right: bool | None = None, selection_loops_to_parent: bool | None = None) -> _bauiv1.Widget:
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.

def safecolor( color: Sequence[float], target_intensity: float = 0.6) -> tuple[float, ...]:
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.

def screenmessage( message: str | Lstr, color: Optional[Sequence[float]] = None, log: bool = False) -> None:
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.

def scrollwidget( *, edit: _bauiv1.Widget | None = None, parent: _bauiv1.Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, background: bool | None = None, selected_child: _bauiv1.Widget | None = None, capture_arrows: bool = False, on_select_call: Optional[Callable] = None, center_small_content: bool | None = None, center_small_content_horizontally: bool | None = None, color: Optional[Sequence[float]] = None, highlight: bool | None = None, border_opacity: float | None = None, simple_culling_v: float | None = None, selection_loops_to_parent: bool | None = None, claims_left_right: bool | None = None, claims_up_down: bool | None = None, autoselect: bool | None = None) -> _bauiv1.Widget:
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.

def set_analytics_screen(screen: str) -> None:
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.)

class Sound:
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.

def play(self, volume: float = 1.0) -> None:
62    def play(self, volume: float = 1.0) -> None:
63        """Play the sound locally."""
64        return None

Play the sound locally.

def stop(self) -> None:
66    def stop(self) -> None:
67        """Stop the sound if it is playing."""
68        return None

Stop the sound if it is playing.

class SpecialChar(enum.Enum):
 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.

DOWN_ARROW = <SpecialChar.DOWN_ARROW: 0>
UP_ARROW = <SpecialChar.UP_ARROW: 1>
LEFT_ARROW = <SpecialChar.LEFT_ARROW: 2>
RIGHT_ARROW = <SpecialChar.RIGHT_ARROW: 3>
TOP_BUTTON = <SpecialChar.TOP_BUTTON: 4>
LEFT_BUTTON = <SpecialChar.LEFT_BUTTON: 5>
RIGHT_BUTTON = <SpecialChar.RIGHT_BUTTON: 6>
BOTTOM_BUTTON = <SpecialChar.BOTTOM_BUTTON: 7>
DELETE = <SpecialChar.DELETE: 8>
SHIFT = <SpecialChar.SHIFT: 9>
BACK = <SpecialChar.BACK: 10>
LOGO_FLAT = <SpecialChar.LOGO_FLAT: 11>
REWIND_BUTTON = <SpecialChar.REWIND_BUTTON: 12>
PLAY_PAUSE_BUTTON = <SpecialChar.PLAY_PAUSE_BUTTON: 13>
FAST_FORWARD_BUTTON = <SpecialChar.FAST_FORWARD_BUTTON: 14>
DPAD_CENTER_BUTTON = <SpecialChar.DPAD_CENTER_BUTTON: 15>
PLAY_STATION_CROSS_BUTTON = <SpecialChar.PLAY_STATION_CROSS_BUTTON: 16>
PLAY_STATION_CIRCLE_BUTTON = <SpecialChar.PLAY_STATION_CIRCLE_BUTTON: 17>
PLAY_STATION_TRIANGLE_BUTTON = <SpecialChar.PLAY_STATION_TRIANGLE_BUTTON: 18>
PLAY_STATION_SQUARE_BUTTON = <SpecialChar.PLAY_STATION_SQUARE_BUTTON: 19>
PLAY_BUTTON = <SpecialChar.PLAY_BUTTON: 20>
PAUSE_BUTTON = <SpecialChar.PAUSE_BUTTON: 21>
OUYA_BUTTON_O = <SpecialChar.OUYA_BUTTON_O: 22>
OUYA_BUTTON_U = <SpecialChar.OUYA_BUTTON_U: 23>
OUYA_BUTTON_Y = <SpecialChar.OUYA_BUTTON_Y: 24>
OUYA_BUTTON_A = <SpecialChar.OUYA_BUTTON_A: 25>
TOKEN = <SpecialChar.TOKEN: 26>
TICKET = <SpecialChar.TICKET: 28>
DICE_BUTTON1 = <SpecialChar.DICE_BUTTON1: 31>
DICE_BUTTON2 = <SpecialChar.DICE_BUTTON2: 32>
DICE_BUTTON3 = <SpecialChar.DICE_BUTTON3: 33>
DICE_BUTTON4 = <SpecialChar.DICE_BUTTON4: 34>
PARTY_ICON = <SpecialChar.PARTY_ICON: 36>
TEST_ACCOUNT = <SpecialChar.TEST_ACCOUNT: 37>
TICKET_BACKING = <SpecialChar.TICKET_BACKING: 38>
TROPHY1 = <SpecialChar.TROPHY1: 39>
TROPHY2 = <SpecialChar.TROPHY2: 40>
TROPHY3 = <SpecialChar.TROPHY3: 41>
TROPHY0A = <SpecialChar.TROPHY0A: 42>
TROPHY0B = <SpecialChar.TROPHY0B: 43>
TROPHY4 = <SpecialChar.TROPHY4: 44>
LOCAL_ACCOUNT = <SpecialChar.LOCAL_ACCOUNT: 45>
FLAG_UNITED_STATES = <SpecialChar.FLAG_UNITED_STATES: 47>
FLAG_MEXICO = <SpecialChar.FLAG_MEXICO: 48>
FLAG_GERMANY = <SpecialChar.FLAG_GERMANY: 49>
FLAG_BRAZIL = <SpecialChar.FLAG_BRAZIL: 50>
FLAG_RUSSIA = <SpecialChar.FLAG_RUSSIA: 51>
FLAG_CHINA = <SpecialChar.FLAG_CHINA: 52>
FLAG_UNITED_KINGDOM = <SpecialChar.FLAG_UNITED_KINGDOM: 53>
FLAG_CANADA = <SpecialChar.FLAG_CANADA: 54>
FLAG_INDIA = <SpecialChar.FLAG_INDIA: 55>
FLAG_JAPAN = <SpecialChar.FLAG_JAPAN: 56>
FLAG_FRANCE = <SpecialChar.FLAG_FRANCE: 57>
FLAG_INDONESIA = <SpecialChar.FLAG_INDONESIA: 58>
FLAG_ITALY = <SpecialChar.FLAG_ITALY: 59>
FLAG_SOUTH_KOREA = <SpecialChar.FLAG_SOUTH_KOREA: 60>
FLAG_NETHERLANDS = <SpecialChar.FLAG_NETHERLANDS: 61>
FEDORA = <SpecialChar.FEDORA: 62>
HAL = <SpecialChar.HAL: 63>
CROWN = <SpecialChar.CROWN: 64>
YIN_YANG = <SpecialChar.YIN_YANG: 65>
EYE_BALL = <SpecialChar.EYE_BALL: 66>
SKULL = <SpecialChar.SKULL: 67>
HEART = <SpecialChar.HEART: 68>
DRAGON = <SpecialChar.DRAGON: 69>
HELMET = <SpecialChar.HELMET: 70>
MUSHROOM = <SpecialChar.MUSHROOM: 71>
NINJA_STAR = <SpecialChar.NINJA_STAR: 72>
VIKING_HELMET = <SpecialChar.VIKING_HELMET: 73>
MOON = <SpecialChar.MOON: 74>
SPIDER = <SpecialChar.SPIDER: 75>
FIREBALL = <SpecialChar.FIREBALL: 76>
FLAG_UNITED_ARAB_EMIRATES = <SpecialChar.FLAG_UNITED_ARAB_EMIRATES: 77>
FLAG_QATAR = <SpecialChar.FLAG_QATAR: 78>
FLAG_EGYPT = <SpecialChar.FLAG_EGYPT: 79>
FLAG_KUWAIT = <SpecialChar.FLAG_KUWAIT: 80>
FLAG_ALGERIA = <SpecialChar.FLAG_ALGERIA: 81>
FLAG_SAUDI_ARABIA = <SpecialChar.FLAG_SAUDI_ARABIA: 82>
FLAG_MALAYSIA = <SpecialChar.FLAG_MALAYSIA: 83>
FLAG_CZECH_REPUBLIC = <SpecialChar.FLAG_CZECH_REPUBLIC: 84>
FLAG_AUSTRALIA = <SpecialChar.FLAG_AUSTRALIA: 85>
FLAG_SINGAPORE = <SpecialChar.FLAG_SINGAPORE: 86>
FLAG_IRAN = <SpecialChar.FLAG_IRAN: 90>
FLAG_POLAND = <SpecialChar.FLAG_POLAND: 91>
FLAG_ARGENTINA = <SpecialChar.FLAG_ARGENTINA: 92>
FLAG_PHILIPPINES = <SpecialChar.FLAG_PHILIPPINES: 93>
FLAG_CHILE = <SpecialChar.FLAG_CHILE: 94>
MIKIROG = <SpecialChar.MIKIROG: 95>
def spinnerwidget( *, edit: _bauiv1.Widget | None = None, parent: _bauiv1.Widget | None = None, size: float | None = None, position: Optional[Sequence[float]] = None, style: Optional[Literal['bomb', 'simple']] = None, visible: bool | None = None) -> _bauiv1.Widget:
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.

def supports_unicode_display() -> bool:
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.

class Texture:
71class Texture:
72    """Texture asset for local user interface purposes."""
73
74    pass

Texture asset for local user interface purposes.

def textwidget( *, edit: _bauiv1.Widget | None = None, parent: _bauiv1.Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, text: str | Lstr | None = None, v_align: str | None = None, h_align: str | None = None, editable: bool | None = None, padding: float | None = None, on_return_press_call: Optional[Callable[[], NoneType]] = None, on_activate_call: Optional[Callable[[], NoneType]] = None, selectable: bool | None = None, query: _bauiv1.Widget | None = None, max_chars: int | None = None, color: Optional[Sequence[float]] = None, click_activate: bool | None = None, on_select_call: Optional[Callable[[], NoneType]] = None, always_highlight: bool | None = None, draw_controller: _bauiv1.Widget | None = None, scale: float | None = None, corner_scale: float | None = None, description: str | Lstr | None = None, transition_delay: float | None = None, maxwidth: float | None = None, max_height: float | None = None, flatness: float | None = None, shadow: float | None = None, autoselect: bool | None = None, rotate: float | None = None, enabled: bool | None = None, force_internal_editing: bool | None = None, always_show_carat: bool | None = None, big: bool | None = None, extra_touch_border_scale: float | None = None, res_scale: float | None = None, query_max_chars: _bauiv1.Widget | None = None, query_description: _bauiv1.Widget | None = None, adapter_finished: bool | None = None, glow_type: str | None = None, allow_clear_button: bool | None = None) -> _bauiv1.Widget:
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.

def timestring(timeval: float | int, centi: bool = True) -> Lstr:
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.

def uicleanupcheck(obj: Any, widget: _bauiv1.Widget) -> None:
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.

class UIScale(enum.Enum):
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.

SMALL = <UIScale.SMALL: 0>
MEDIUM = <UIScale.MEDIUM: 1>
LARGE = <UIScale.LARGE: 2>
class UIV1AppSubsystem(babase._appsubsystem.AppSubsystem):
 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'.

quit_window: _bauiv1.Widget | None
window_states: dict[type, typing.Any]
cleanupchecks: list[bauiv1._uitypes.UICleanupCheck]
upkeeptimer: _babase.AppTimer | None
title_color
heading_color
infotextcolor
window_auto_recreate_suppress_count
root_ui_calls: dict[UIV1AppSubsystem.RootUIElement, typing.Callable[[], NoneType]]
available: bool
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.

@override
def reset(self) -> None:
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.

uiscale: UIScale
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.

@override
def on_app_loading(self) -> None:
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.

def get_main_window(self) -> MainWindow | None:
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.

def set_main_window( self, window: MainWindow, *, from_window: MainWindow | None | bool = True, is_back: bool = False, is_top_level: bool = False, is_auxiliary: bool = False, back_state: MainWindowState | None = None, suppress_warning: bool = False) -> None:
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.

def has_main_window(self) -> bool:
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.

def clear_main_window(self, transition: str | None = None) -> None:
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.

def save_main_window_state( self, window: MainWindow) -> MainWindowState:
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.

def restore_main_window_state(self, state: MainWindowState) -> None:
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.

def should_suppress_window_recreates(self) -> bool:
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?

@override
def on_ui_scale_change(self) -> None:
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.

@override
def on_screen_size_change(self) -> None:
439    @override
440    def on_screen_size_change(self) -> None:
441
442        self._schedule_main_win_recreate()

Called when the screen size changes.

Will not be called for the initial screen size.

class UIV1AppSubsystem.RootUIElement(enum.Enum):
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.

MENU_BUTTON = <RootUIElement.MENU_BUTTON: 'menu_button'>
SQUAD_BUTTON = <RootUIElement.SQUAD_BUTTON: 'squad_button'>
ACCOUNT_BUTTON = <RootUIElement.ACCOUNT_BUTTON: 'account_button'>
SETTINGS_BUTTON = <RootUIElement.SETTINGS_BUTTON: 'settings_button'>
INBOX_BUTTON = <RootUIElement.INBOX_BUTTON: 'inbox_button'>
STORE_BUTTON = <RootUIElement.STORE_BUTTON: 'store_button'>
INVENTORY_BUTTON = <RootUIElement.INVENTORY_BUTTON: 'inventory_button'>
ACHIEVEMENTS_BUTTON = <RootUIElement.ACHIEVEMENTS_BUTTON: 'achievements_button'>
GET_TOKENS_BUTTON = <RootUIElement.GET_TOKENS_BUTTON: 'get_tokens_button'>
TICKETS_METER = <RootUIElement.TICKETS_METER: 'tickets_meter'>
TOKENS_METER = <RootUIElement.TOKENS_METER: 'tokens_meter'>
TROPHY_METER = <RootUIElement.TROPHY_METER: 'trophy_meter'>
LEVEL_METER = <RootUIElement.LEVEL_METER: 'level_meter'>
CHEST_SLOT_0 = <RootUIElement.CHEST_SLOT_0: 'chest_slot_0'>
CHEST_SLOT_1 = <RootUIElement.CHEST_SLOT_1: 'chest_slot_1'>
CHEST_SLOT_2 = <RootUIElement.CHEST_SLOT_2: 'chest_slot_2'>
CHEST_SLOT_3 = <RootUIElement.CHEST_SLOT_3: 'chest_slot_3'>
def utc_now_cloud() -> datetime.datetime:
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.

WeakCall = <class 'babase._general._WeakCall'>
def widget( *, edit: _bauiv1.Widget, up_widget: _bauiv1.Widget | None = None, down_widget: _bauiv1.Widget | None = None, left_widget: _bauiv1.Widget | None = None, right_widget: _bauiv1.Widget | None = None, show_buffer_top: float | None = None, show_buffer_bottom: float | None = None, show_buffer_left: float | None = None, show_buffer_right: float | None = None, depth_range: tuple[float, float] | None = None, autoselect: bool | None = None) -> None:
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.

class Widget:
 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.

transitioning_out: bool

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.

def activate(self) -> None:
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.

def add_delete_callback(self, call: Callable) -> None:
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.

def delete(self, ignore_missing: bool = True) -> None:
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.

def exists(self) -> bool:
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.

def get_children(self) -> list[_bauiv1.Widget]:
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.

def get_screen_space_center(self) -> tuple[float, float]:
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.

def get_selected_child(self) -> _bauiv1.Widget | None:
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.

def get_widget_type(self) -> str:
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.

class Window:
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.

Window( root_widget: _bauiv1.Widget, cleanupcheck: bool = True, prevent_main_window_auto_recreate: bool = True)
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)
def get_root_widget(self) -> _bauiv1.Widget:
62    def get_root_widget(self) -> bauiv1.Widget:
63        """Return the root widget."""
64        return self._root_widget

Return the root widget.