bauiv1

Ballistica user interface api version 1

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Ballistica user interface api version 1"""
  4
  5# ba_meta require api 8
  6
  7# The stuff we expose here at the top level is our 'public' api.
  8# It should only be imported by code outside of this package or
  9# from 'if TYPE_CHECKING' blocks (which will not exec at runtime).
 10# Code within our package should import things directly from their
 11# submodules.
 12
 13from __future__ import annotations
 14
 15# pylint: disable=redefined-builtin
 16
 17import logging
 18
 19from efro.util import set_canonical_module_names
 20from babase import (
 21    add_clean_frame_callback,
 22    app,
 23    AppIntent,
 24    AppIntentDefault,
 25    AppIntentExec,
 26    AppMode,
 27    appname,
 28    appnameupper,
 29    apptime,
 30    AppTime,
 31    apptimer,
 32    AppTimer,
 33    Call,
 34    fullscreen_control_available,
 35    fullscreen_control_get,
 36    fullscreen_control_key_shortcut,
 37    fullscreen_control_set,
 38    charstr,
 39    clipboard_is_supported,
 40    clipboard_set_text,
 41    commit_app_config,
 42    ContextRef,
 43    displaytime,
 44    DisplayTime,
 45    displaytimer,
 46    DisplayTimer,
 47    do_once,
 48    fade_screen,
 49    get_display_resolution,
 50    get_input_idle_time,
 51    get_ip_address_type,
 52    get_low_level_config_value,
 53    get_max_graphics_quality,
 54    get_remote_app_name,
 55    get_replays_dir,
 56    get_string_height,
 57    get_string_width,
 58    get_type_name,
 59    getclass,
 60    have_permission,
 61    in_logic_thread,
 62    increment_analytics_count,
 63    is_browser_likely_available,
 64    is_xcode_build,
 65    lock_all_input,
 66    LoginAdapter,
 67    LoginInfo,
 68    Lstr,
 69    native_review_request,
 70    native_review_request_supported,
 71    NotFoundError,
 72    open_file_externally,
 73    Permission,
 74    Plugin,
 75    PluginSpec,
 76    pushcall,
 77    quit,
 78    QuitType,
 79    request_permission,
 80    safecolor,
 81    screenmessage,
 82    set_analytics_screen,
 83    set_low_level_config_value,
 84    set_ui_input_device,
 85    SpecialChar,
 86    supports_max_fps,
 87    supports_vsync,
 88    timestring,
 89    UIScale,
 90    unlock_all_input,
 91    WeakCall,
 92    workspaces_in_use,
 93)
 94
 95from _bauiv1 import (
 96    buttonwidget,
 97    checkboxwidget,
 98    columnwidget,
 99    containerwidget,
100    get_qrcode_texture,
101    get_special_widget,
102    getmesh,
103    getsound,
104    gettexture,
105    hscrollwidget,
106    imagewidget,
107    is_party_icon_visible,
108    Mesh,
109    open_url,
110    rowwidget,
111    scrollwidget,
112    set_party_icon_always_visible,
113    set_party_window_open,
114    Sound,
115    Texture,
116    textwidget,
117    uibounds,
118    Widget,
119    widget,
120)
121from bauiv1._keyboard import Keyboard
122from bauiv1._uitypes import Window, uicleanupcheck
123from bauiv1._subsystem import UIV1Subsystem
124
125__all__ = [
126    'add_clean_frame_callback',
127    'app',
128    'AppIntent',
129    'AppIntentDefault',
130    'AppIntentExec',
131    'AppMode',
132    'appname',
133    'appnameupper',
134    'appnameupper',
135    'apptime',
136    'AppTime',
137    'apptimer',
138    'AppTimer',
139    'buttonwidget',
140    'Call',
141    'fullscreen_control_available',
142    'fullscreen_control_get',
143    'fullscreen_control_key_shortcut',
144    'fullscreen_control_set',
145    'charstr',
146    'checkboxwidget',
147    'clipboard_is_supported',
148    'clipboard_set_text',
149    'columnwidget',
150    'commit_app_config',
151    'containerwidget',
152    'ContextRef',
153    'displaytime',
154    'DisplayTime',
155    'displaytimer',
156    'DisplayTimer',
157    'do_once',
158    'fade_screen',
159    'get_display_resolution',
160    'get_input_idle_time',
161    'get_ip_address_type',
162    'get_low_level_config_value',
163    'get_max_graphics_quality',
164    'get_qrcode_texture',
165    'get_remote_app_name',
166    'get_replays_dir',
167    'get_special_widget',
168    'get_string_height',
169    'get_string_width',
170    'get_type_name',
171    'getclass',
172    'getmesh',
173    'getsound',
174    'gettexture',
175    'have_permission',
176    'hscrollwidget',
177    'imagewidget',
178    'in_logic_thread',
179    'increment_analytics_count',
180    'is_browser_likely_available',
181    'is_party_icon_visible',
182    'is_xcode_build',
183    'Keyboard',
184    'lock_all_input',
185    'LoginAdapter',
186    'LoginInfo',
187    'Lstr',
188    'Mesh',
189    'native_review_request',
190    'native_review_request_supported',
191    'NotFoundError',
192    'open_file_externally',
193    'open_url',
194    'Permission',
195    'Plugin',
196    'PluginSpec',
197    'pushcall',
198    'quit',
199    'QuitType',
200    'request_permission',
201    'rowwidget',
202    'safecolor',
203    'screenmessage',
204    'scrollwidget',
205    'set_analytics_screen',
206    'set_low_level_config_value',
207    'set_party_icon_always_visible',
208    'set_party_window_open',
209    'set_ui_input_device',
210    'Sound',
211    'SpecialChar',
212    'supports_max_fps',
213    'supports_vsync',
214    'Texture',
215    'textwidget',
216    'timestring',
217    'uibounds',
218    'uicleanupcheck',
219    'UIScale',
220    'UIV1Subsystem',
221    'unlock_all_input',
222    'WeakCall',
223    'widget',
224    'Widget',
225    'Window',
226    'workspaces_in_use',
227]
228
229# We want stuff to show up as bauiv1.Foo instead of bauiv1._sub.Foo.
230set_canonical_module_names(globals())
231
232# Sanity check: we want to keep ballistica's dependencies and
233# bootstrapping order clearly defined; let's check a few particular
234# modules to make sure they never directly or indirectly import us
235# before their own execs complete.
236if __debug__:
237    for _mdl in 'babase', '_babase':
238        if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'):
239            logging.warning(
240                '%s was imported before %s finished importing;'
241                ' should not happen.',
242                __name__,
243                _mdl,
244            )
app = <babase._app.App object>
class AppIntent:
13class AppIntent:
14    """A high level directive given to the app.
15
16    Category: **App Classes**
17    """

A high level directive given to the app.

Category: App Classes

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

Tells the app to simply run in its default mode.

class AppIntentExec(bauiv1.AppIntent):
24class AppIntentExec(AppIntent):
25    """Tells the app to exec some Python code."""
26
27    def __init__(self, code: str):
28        self.code = code

Tells the app to exec some Python code.

AppIntentExec(code: str)
27    def __init__(self, code: str):
28        self.code = code
code
class AppMode:
14class AppMode:
15    """A high level mode for the app.
16
17    Category: **App Classes**
18
19    """
20
21    @classmethod
22    def get_app_experience(cls) -> AppExperience:
23        """Return the overall experience provided by this mode."""
24        raise NotImplementedError('AppMode subclasses must override this.')
25
26    @classmethod
27    def can_handle_intent(cls, intent: AppIntent) -> bool:
28        """Return whether this mode can handle the provided intent.
29
30        For this to return True, the AppMode must claim to support the
31        provided intent (via its _supports_intent() method) AND the
32        AppExperience associated with the AppMode must be supported by
33        the current app and runtime environment.
34        """
35        # FIXME: check AppExperience.
36        return cls._supports_intent(intent)
37
38    @classmethod
39    def _supports_intent(cls, intent: AppIntent) -> bool:
40        """Return whether our mode can handle the provided intent.
41
42        AppModes should override this to define what they can handle.
43        Note that AppExperience does not have to be considered here; that
44        is handled automatically by the can_handle_intent() call."""
45        raise NotImplementedError('AppMode subclasses must override this.')
46
47    def handle_intent(self, intent: AppIntent) -> None:
48        """Handle an intent."""
49        raise NotImplementedError('AppMode subclasses must override this.')
50
51    def on_activate(self) -> None:
52        """Called when the mode is being activated."""
53
54    def on_deactivate(self) -> None:
55        """Called when the mode is being deactivated."""
56
57    def on_app_active_changed(self) -> None:
58        """Called when babase.app.active changes.
59
60        The app-mode may want to take action such as pausing a running
61        game in such cases.
62        """

A high level mode for the app.

Category: App Classes

@classmethod
def get_app_experience(cls) -> bacommon.app.AppExperience:
21    @classmethod
22    def get_app_experience(cls) -> AppExperience:
23        """Return the overall experience provided by this mode."""
24        raise NotImplementedError('AppMode subclasses must override this.')

Return the overall experience provided by this mode.

@classmethod
def can_handle_intent(cls, intent: AppIntent) -> bool:
26    @classmethod
27    def can_handle_intent(cls, intent: AppIntent) -> bool:
28        """Return whether this mode can handle the provided intent.
29
30        For this to return True, the AppMode must claim to support the
31        provided intent (via its _supports_intent() method) AND the
32        AppExperience associated with the AppMode must be supported by
33        the current app and runtime environment.
34        """
35        # FIXME: check AppExperience.
36        return cls._supports_intent(intent)

Return whether this mode can handle the provided intent.

For this to return True, the AppMode must claim to support the provided intent (via its _supports_intent() method) AND the AppExperience associated with the AppMode must be supported by the current app and runtime environment.

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

Handle an intent.

def on_activate(self) -> None:
51    def on_activate(self) -> None:
52        """Called when the mode is being activated."""

Called when the mode is being activated.

def on_deactivate(self) -> None:
54    def on_deactivate(self) -> None:
55        """Called when the mode is being deactivated."""

Called when the mode is being deactivated.

def on_app_active_changed(self) -> None:
57    def on_app_active_changed(self) -> None:
58        """Called when babase.app.active changes.
59
60        The app-mode may want to take action such as pausing a running
61        game in such cases.
62        """

Called when babase.app.active changes.

The app-mode may want to take action such as pausing a running game in such cases.

def apptime() -> AppTime:
547def apptime() -> babase.AppTime:
548    """Return the current app-time in seconds.
549
550    Category: **General Utility Functions**
551
552    App-time is a monotonic time value; it starts at 0.0 when the app
553    launches and will never jump by large amounts or go backwards, even if
554    the system time changes. Its progression will pause when the app is in
555    a suspended state.
556
557    Note that the AppTime returned here is simply float; it just has a
558    unique type in the type-checker's eyes to help prevent it from being
559    accidentally used with time functionality expecting other time types.
560    """
561    import babase  # pylint: disable=cyclic-import
562
563    return babase.AppTime(0.0)

Return the current app-time in seconds.

Category: General Utility Functions

App-time is a monotonic time value; it starts at 0.0 when the app launches and will never jump by large amounts or go backwards, even if the system time changes. Its progression will pause when the app is in a suspended state.

Note that the AppTime returned here is simply float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

AppTime = AppTime
def apptimer(time: float, call: Callable[[], Any]) -> None:
566def apptimer(time: float, call: Callable[[], Any]) -> None:
567    """Schedule a callable object to run based on app-time.
568
569    Category: **General Utility Functions**
570
571    This function creates a one-off timer which cannot be canceled or
572    modified once created. If you require the ability to do so, or need
573    a repeating timer, use the babase.AppTimer class instead.
574
575    ##### Arguments
576    ###### time (float)
577    > Length of time in seconds that the timer will wait before firing.
578
579    ###### call (Callable[[], Any])
580    > A callable Python object. Note that the timer will retain a
581    strong reference to the callable for as long as the timer exists, so you
582    may want to look into concepts such as babase.WeakCall if that is not
583    desired.
584
585    ##### Examples
586    Print some stuff through time:
587    >>> babase.screenmessage('hello from now!')
588    >>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
589                              'hello from the future!'))
590    >>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
591    ...                       'hello from the future 2!'))
592    """
593    return None

Schedule a callable object to run based on app-time.

Category: General Utility Functions

This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.AppTimer class instead.

Arguments
time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

Examples

Print some stuff through time:

>>> babase.screenmessage('hello from now!')
>>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
                          'hello from the future!'))
>>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
...                       'hello from the future 2!'))
class AppTimer:
53class AppTimer:
54    """Timers are used to run code at later points in time.
55
56    Category: **General Utility Classes**
57
58    This class encapsulates a timer based on app-time.
59    The underlying timer will be destroyed when this object is no longer
60    referenced. If you do not want to worry about keeping a reference to
61    your timer around, use the babase.apptimer() function instead to get a
62    one-off timer.
63
64    ##### Arguments
65    ###### time
66    > Length of time in seconds that the timer will wait before firing.
67
68    ###### call
69    > A callable Python object. Remember that the timer will retain a
70    strong reference to the callable for as long as it exists, so you
71    may want to look into concepts such as babase.WeakCall if that is not
72    desired.
73
74    ###### repeat
75    > If True, the timer will fire repeatedly, with each successive
76    firing having the same delay as the first.
77
78    ##### Example
79
80    Use a Timer object to print repeatedly for a few seconds:
81    ... def say_it():
82    ...     babase.screenmessage('BADGER!')
83    ... def stop_saying_it():
84    ...     global g_timer
85    ...     g_timer = None
86    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
87    ... # Create our timer; it will run as long as we have the self.t ref.
88    ... g_timer = babase.AppTimer(0.3, say_it, repeat=True)
89    ... # Now fire off a one-shot timer to kill it.
90    ... babase.apptimer(3.89, stop_saying_it)
91    """
92
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass

Timers are used to run code at later points in time.

Category: General Utility Classes

This class encapsulates a timer based on app-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.apptimer() function instead to get a one-off timer.

Arguments
time

Length of time in seconds that the timer will wait before firing.

call

A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.AppTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.apptimer(3.89, stop_saying_it)

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

Create or edit a button widget.

Category: User Interface Functions

Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.

Call = <class 'babase._general._Call'>
def charstr(char_id: SpecialChar) -> str:
616def charstr(char_id: babase.SpecialChar) -> str:
617    """Get a unicode string representing a special character.
618
619    Category: **General Utility Functions**
620
621    Note that these utilize the private-use block of unicode characters
622    (U+E000-U+F8FF) and are specific to the game; exporting or rendering
623    them elsewhere will be meaningless.
624
625    See babase.SpecialChar for the list of available characters.
626    """
627    return str()

Get a unicode string representing a special character.

Category: General Utility Functions

Note that these utilize the private-use block of unicode characters (U+E000-U+F8FF) and are specific to the game; exporting or rendering them elsewhere will be meaningless.

See babase.SpecialChar for the list of available characters.

def checkboxwidget( edit: Widget | None = None, parent: 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) -> Widget:
204def checkboxwidget(
205    edit: bauiv1.Widget | None = None,
206    parent: bauiv1.Widget | None = None,
207    size: Sequence[float] | None = None,
208    position: Sequence[float] | None = None,
209    text: str | bauiv1.Lstr | None = None,
210    value: bool | None = None,
211    on_value_change_call: Callable[[bool], None] | None = None,
212    on_select_call: Callable[[], None] | None = None,
213    text_scale: float | None = None,
214    textcolor: Sequence[float] | None = None,
215    scale: float | None = None,
216    is_radio_button: bool | None = None,
217    maxwidth: float | None = None,
218    autoselect: bool | None = None,
219    color: Sequence[float] | None = None,
220) -> bauiv1.Widget:
221    """Create or edit a check-box widget.
222
223    Category: **User Interface Functions**
224
225    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
226    a new one is created and returned. Arguments that are not set to None
227    are applied to the Widget.
228    """
229    import bauiv1  # pylint: disable=cyclic-import
230
231    return bauiv1.Widget()

Create or edit a check-box widget.

Category: User Interface Functions

Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.

def clipboard_is_supported() -> bool:
652def clipboard_is_supported() -> bool:
653    """Return whether this platform supports clipboard operations at all.
654
655    Category: **General Utility Functions**
656
657    If this returns False, UIs should not show 'copy to clipboard'
658    buttons, etc.
659    """
660    return bool()

Return whether this platform supports clipboard operations at all.

Category: General Utility Functions

If this returns False, UIs should not show 'copy to clipboard' buttons, etc.

def clipboard_set_text(value: str) -> None:
663def clipboard_set_text(value: str) -> None:
664    """Copy a string to the system clipboard.
665
666    Category: **General Utility Functions**
667
668    Ensure that babase.clipboard_is_supported() returns True before adding
669     buttons/etc. that make use of this functionality.
670    """
671    return None

Copy a string to the system clipboard.

Category: General Utility Functions

Ensure that babase.clipboard_is_supported() returns True before adding buttons/etc. that make use of this functionality.

def columnwidget( edit: Widget | None = None, parent: Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, background: bool | None = None, selected_child: Widget | None = None, visible_child: 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, claims_tab: bool | None = None) -> Widget:
234def columnwidget(
235    edit: bauiv1.Widget | None = None,
236    parent: bauiv1.Widget | None = None,
237    size: Sequence[float] | None = None,
238    position: Sequence[float] | None = None,
239    background: bool | None = None,
240    selected_child: bauiv1.Widget | None = None,
241    visible_child: bauiv1.Widget | None = None,
242    single_depth: bool | None = None,
243    print_list_exit_instructions: bool | None = None,
244    left_border: float | None = None,
245    top_border: float | None = None,
246    bottom_border: float | None = None,
247    selection_loops_to_parent: bool | None = None,
248    border: float | None = None,
249    margin: float | None = None,
250    claims_left_right: bool | None = None,
251    claims_tab: bool | None = None,
252) -> bauiv1.Widget:
253    """Create or edit a column widget.
254
255    Category: **User Interface Functions**
256
257    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
258    a new one is created and returned. Arguments that are not set to None
259    are applied to the Widget.
260    """
261    import bauiv1  # pylint: disable=cyclic-import
262
263    return bauiv1.Widget()

Create or edit a column widget.

Category: User Interface Functions

Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.

def containerwidget( edit: Widget | None = None, parent: Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, background: bool | None = None, selected_child: Widget | None = None, transition: str | None = None, cancel_button: Widget | None = None, start_button: Widget | None = None, root_selectable: bool | None = None, on_activate_call: Optional[Callable[[], NoneType]] = None, claims_left_right: bool | None = None, claims_tab: 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: 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: str | None = None, on_select_call: Optional[Callable[[], NoneType]] = None, claim_outside_clicks: bool | None = None, claims_up_down: bool | None = None) -> Widget:
266def containerwidget(
267    edit: bauiv1.Widget | None = None,
268    parent: bauiv1.Widget | None = None,
269    size: Sequence[float] | None = None,
270    position: Sequence[float] | None = None,
271    background: bool | None = None,
272    selected_child: bauiv1.Widget | None = None,
273    transition: str | None = None,
274    cancel_button: bauiv1.Widget | None = None,
275    start_button: bauiv1.Widget | None = None,
276    root_selectable: bool | None = None,
277    on_activate_call: Callable[[], None] | None = None,
278    claims_left_right: bool | None = None,
279    claims_tab: bool | None = None,
280    selection_loops: bool | None = None,
281    selection_loops_to_parent: bool | None = None,
282    scale: float | None = None,
283    on_outside_click_call: Callable[[], None] | None = None,
284    single_depth: bool | None = None,
285    visible_child: bauiv1.Widget | None = None,
286    stack_offset: Sequence[float] | None = None,
287    color: Sequence[float] | None = None,
288    on_cancel_call: Callable[[], None] | None = None,
289    print_list_exit_instructions: bool | None = None,
290    click_activate: bool | None = None,
291    always_highlight: bool | None = None,
292    selectable: bool | None = None,
293    scale_origin_stack_offset: Sequence[float] | None = None,
294    toolbar_visibility: str | None = None,
295    on_select_call: Callable[[], None] | None = None,
296    claim_outside_clicks: bool | None = None,
297    claims_up_down: bool | None = None,
298) -> bauiv1.Widget:
299    """Create or edit a container widget.
300
301    Category: **User Interface Functions**
302
303    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
304    a new one is created and returned. Arguments that are not set to None
305    are applied to the Widget.
306    """
307    import bauiv1  # pylint: disable=cyclic-import
308
309    return bauiv1.Widget()

Create or edit a container widget.

Category: User Interface Functions

Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.

class ContextRef:
148class ContextRef:
149    """Store or use a ballistica context.
150
151    Category: **General Utility Classes**
152
153    Many operations such as bascenev1.newnode() or bascenev1.gettexture()
154    operate implicitly on a current 'context'. A context is some sort of
155    state that functionality can implicitly use. Context determines, for
156    example, which scene nodes or textures get added to without having to
157    specify it explicitly in the newnode()/gettexture() call. Contexts can
158    also affect object lifecycles; for example a babase.ContextCall will
159    become a no-op when the context it was created in is destroyed.
160
161    In general, if you are a modder, you should not need to worry about
162    contexts; mod code should mostly be getting run in the correct
163    context and timers and other callbacks will take care of saving
164    and restoring contexts automatically. There may be rare cases,
165    however, where you need to deal directly with contexts, and that is
166    where this class comes in.
167
168    Creating a babase.ContextRef() will capture a reference to the current
169    context. Other modules may provide ways to access their contexts; for
170    example a bascenev1.Activity instance has a 'context' attribute. You
171    can also use babase.ContextRef.empty() to create a reference to *no*
172    context. Some code such as UI calls may expect this and may complain
173    if you try to use them within a context.
174
175    ##### Usage
176    ContextRefs are generally used with the Python 'with' statement, which
177    sets the context they point to as current on entry and resets it to
178    the previous value on exit.
179
180    ##### Example
181    Explicitly create a few UI bits with no context set.
182    (UI stuff may complain if called within a context):
183    >>> with bui.ContextRef.empty():
184    ...     my_container = bui.containerwidget()
185    """
186
187    def __init__(
188        self,
189    ) -> None:
190        pass
191
192    def __enter__(self) -> None:
193        """Support for "with" statement."""
194        pass
195
196    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
197        """Support for "with" statement."""
198        pass
199
200    @classmethod
201    def empty(cls) -> ContextRef:
202        """Return a ContextRef pointing to no context.
203
204        This is useful when code should be run free of a context.
205        For example, UI code generally insists on being run this way.
206        Otherwise, callbacks set on the UI could inadvertently stop working
207        due to a game activity ending, which would be unintuitive behavior.
208        """
209        return ContextRef()
210
211    def is_empty(self) -> bool:
212        """Whether the context was created as empty."""
213        return bool()
214
215    def is_expired(self) -> bool:
216        """Whether the context has expired."""
217        return bool()

Store or use a ballistica context.

Category: General Utility Classes

Many operations such as bascenev1.newnode() or bascenev1.gettexture() operate implicitly on a current 'context'. A context is some sort of state that functionality can implicitly use. Context determines, for example, which scene nodes or textures get added to without having to specify it explicitly in the newnode()/gettexture() call. Contexts can also affect object lifecycles; for example a babase.ContextCall will become a no-op when the context it was created in is destroyed.

In general, if you are a modder, you should not need to worry about contexts; mod code should mostly be getting run in the correct context and timers and other callbacks will take care of saving and restoring contexts automatically. There may be rare cases, however, where you need to deal directly with contexts, and that is where this class comes in.

Creating a babase.ContextRef() will capture a reference to the current context. Other modules may provide ways to access their contexts; for example a bascenev1.Activity instance has a 'context' attribute. You can also use babase.ContextRef.empty() to create a reference to no context. Some code such as UI calls may expect this and may complain if you try to use them within a context.

Usage

ContextRefs are generally used with the Python 'with' statement, which sets the context they point to as current on entry and resets it to the previous value on exit.

Example

Explicitly create a few UI bits with no context set. (UI stuff may complain if called within a context):

>>> with bui.ContextRef.empty():
...     my_container = bui.containerwidget()
@classmethod
def empty(cls) -> ContextRef:
200    @classmethod
201    def empty(cls) -> ContextRef:
202        """Return a ContextRef pointing to no context.
203
204        This is useful when code should be run free of a context.
205        For example, UI code generally insists on being run this way.
206        Otherwise, callbacks set on the UI could inadvertently stop working
207        due to a game activity ending, which would be unintuitive behavior.
208        """
209        return ContextRef()

Return a ContextRef pointing to no context.

This is useful when code should be run free of a context. For example, UI code generally insists on being run this way. Otherwise, callbacks set on the UI could inadvertently stop working due to a game activity ending, which would be unintuitive behavior.

def is_empty(self) -> bool:
211    def is_empty(self) -> bool:
212        """Whether the context was created as empty."""
213        return bool()

Whether the context was created as empty.

def is_expired(self) -> bool:
215    def is_expired(self) -> bool:
216        """Whether the context has expired."""
217        return bool()

Whether the context has expired.

def displaytime() -> DisplayTime:
756def displaytime() -> babase.DisplayTime:
757    """Return the current display-time in seconds.
758
759    Category: **General Utility Functions**
760
761    Display-time is a time value intended to be used for animation and other
762    visual purposes. It will generally increment by a consistent amount each
763    frame. It will pass at an overall similar rate to AppTime, but trades
764    accuracy for smoothness.
765
766    Note that the value returned here is simply a float; it just has a
767    unique type in the type-checker's eyes to help prevent it from being
768    accidentally used with time functionality expecting other time types.
769    """
770    import babase  # pylint: disable=cyclic-import
771
772    return babase.DisplayTime(0.0)

Return the current display-time in seconds.

Category: General Utility Functions

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

DisplayTime = DisplayTime
def displaytimer(time: float, call: Callable[[], Any]) -> None:
775def displaytimer(time: float, call: Callable[[], Any]) -> None:
776    """Schedule a callable object to run based on display-time.
777
778    Category: **General Utility Functions**
779
780    This function creates a one-off timer which cannot be canceled or
781    modified once created. If you require the ability to do so, or need
782    a repeating timer, use the babase.DisplayTimer class instead.
783
784    Display-time is a time value intended to be used for animation and other
785    visual purposes. It will generally increment by a consistent amount each
786    frame. It will pass at an overall similar rate to AppTime, but trades
787    accuracy for smoothness.
788
789    ##### Arguments
790    ###### time (float)
791    > Length of time in seconds that the timer will wait before firing.
792
793    ###### call (Callable[[], Any])
794    > A callable Python object. Note that the timer will retain a
795    strong reference to the callable for as long as the timer exists, so you
796    may want to look into concepts such as babase.WeakCall if that is not
797    desired.
798
799    ##### Examples
800    Print some stuff through time:
801    >>> babase.screenmessage('hello from now!')
802    >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
803    ...                       'hello from the future!'))
804    >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
805    ...                       'hello from the future 2!'))
806    """
807    return None

Schedule a callable object to run based on display-time.

Category: General Utility Functions

This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.DisplayTimer class instead.

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Arguments
time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

Examples

Print some stuff through time:

>>> babase.screenmessage('hello from now!')
>>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
...                       'hello from the future!'))
>>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
...                       'hello from the future 2!'))
class DisplayTimer:
220class DisplayTimer:
221    """Timers are used to run code at later points in time.
222
223    Category: **General Utility Classes**
224
225    This class encapsulates a timer based on display-time.
226    The underlying timer will be destroyed when this object is no longer
227    referenced. If you do not want to worry about keeping a reference to
228    your timer around, use the babase.displaytimer() function instead to get a
229    one-off timer.
230
231    Display-time is a time value intended to be used for animation and
232    other visual purposes. It will generally increment by a consistent
233    amount each frame. It will pass at an overall similar rate to AppTime,
234    but trades accuracy for smoothness.
235
236    ##### Arguments
237    ###### time
238    > Length of time in seconds that the timer will wait before firing.
239
240    ###### call
241    > A callable Python object. Remember that the timer will retain a
242    strong reference to the callable for as long as it exists, so you
243    may want to look into concepts such as babase.WeakCall if that is not
244    desired.
245
246    ###### repeat
247    > If True, the timer will fire repeatedly, with each successive
248    firing having the same delay as the first.
249
250    ##### Example
251
252    Use a Timer object to print repeatedly for a few seconds:
253    ... def say_it():
254    ...     babase.screenmessage('BADGER!')
255    ... def stop_saying_it():
256    ...     global g_timer
257    ...     g_timer = None
258    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
259    ... # Create our timer; it will run as long as we have the self.t ref.
260    ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True)
261    ... # Now fire off a one-shot timer to kill it.
262    ... babase.displaytimer(3.89, stop_saying_it)
263    """
264
265    def __init__(
266        self, time: float, call: Callable[[], Any], repeat: bool = False
267    ) -> None:
268        pass

Timers are used to run code at later points in time.

Category: General Utility Classes

This class encapsulates a timer based on display-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.displaytimer() function instead to get a one-off timer.

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Arguments
time

Length of time in seconds that the timer will wait before firing.

call

A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.displaytimer(3.89, stop_saying_it)

DisplayTimer(time: float, call: Callable[[], Any], repeat: bool = False)
265    def __init__(
266        self, time: float, call: Callable[[], Any], repeat: bool = False
267    ) -> None:
268        pass
def do_once() -> bool:
815def do_once() -> bool:
816    """Return whether this is the first time running a line of code.
817
818    Category: **General Utility Functions**
819
820    This is used by 'print_once()' type calls to keep from overflowing
821    logs. The call functions by registering the filename and line where
822    The call is made from.  Returns True if this location has not been
823    registered already, and False if it has.
824
825    ##### Example
826    This print will only fire for the first loop iteration:
827    >>> for i in range(10):
828    ... if babase.do_once():
829    ...     print('HelloWorld once from loop!')
830    """
831    return bool()

Return whether this is the first time running a line of code.

Category: General Utility Functions

This is used by 'print_once()' type calls to keep from overflowing logs. The call functions by registering the filename and line where The call is made from. Returns True if this location has not been registered already, and False if it has.

Example

This print will only fire for the first loop iteration:

>>> for i in range(10):
... if babase.do_once():
...     print('HelloWorld once from loop!')
def get_input_idle_time() -> float:
980def get_input_idle_time() -> float:
981    """Return seconds since any local input occurred (touch, keypress, etc.)."""
982    return float()

Return seconds since any local input occurred (touch, keypress, etc.).

def get_ip_address_type(addr: str) -> socket.AddressFamily:
54def get_ip_address_type(addr: str) -> socket.AddressFamily:
55    """Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
56    import socket
57
58    socket_type = None
59
60    # First try it as an ipv4 address.
61    try:
62        socket.inet_pton(socket.AF_INET, addr)
63        socket_type = socket.AF_INET
64    except OSError:
65        pass
66
67    # Hmm apparently not ipv4; try ipv6.
68    if socket_type is None:
69        try:
70            socket.inet_pton(socket.AF_INET6, addr)
71            socket_type = socket.AF_INET6
72        except OSError:
73            pass
74    if socket_type is None:
75        raise ValueError(f'addr seems to be neither v4 or v6: {addr}')
76    return socket_type

Return socket.AF_INET6 or socket.AF_INET4 for the provided address.

def get_qrcode_texture(url: str) -> Texture:
312def get_qrcode_texture(url: str) -> bauiv1.Texture:
313    """Return a QR code texture.
314
315    The provided url must be 64 bytes or less.
316    """
317    import bauiv1  # pylint: disable=cyclic-import
318
319    return bauiv1.Texture()

Return a QR code texture.

The provided url must be 64 bytes or less.

def get_type_name(cls: type) -> str:
107def get_type_name(cls: type) -> str:
108    """Return a full type name including module for a class."""
109    return f'{cls.__module__}.{cls.__name__}'

Return a full type name including module for a class.

def getclass(name: str, subclassof: type[~T]) -> type[~T]:
70def getclass(name: str, subclassof: type[T]) -> type[T]:
71    """Given a full class name such as foo.bar.MyClass, return the class.
72
73    Category: **General Utility Functions**
74
75    The class will be checked to make sure it is a subclass of the provided
76    'subclassof' class, and a TypeError will be raised if not.
77    """
78    import importlib
79
80    splits = name.split('.')
81    modulename = '.'.join(splits[:-1])
82    classname = splits[-1]
83    module = importlib.import_module(modulename)
84    cls: type = getattr(module, classname)
85
86    if not issubclass(cls, subclassof):
87        raise TypeError(f'{name} is not a subclass of {subclassof}.')
88    return cls

Given a full class name such as foo.bar.MyClass, return the class.

Category: General Utility Functions

The class will be checked to make sure it is a subclass of the provided 'subclassof' class, and a TypeError will be raised if not.

def getmesh(name: str) -> Mesh:
329def getmesh(name: str) -> bauiv1.Mesh:
330    """Load a mesh for use solely in the local user interface."""
331    import bauiv1  # pylint: disable=cyclic-import
332
333    return bauiv1.Mesh()

Load a mesh for use solely in the local user interface.

def getsound(name: str) -> Sound:
336def getsound(name: str) -> bauiv1.Sound:
337    """Load a sound for use in the ui."""
338    import bauiv1  # pylint: disable=cyclic-import
339
340    return bauiv1.Sound()

Load a sound for use in the ui.

def gettexture(name: str) -> Texture:
343def gettexture(name: str) -> bauiv1.Texture:
344    """Load a texture for use in the ui."""
345    import bauiv1  # pylint: disable=cyclic-import
346
347    return bauiv1.Texture()

Load a texture for use in the ui.

def hscrollwidget( edit: Widget | None = None, parent: Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, background: bool | None = None, selected_child: 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, claims_tab: bool | None = None) -> Widget:
350def hscrollwidget(
351    edit: bauiv1.Widget | None = None,
352    parent: bauiv1.Widget | None = None,
353    size: Sequence[float] | None = None,
354    position: Sequence[float] | None = None,
355    background: bool | None = None,
356    selected_child: bauiv1.Widget | None = None,
357    capture_arrows: bool | None = None,
358    on_select_call: Callable[[], None] | None = None,
359    center_small_content: bool | None = None,
360    color: Sequence[float] | None = None,
361    highlight: bool | None = None,
362    border_opacity: float | None = None,
363    simple_culling_h: float | None = None,
364    claims_left_right: bool | None = None,
365    claims_up_down: bool | None = None,
366    claims_tab: bool | None = None,
367) -> bauiv1.Widget:
368    """Create or edit a horizontal scroll widget.
369
370    Category: **User Interface Functions**
371
372    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
373    a new one is created and returned. Arguments that are not set to None
374    are applied to the Widget.
375    """
376    import bauiv1  # pylint: disable=cyclic-import
377
378    return bauiv1.Widget()

Create or edit a horizontal scroll widget.

Category: User Interface Functions

Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.

def imagewidget( edit: Widget | None = None, parent: Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, color: Optional[Sequence[float]] = None, texture: Texture | None = None, opacity: float | None = None, mesh_transparent: Mesh | None = None, mesh_opaque: Mesh | None = None, has_alpha_channel: bool = True, tint_texture: Texture | None = None, tint_color: Optional[Sequence[float]] = None, transition_delay: float | None = None, draw_controller: Widget | None = None, tint2_color: Optional[Sequence[float]] = None, tilt_scale: float | None = None, mask_texture: Texture | None = None, radial_amount: float | None = None) -> Widget:
381def imagewidget(
382    edit: bauiv1.Widget | None = None,
383    parent: bauiv1.Widget | None = None,
384    size: Sequence[float] | None = None,
385    position: Sequence[float] | None = None,
386    color: Sequence[float] | None = None,
387    texture: bauiv1.Texture | None = None,
388    opacity: float | None = None,
389    mesh_transparent: bauiv1.Mesh | None = None,
390    mesh_opaque: bauiv1.Mesh | None = None,
391    has_alpha_channel: bool = True,
392    tint_texture: bauiv1.Texture | None = None,
393    tint_color: Sequence[float] | None = None,
394    transition_delay: float | None = None,
395    draw_controller: bauiv1.Widget | None = None,
396    tint2_color: Sequence[float] | None = None,
397    tilt_scale: float | None = None,
398    mask_texture: bauiv1.Texture | None = None,
399    radial_amount: float | None = None,
400) -> bauiv1.Widget:
401    """Create or edit an image widget.
402
403    Category: **User Interface Functions**
404
405    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
406    a new one is created and returned. Arguments that are not set to None
407    are applied to the Widget.
408    """
409    import bauiv1  # pylint: disable=cyclic-import
410
411    return bauiv1.Widget()

Create or edit an image widget.

Category: User Interface Functions

Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.

def is_browser_likely_available() -> bool:
27def is_browser_likely_available() -> bool:
28    """Return whether a browser likely exists on the current device.
29
30    category: General Utility Functions
31
32    If this returns False you may want to avoid calling babase.show_url()
33    with any lengthy addresses. (ba.show_url() will display an address
34    as a string in a window if unable to bring up a browser, but that
35    is only useful for simple URLs.)
36    """
37    app = _babase.app
38
39    if app.classic is None:
40        logging.warning(
41            'is_browser_likely_available() needs to be updated'
42            ' to work without classic.'
43        )
44        return True
45
46    platform = app.classic.platform
47    hastouchscreen = _babase.hastouchscreen()
48
49    # If we're on a vr device or an android device with no touchscreen,
50    # assume no browser.
51    # FIXME: Might not be the case anymore; should make this definable
52    #  at the platform level.
53    if app.env.vr or (platform == 'android' and not hastouchscreen):
54        return False
55
56    # Anywhere else assume we've got one.
57    return True

Return whether a browser likely exists on the current device.

category: General Utility Functions

If this returns False you may want to avoid calling babase.show_url() with any lengthy addresses. (ba.show_url() will display an address as a string in a window if unable to bring up a browser, but that is only useful for simple URLs.)

class Keyboard:
14class Keyboard:
15    """Chars definitions for on-screen keyboard.
16
17    Category: **App Classes**
18
19    Keyboards are discoverable by the meta-tag system
20    and the user can select which one they want to use.
21    On-screen keyboard uses chars from active babase.Keyboard.
22    """
23
24    name: str
25    """Displays when user selecting this keyboard."""
26
27    chars: list[tuple[str, ...]]
28    """Used for row/column lengths."""
29
30    pages: dict[str, tuple[str, ...]]
31    """Extra chars like emojis."""
32
33    nums: tuple[str, ...]
34    """The 'num' page."""

Chars definitions for on-screen keyboard.

Category: App Classes

Keyboards are discoverable by the meta-tag system and the user can select which one they want to use. On-screen keyboard uses chars from active babase.Keyboard.

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 DEBUG_LOG:
 98            if state is None:
 99                logging.debug(
100                    'LoginAdapter: %s implicit state changed;'
101                    ' now signed out.',
102                    self.login_type.name,
103                )
104            else:
105                logging.debug(
106                    'LoginAdapter: %s implicit state changed;'
107                    ' now signed in as %s.',
108                    self.login_type.name,
109                    state.display_name,
110                )
111
112        self._implicit_login_state = state
113        self._implicit_login_state_dirty = True
114
115        # (possibly) push it to the app for handling.
116        self._update_implicit_login_state()
117
118        # This might affect whether we consider that back-end as 'active'.
119        self._update_back_end_active()
120
121    def set_active_logins(self, logins: dict[LoginType, str]) -> None:
122        """Keep the adapter informed of actively used logins.
123
124        This should be called by the app's account subsystem to
125        keep adapters up to date on the full set of logins attached
126        to the currently-in-use account.
127        Note that the logins dict passed in should be immutable as
128        only a reference to it is stored, not a copy.
129        """
130        assert _babase.in_logic_thread()
131        if DEBUG_LOG:
132            logging.debug(
133                'LoginAdapter: %s adapter got active logins %s.',
134                self.login_type.name,
135                {k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
136            )
137
138        self._active_login_id = logins.get(self.login_type)
139        self._update_back_end_active()
140
141    def on_back_end_active_change(self, active: bool) -> None:
142        """Called when active state for the back-end is (possibly) changing.
143
144        Meant to be overridden by subclasses.
145        Being active means that the implicit login provided by the back-end
146        is actually being used by the app. It should therefore register
147        unlocked achievements, leaderboard scores, allow viewing native
148        UIs, etc. When not active it should ignore everything and behave
149        as if signed out, even if it technically is still signed in.
150        """
151        assert _babase.in_logic_thread()
152        del active  # Unused.
153
154    @final
155    def sign_in(
156        self,
157        result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
158        description: str,
159    ) -> None:
160        """Attempt to sign in via this adapter.
161
162        This can be called even if the back-end is not implicitly signed in;
163        the adapter will attempt to sign in if possible. An exception will
164        be returned if the sign-in attempt fails.
165        """
166        assert _babase.in_logic_thread()
167        from babase._general import Call
168
169        # Have been seeing multiple sign-in attempts come through
170        # nearly simultaneously which can be problematic server-side.
171        # Let's error if a sign-in attempt is made within a few seconds
172        # of the last one to try and address this.
173        now = time.monotonic()
174        appnow = _babase.apptime()
175        if self._last_sign_in_time is not None:
176            since_last = now - self._last_sign_in_time
177            if since_last < 1.0:
178                logging.warning(
179                    'LoginAdapter: %s adapter sign_in() called too soon'
180                    ' (%.2fs) after last; this-desc="%s", last-desc="%s",'
181                    ' ba-app-time=%.2f.',
182                    self.login_type.name,
183                    since_last,
184                    description,
185                    self._last_sign_in_desc,
186                    appnow,
187                )
188                _babase.pushcall(
189                    Call(
190                        result_cb,
191                        self,
192                        RuntimeError('sign_in called too soon after last.'),
193                    )
194                )
195                return
196
197        self._last_sign_in_desc = description
198        self._last_sign_in_time = now
199
200        if DEBUG_LOG:
201            logging.debug(
202                'LoginAdapter: %s adapter sign_in() called;'
203                ' fetching sign-in-token...',
204                self.login_type.name,
205            )
206
207        def _got_sign_in_token_result(result: str | None) -> None:
208            import bacommon.cloud
209
210            # Failed to get a sign-in-token.
211            if result is None:
212                if DEBUG_LOG:
213                    logging.debug(
214                        'LoginAdapter: %s adapter sign-in-token fetch failed;'
215                        ' aborting sign-in.',
216                        self.login_type.name,
217                    )
218                _babase.pushcall(
219                    Call(
220                        result_cb,
221                        self,
222                        RuntimeError('fetch-sign-in-token failed.'),
223                    )
224                )
225                return
226
227            # Got a sign-in token! Now pass it to the cloud which will use
228            # it to verify our identity and give us app credentials on
229            # success.
230            if DEBUG_LOG:
231                logging.debug(
232                    'LoginAdapter: %s adapter sign-in-token fetch succeeded;'
233                    ' passing to cloud for verification...',
234                    self.login_type.name,
235                )
236
237            def _got_sign_in_response(
238                response: bacommon.cloud.SignInResponse | Exception,
239            ) -> None:
240                # This likely means we couldn't communicate with the server.
241                if isinstance(response, Exception):
242                    if DEBUG_LOG:
243                        logging.debug(
244                            'LoginAdapter: %s adapter got error'
245                            ' sign-in response: %s',
246                            self.login_type.name,
247                            response,
248                        )
249                    _babase.pushcall(Call(result_cb, self, response))
250                else:
251                    # This means our credentials were explicitly rejected.
252                    if response.credentials is None:
253                        result2: LoginAdapter.SignInResult | Exception = (
254                            RuntimeError('Sign-in-token was rejected.')
255                        )
256                    else:
257                        if DEBUG_LOG:
258                            logging.debug(
259                                'LoginAdapter: %s adapter got successful'
260                                ' sign-in response',
261                                self.login_type.name,
262                            )
263                        result2 = self.SignInResult(
264                            credentials=response.credentials
265                        )
266                    _babase.pushcall(Call(result_cb, self, result2))
267
268            assert _babase.app.plus is not None
269            _babase.app.plus.cloud.send_message_cb(
270                bacommon.cloud.SignInMessage(
271                    self.login_type,
272                    result,
273                    description=description,
274                    apptime=appnow,
275                ),
276                on_response=_got_sign_in_response,
277            )
278
279        # Kick off the sign-in process by fetching a sign-in token.
280        self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
281
282    def is_back_end_active(self) -> bool:
283        """Is this adapter's back-end currently active?"""
284        return self._back_end_active
285
286    def get_sign_in_token(
287        self, completion_cb: Callable[[str | None], None]
288    ) -> None:
289        """Get a sign-in token from the adapter back end.
290
291        This token is then passed to the master-server to complete the
292        sign-in process. The adapter can use this opportunity to bring
293        up account creation UI, call its internal sign_in function, etc.
294        as needed. The provided completion_cb should then be called with
295        either a token or None if sign in failed or was cancelled.
296        """
297        from babase._general import Call
298
299        # Default implementation simply fails immediately.
300        _babase.pushcall(Call(completion_cb, None))
301
302    def _update_implicit_login_state(self) -> None:
303        # If we've received an implicit login state, schedule it to be
304        # sent along to the app. We wait until on-app-loading has been
305        # called so that account-client-v2 has had a chance to load
306        # any existing state so it can properly respond to this.
307        if self._implicit_login_state_dirty and self._on_app_loading_called:
308            from babase._general import Call
309
310            if DEBUG_LOG:
311                logging.debug(
312                    'LoginAdapter: %s adapter sending'
313                    ' implicit-state-changed to app.',
314                    self.login_type.name,
315                )
316
317            assert _babase.app.plus is not None
318            _babase.pushcall(
319                Call(
320                    _babase.app.plus.accounts.on_implicit_login_state_changed,
321                    self.login_type,
322                    self._implicit_login_state,
323                )
324            )
325            self._implicit_login_state_dirty = False
326
327    def _update_back_end_active(self) -> None:
328        was_active = self._back_end_active
329        if self._implicit_login_state is None:
330            is_active = False
331        else:
332            is_active = (
333                self._implicit_login_state.login_id == self._active_login_id
334            )
335        if was_active != is_active:
336            if DEBUG_LOG:
337                logging.debug(
338                    'LoginAdapter: %s adapter back-end-active is now %s.',
339                    self.login_type.name,
340                    is_active,
341                )
342            self.on_back_end_active_change(is_active)
343            self._back_end_active = is_active

Allows using implicit login types in an explicit way.

Some login types such as Google Play Game Services or Game Center are basically always present and often do not provide a way to log out from within a running app, so this adapter exists to use them in a flexible manner by 'attaching' and 'detaching' from an always-present login, allowing for its use alongside other login types. It also provides common functionality for server-side account verification and other handy bits.

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 DEBUG_LOG:
 98            if state is None:
 99                logging.debug(
100                    'LoginAdapter: %s implicit state changed;'
101                    ' now signed out.',
102                    self.login_type.name,
103                )
104            else:
105                logging.debug(
106                    'LoginAdapter: %s implicit state changed;'
107                    ' now signed in as %s.',
108                    self.login_type.name,
109                    state.display_name,
110                )
111
112        self._implicit_login_state = state
113        self._implicit_login_state_dirty = True
114
115        # (possibly) push it to the app for handling.
116        self._update_implicit_login_state()
117
118        # This might affect whether we consider that back-end as 'active'.
119        self._update_back_end_active()

Keep the adapter informed of implicit login states.

This should be called by the adapter back-end when an account of their associated type gets logged in or out.

def set_active_logins(self, logins: dict[bacommon.login.LoginType, str]) -> None:
121    def set_active_logins(self, logins: dict[LoginType, str]) -> None:
122        """Keep the adapter informed of actively used logins.
123
124        This should be called by the app's account subsystem to
125        keep adapters up to date on the full set of logins attached
126        to the currently-in-use account.
127        Note that the logins dict passed in should be immutable as
128        only a reference to it is stored, not a copy.
129        """
130        assert _babase.in_logic_thread()
131        if DEBUG_LOG:
132            logging.debug(
133                'LoginAdapter: %s adapter got active logins %s.',
134                self.login_type.name,
135                {k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
136            )
137
138        self._active_login_id = logins.get(self.login_type)
139        self._update_back_end_active()

Keep the adapter informed of actively used logins.

This should be called by the app's account subsystem to keep adapters up to date on the full set of logins attached to the currently-in-use account. Note that the logins dict passed in should be immutable as only a reference to it is stored, not a copy.

def on_back_end_active_change(self, active: bool) -> None:
141    def on_back_end_active_change(self, active: bool) -> None:
142        """Called when active state for the back-end is (possibly) changing.
143
144        Meant to be overridden by subclasses.
145        Being active means that the implicit login provided by the back-end
146        is actually being used by the app. It should therefore register
147        unlocked achievements, leaderboard scores, allow viewing native
148        UIs, etc. When not active it should ignore everything and behave
149        as if signed out, even if it technically is still signed in.
150        """
151        assert _babase.in_logic_thread()
152        del active  # Unused.

Called when active state for the back-end is (possibly) changing.

Meant to be overridden by subclasses. Being active means that the implicit login provided by the back-end is actually being used by the app. It should therefore register unlocked achievements, leaderboard scores, allow viewing native UIs, etc. When not active it should ignore everything and behave as if signed out, even if it technically is still signed in.

@final
def sign_in( self, result_cb: Callable[[LoginAdapter, LoginAdapter.SignInResult | Exception], NoneType], description: str) -> None:
154    @final
155    def sign_in(
156        self,
157        result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
158        description: str,
159    ) -> None:
160        """Attempt to sign in via this adapter.
161
162        This can be called even if the back-end is not implicitly signed in;
163        the adapter will attempt to sign in if possible. An exception will
164        be returned if the sign-in attempt fails.
165        """
166        assert _babase.in_logic_thread()
167        from babase._general import Call
168
169        # Have been seeing multiple sign-in attempts come through
170        # nearly simultaneously which can be problematic server-side.
171        # Let's error if a sign-in attempt is made within a few seconds
172        # of the last one to try and address this.
173        now = time.monotonic()
174        appnow = _babase.apptime()
175        if self._last_sign_in_time is not None:
176            since_last = now - self._last_sign_in_time
177            if since_last < 1.0:
178                logging.warning(
179                    'LoginAdapter: %s adapter sign_in() called too soon'
180                    ' (%.2fs) after last; this-desc="%s", last-desc="%s",'
181                    ' ba-app-time=%.2f.',
182                    self.login_type.name,
183                    since_last,
184                    description,
185                    self._last_sign_in_desc,
186                    appnow,
187                )
188                _babase.pushcall(
189                    Call(
190                        result_cb,
191                        self,
192                        RuntimeError('sign_in called too soon after last.'),
193                    )
194                )
195                return
196
197        self._last_sign_in_desc = description
198        self._last_sign_in_time = now
199
200        if DEBUG_LOG:
201            logging.debug(
202                'LoginAdapter: %s adapter sign_in() called;'
203                ' fetching sign-in-token...',
204                self.login_type.name,
205            )
206
207        def _got_sign_in_token_result(result: str | None) -> None:
208            import bacommon.cloud
209
210            # Failed to get a sign-in-token.
211            if result is None:
212                if DEBUG_LOG:
213                    logging.debug(
214                        'LoginAdapter: %s adapter sign-in-token fetch failed;'
215                        ' aborting sign-in.',
216                        self.login_type.name,
217                    )
218                _babase.pushcall(
219                    Call(
220                        result_cb,
221                        self,
222                        RuntimeError('fetch-sign-in-token failed.'),
223                    )
224                )
225                return
226
227            # Got a sign-in token! Now pass it to the cloud which will use
228            # it to verify our identity and give us app credentials on
229            # success.
230            if DEBUG_LOG:
231                logging.debug(
232                    'LoginAdapter: %s adapter sign-in-token fetch succeeded;'
233                    ' passing to cloud for verification...',
234                    self.login_type.name,
235                )
236
237            def _got_sign_in_response(
238                response: bacommon.cloud.SignInResponse | Exception,
239            ) -> None:
240                # This likely means we couldn't communicate with the server.
241                if isinstance(response, Exception):
242                    if DEBUG_LOG:
243                        logging.debug(
244                            'LoginAdapter: %s adapter got error'
245                            ' sign-in response: %s',
246                            self.login_type.name,
247                            response,
248                        )
249                    _babase.pushcall(Call(result_cb, self, response))
250                else:
251                    # This means our credentials were explicitly rejected.
252                    if response.credentials is None:
253                        result2: LoginAdapter.SignInResult | Exception = (
254                            RuntimeError('Sign-in-token was rejected.')
255                        )
256                    else:
257                        if DEBUG_LOG:
258                            logging.debug(
259                                'LoginAdapter: %s adapter got successful'
260                                ' sign-in response',
261                                self.login_type.name,
262                            )
263                        result2 = self.SignInResult(
264                            credentials=response.credentials
265                        )
266                    _babase.pushcall(Call(result_cb, self, result2))
267
268            assert _babase.app.plus is not None
269            _babase.app.plus.cloud.send_message_cb(
270                bacommon.cloud.SignInMessage(
271                    self.login_type,
272                    result,
273                    description=description,
274                    apptime=appnow,
275                ),
276                on_response=_got_sign_in_response,
277            )
278
279        # Kick off the sign-in process by fetching a sign-in token.
280        self.get_sign_in_token(completion_cb=_got_sign_in_token_result)

Attempt to sign in via this adapter.

This can be called even if the back-end is not implicitly signed in; the adapter will attempt to sign in if possible. An exception will be returned if the sign-in attempt fails.

def is_back_end_active(self) -> bool:
282    def is_back_end_active(self) -> bool:
283        """Is this adapter's back-end currently active?"""
284        return self._back_end_active

Is this adapter's back-end currently active?

def get_sign_in_token(self, completion_cb: Callable[[str | None], NoneType]) -> None:
286    def get_sign_in_token(
287        self, completion_cb: Callable[[str | None], None]
288    ) -> None:
289        """Get a sign-in token from the adapter back end.
290
291        This token is then passed to the master-server to complete the
292        sign-in process. The adapter can use this opportunity to bring
293        up account creation UI, call its internal sign_in function, etc.
294        as needed. The provided completion_cb should then be called with
295        either a token or None if sign in failed or was cancelled.
296        """
297        from babase._general import Call
298
299        # Default implementation simply fails immediately.
300        _babase.pushcall(Call(completion_cb, None))

Get a sign-in token from the adapter back end.

This token is then passed to the master-server to complete the sign-in process. The adapter can use this opportunity to bring up account creation UI, call its internal sign_in function, etc. as needed. The provided completion_cb should then be called with either a token or None if sign in failed or was cancelled.

@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:
440class Lstr:
441    """Used to define strings in a language-independent way.
442
443    Category: **General Utility Classes**
444
445    These should be used whenever possible in place of hard-coded
446    strings so that in-game or UI elements show up correctly on all
447    clients in their currently-active language.
448
449    To see available resource keys, look at any of the bs_language_*.py
450    files in the game or the translations pages at
451    legacy.ballistica.net/translate.
452
453    ##### Examples
454    EXAMPLE 1: specify a string from a resource path
455    >>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')
456
457    EXAMPLE 2: specify a translated string via a category and english
458    value; if a translated value is available, it will be used; otherwise
459    the english value will be. To see available translation categories,
460    look under the 'translations' resource section.
461    >>> mynode.text = babase.Lstr(translate=('gameDescriptions',
462    ...                                  'Defeat all enemies'))
463
464    EXAMPLE 3: specify a raw value and some substitutions. Substitutions
465    can be used with resource and translate modes as well.
466    >>> mynode.text = babase.Lstr(value='${A} / ${B}',
467    ...               subs=[('${A}', str(score)), ('${B}', str(total))])
468
469    EXAMPLE 4: babase.Lstr's can be nested. This example would display the
470    resource at res_a but replace ${NAME} with the value of the
471    resource at res_b
472    >>> mytextnode.text = babase.Lstr(
473    ...     resource='res_a',
474    ...     subs=[('${NAME}', babase.Lstr(resource='res_b'))])
475    """
476
477    # pylint: disable=dangerous-default-value
478    # noinspection PyDefaultArgument
479    @overload
480    def __init__(
481        self,
482        *,
483        resource: str,
484        fallback_resource: str = '',
485        fallback_value: str = '',
486        subs: Sequence[tuple[str, str | Lstr]] = [],
487    ) -> None:
488        """Create an Lstr from a string resource."""
489
490    # noinspection PyShadowingNames,PyDefaultArgument
491    @overload
492    def __init__(
493        self,
494        *,
495        translate: tuple[str, str],
496        subs: Sequence[tuple[str, str | Lstr]] = [],
497    ) -> None:
498        """Create an Lstr by translating a string in a category."""
499
500    # noinspection PyDefaultArgument
501    @overload
502    def __init__(
503        self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = []
504    ) -> None:
505        """Create an Lstr from a raw string value."""
506
507    # pylint: enable=redefined-outer-name, dangerous-default-value
508
509    def __init__(self, *args: Any, **keywds: Any) -> None:
510        """Instantiate a Lstr.
511
512        Pass a value for either 'resource', 'translate',
513        or 'value'. (see Lstr help for examples).
514        'subs' can be a sequence of 2-member sequences consisting of values
515        and replacements.
516        'fallback_resource' can be a resource key that will be used if the
517        main one is not present for
518        the current language in place of falling back to the english value
519        ('resource' mode only).
520        'fallback_value' can be a literal string that will be used if neither
521        the resource nor the fallback resource is found ('resource' mode only).
522        """
523        # pylint: disable=too-many-branches
524        if args:
525            raise TypeError('Lstr accepts only keyword arguments')
526
527        # Basically just store the exact args they passed.
528        # However if they passed any Lstr values for subs,
529        # replace them with that Lstr's dict.
530        self.args = keywds
531        our_type = type(self)
532
533        if isinstance(self.args.get('value'), our_type):
534            raise TypeError("'value' must be a regular string; not an Lstr")
535
536        if 'subs' in self.args:
537            subs_new = []
538            for key, value in keywds['subs']:
539                if isinstance(value, our_type):
540                    subs_new.append((key, value.args))
541                else:
542                    subs_new.append((key, value))
543            self.args['subs'] = subs_new
544
545        # As of protocol 31 we support compact key names
546        # ('t' instead of 'translate', etc). Convert as needed.
547        if 'translate' in keywds:
548            keywds['t'] = keywds['translate']
549            del keywds['translate']
550        if 'resource' in keywds:
551            keywds['r'] = keywds['resource']
552            del keywds['resource']
553        if 'value' in keywds:
554            keywds['v'] = keywds['value']
555            del keywds['value']
556        if 'fallback' in keywds:
557            from babase import _error
558
559            _error.print_error(
560                'deprecated "fallback" arg passed to Lstr(); use '
561                'either "fallback_resource" or "fallback_value"',
562                once=True,
563            )
564            keywds['f'] = keywds['fallback']
565            del keywds['fallback']
566        if 'fallback_resource' in keywds:
567            keywds['f'] = keywds['fallback_resource']
568            del keywds['fallback_resource']
569        if 'subs' in keywds:
570            keywds['s'] = keywds['subs']
571            del keywds['subs']
572        if 'fallback_value' in keywds:
573            keywds['fv'] = keywds['fallback_value']
574            del keywds['fallback_value']
575
576    def evaluate(self) -> str:
577        """Evaluate the Lstr and returns a flat string in the current language.
578
579        You should avoid doing this as much as possible and instead pass
580        and store Lstr values.
581        """
582        return _babase.evaluate_lstr(self._get_json())
583
584    def is_flat_value(self) -> bool:
585        """Return whether the Lstr is a 'flat' value.
586
587        This is defined as a simple string value incorporating no
588        translations, resources, or substitutions. In this case it may
589        be reasonable to replace it with a raw string value, perform
590        string manipulation on it, etc.
591        """
592        return bool('v' in self.args and not self.args.get('s', []))
593
594    def _get_json(self) -> str:
595        try:
596            return json.dumps(self.args, separators=(',', ':'))
597        except Exception:
598            from babase import _error
599
600            _error.print_exception('_get_json failed for', self.args)
601            return 'JSON_ERR'
602
603    @override
604    def __str__(self) -> str:
605        return '<ba.Lstr: ' + self._get_json() + '>'
606
607    @override
608    def __repr__(self) -> str:
609        return '<ba.Lstr: ' + self._get_json() + '>'
610
611    @staticmethod
612    def from_json(json_string: str) -> babase.Lstr:
613        """Given a json string, returns a babase.Lstr. Does no validation."""
614        lstr = Lstr(value='')
615        lstr.args = json.loads(json_string)
616        return lstr

Used to define strings in a language-independent way.

Category: General Utility Classes

These should be used whenever possible in place of hard-coded strings so that in-game or UI elements show up correctly on all clients in their currently-active language.

To see available resource keys, look at any of the bs_language_*.py files in the game or the translations pages at legacy.ballistica.net/translate.

Examples

EXAMPLE 1: specify a string from a resource path

>>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')

EXAMPLE 2: specify a translated string via a category and english value; if a translated value is available, it will be used; otherwise the english value will be. To see available translation categories, look under the 'translations' resource section.

>>> mynode.text = babase.Lstr(translate=('gameDescriptions',
...                                  'Defeat all enemies'))

EXAMPLE 3: specify a raw value and some substitutions. Substitutions can be used with resource and translate modes as well.

>>> mynode.text = babase.Lstr(value='${A} / ${B}',
...               subs=[('${A}', str(score)), ('${B}', str(total))])

EXAMPLE 4: babase.Lstr's can be nested. This example would display the resource at res_a but replace ${NAME} with the value of the resource at res_b

>>> mytextnode.text = babase.Lstr(
...     resource='res_a',
...     subs=[('${NAME}', babase.Lstr(resource='res_b'))])
Lstr(*args: Any, **keywds: Any)
509    def __init__(self, *args: Any, **keywds: Any) -> None:
510        """Instantiate a Lstr.
511
512        Pass a value for either 'resource', 'translate',
513        or 'value'. (see Lstr help for examples).
514        'subs' can be a sequence of 2-member sequences consisting of values
515        and replacements.
516        'fallback_resource' can be a resource key that will be used if the
517        main one is not present for
518        the current language in place of falling back to the english value
519        ('resource' mode only).
520        'fallback_value' can be a literal string that will be used if neither
521        the resource nor the fallback resource is found ('resource' mode only).
522        """
523        # pylint: disable=too-many-branches
524        if args:
525            raise TypeError('Lstr accepts only keyword arguments')
526
527        # Basically just store the exact args they passed.
528        # However if they passed any Lstr values for subs,
529        # replace them with that Lstr's dict.
530        self.args = keywds
531        our_type = type(self)
532
533        if isinstance(self.args.get('value'), our_type):
534            raise TypeError("'value' must be a regular string; not an Lstr")
535
536        if 'subs' in self.args:
537            subs_new = []
538            for key, value in keywds['subs']:
539                if isinstance(value, our_type):
540                    subs_new.append((key, value.args))
541                else:
542                    subs_new.append((key, value))
543            self.args['subs'] = subs_new
544
545        # As of protocol 31 we support compact key names
546        # ('t' instead of 'translate', etc). Convert as needed.
547        if 'translate' in keywds:
548            keywds['t'] = keywds['translate']
549            del keywds['translate']
550        if 'resource' in keywds:
551            keywds['r'] = keywds['resource']
552            del keywds['resource']
553        if 'value' in keywds:
554            keywds['v'] = keywds['value']
555            del keywds['value']
556        if 'fallback' in keywds:
557            from babase import _error
558
559            _error.print_error(
560                'deprecated "fallback" arg passed to Lstr(); use '
561                'either "fallback_resource" or "fallback_value"',
562                once=True,
563            )
564            keywds['f'] = keywds['fallback']
565            del keywds['fallback']
566        if 'fallback_resource' in keywds:
567            keywds['f'] = keywds['fallback_resource']
568            del keywds['fallback_resource']
569        if 'subs' in keywds:
570            keywds['s'] = keywds['subs']
571            del keywds['subs']
572        if 'fallback_value' in keywds:
573            keywds['fv'] = keywds['fallback_value']
574            del keywds['fallback_value']

Instantiate a Lstr.

Pass a value for either 'resource', 'translate', or 'value'. (see Lstr help for examples). 'subs' can be a sequence of 2-member sequences consisting of values and replacements. 'fallback_resource' can be a resource key that will be used if the main one is not present for the current language in place of falling back to the english value ('resource' mode only). 'fallback_value' can be a literal string that will be used if neither the resource nor the fallback resource is found ('resource' mode only).

args
def evaluate(self) -> str:
576    def evaluate(self) -> str:
577        """Evaluate the Lstr and returns a flat string in the current language.
578
579        You should avoid doing this as much as possible and instead pass
580        and store Lstr values.
581        """
582        return _babase.evaluate_lstr(self._get_json())

Evaluate the Lstr and returns a flat string in the current language.

You should avoid doing this as much as possible and instead pass and store Lstr values.

def is_flat_value(self) -> bool:
584    def is_flat_value(self) -> bool:
585        """Return whether the Lstr is a 'flat' value.
586
587        This is defined as a simple string value incorporating no
588        translations, resources, or substitutions. In this case it may
589        be reasonable to replace it with a raw string value, perform
590        string manipulation on it, etc.
591        """
592        return bool('v' in self.args and not self.args.get('s', []))

Return whether the Lstr is a 'flat' value.

This is defined as a simple string value incorporating no translations, resources, or substitutions. In this case it may be reasonable to replace it with a raw string value, perform string manipulation on it, etc.

@staticmethod
def from_json(json_string: str) -> Lstr:
611    @staticmethod
612    def from_json(json_string: str) -> babase.Lstr:
613        """Given a json string, returns a babase.Lstr. Does no validation."""
614        lstr = Lstr(value='')
615        lstr.args = json.loads(json_string)
616        return lstr

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

class Mesh:
51class Mesh:
52    """Category: **User Interface Classes**"""
53
54    pass

Category: User Interface Classes

class NotFoundError(builtins.Exception):
26class NotFoundError(Exception):
27    """Exception raised when a referenced object does not exist.
28
29    Category: **Exception Classes**
30    """

Exception raised when a referenced object does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
def open_url(address: str, force_internal: bool = False) -> None:
424def open_url(address: str, force_internal: bool = False) -> None:
425    """Open a provided URL.
426
427    Category: **General Utility Functions**
428
429    Open the provided url in a web-browser, or display the URL
430    string in a window if that isn't possible (or if force_internal
431    is True).
432    """
433    return None

Open a provided URL.

Category: General Utility Functions

Open the provided url in a web-browser, or display the URL string in a window if that isn't possible (or if force_internal is True).

class Permission(enum.Enum):
122class Permission(Enum):
123    """Permissions that can be requested from the OS.
124
125    Category: Enums
126    """
127
128    STORAGE = 0

Permissions that can be requested from the OS.

Category: Enums

STORAGE = <Permission.STORAGE: 0>
Inherited Members
enum.Enum
name
value
class Plugin:
322class Plugin:
323    """A plugin to alter app behavior in some way.
324
325    Category: **App Classes**
326
327    Plugins are discoverable by the meta-tag system
328    and the user can select which ones they want to enable.
329    Enabled plugins are then called at specific times as the
330    app is running in order to modify its behavior in some way.
331    """
332
333    def on_app_running(self) -> None:
334        """Called when the app reaches the running state."""
335
336    def on_app_suspend(self) -> None:
337        """Called when the app enters the suspended state."""
338
339    def on_app_unsuspend(self) -> None:
340        """Called when the app exits the suspended state."""
341
342    def on_app_shutdown(self) -> None:
343        """Called when the app is beginning the shutdown process."""
344
345    def on_app_shutdown_complete(self) -> None:
346        """Called when the app has completed the shutdown process."""
347
348    def has_settings_ui(self) -> bool:
349        """Called to ask if we have settings UI we can show."""
350        return False
351
352    def show_settings_ui(self, source_widget: Any | None) -> None:
353        """Called to show our settings UI."""

A plugin to alter app behavior in some way.

Category: App Classes

Plugins are discoverable by the meta-tag system and the user can select which ones they want to enable. Enabled plugins are then called at specific times as the app is running in order to modify its behavior in some way.

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

Called when the app reaches the running state.

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

Called when the app enters the suspended state.

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

Called when the app exits the suspended state.

def on_app_shutdown(self) -> None:
342    def on_app_shutdown(self) -> None:
343        """Called when the app is beginning the shutdown process."""

Called when the app is beginning the shutdown process.

def on_app_shutdown_complete(self) -> None:
345    def on_app_shutdown_complete(self) -> None:
346        """Called when the app has completed the shutdown process."""

Called when the app has completed the shutdown process.

def has_settings_ui(self) -> bool:
348    def has_settings_ui(self) -> bool:
349        """Called to ask if we have settings UI we can show."""
350        return False

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

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

Called to show our settings UI.

class PluginSpec:
227class PluginSpec:
228    """Represents a plugin the engine knows about.
229
230    Category: **App Classes**
231
232    The 'enabled' attr represents whether this plugin is set to load.
233    Getting or setting that attr affects the corresponding app-config
234    key. Remember to commit the app-config after making any changes.
235
236    The 'attempted_load' attr will be True if the engine has attempted
237    to load the plugin. If 'attempted_load' is True for a PluginSpec
238    but the 'plugin' attr is None, it means there was an error loading
239    the plugin. If a plugin's api-version does not match the running
240    app, if a new plugin is detected with auto-enable-plugins disabled,
241    or if the user has explicitly disabled a plugin, the engine will not
242    even attempt to load it.
243    """
244
245    def __init__(self, class_path: str, loadable: bool):
246        self.class_path = class_path
247        self.loadable = loadable
248        self.attempted_load = False
249        self.plugin: Plugin | None = None
250
251    @property
252    def enabled(self) -> bool:
253        """Whether the user wants this plugin to load."""
254        plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {})
255        assert isinstance(plugstates, dict)
256        val = plugstates.get(self.class_path, {}).get('enabled', False) is True
257        return val
258
259    @enabled.setter
260    def enabled(self, val: bool) -> None:
261        plugstates: dict[str, dict] = _babase.app.config.setdefault(
262            'Plugins', {}
263        )
264        assert isinstance(plugstates, dict)
265        plugstate = plugstates.setdefault(self.class_path, {})
266        plugstate['enabled'] = val
267
268    def attempt_load_if_enabled(self) -> Plugin | None:
269        """Possibly load the plugin and log any errors."""
270        from babase._general import getclass
271        from babase._language import Lstr
272
273        assert not self.attempted_load
274        assert self.plugin is None
275
276        if not self.enabled:
277            return None
278        self.attempted_load = True
279        if not self.loadable:
280            return None
281        try:
282            cls = getclass(self.class_path, Plugin)
283        except Exception as exc:
284            _babase.getsimplesound('error').play()
285            _babase.screenmessage(
286                Lstr(
287                    resource='pluginClassLoadErrorText',
288                    subs=[
289                        ('${PLUGIN}', self.class_path),
290                        ('${ERROR}', str(exc)),
291                    ],
292                ),
293                color=(1, 0, 0),
294            )
295            logging.exception(
296                "Error loading plugin class '%s'.", self.class_path
297            )
298            return None
299        try:
300            self.plugin = cls()
301            return self.plugin
302        except Exception as exc:
303            from babase import _error
304
305            _babase.getsimplesound('error').play()
306            _babase.screenmessage(
307                Lstr(
308                    resource='pluginInitErrorText',
309                    subs=[
310                        ('${PLUGIN}', self.class_path),
311                        ('${ERROR}', str(exc)),
312                    ],
313                ),
314                color=(1, 0, 0),
315            )
316            logging.exception(
317                "Error initing plugin class: '%s'.", self.class_path
318            )
319        return None

Represents a plugin the engine knows about.

Category: App Classes

The 'enabled' attr represents whether this plugin is set to load. Getting or setting that attr affects the corresponding app-config key. Remember to commit the app-config after making any changes.

The 'attempted_load' attr will be True if the engine has attempted to load the plugin. If 'attempted_load' is True for a PluginSpec but the 'plugin' attr is None, it means there was an error loading the plugin. If a plugin's api-version does not match the running app, if a new plugin is detected with auto-enable-plugins disabled, or if the user has explicitly disabled a plugin, the engine will not even attempt to load it.

PluginSpec(class_path: str, loadable: bool)
245    def __init__(self, class_path: str, loadable: bool):
246        self.class_path = class_path
247        self.loadable = loadable
248        self.attempted_load = False
249        self.plugin: Plugin | None = None
class_path
loadable
attempted_load
plugin: Plugin | None
enabled: bool
251    @property
252    def enabled(self) -> bool:
253        """Whether the user wants this plugin to load."""
254        plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {})
255        assert isinstance(plugstates, dict)
256        val = plugstates.get(self.class_path, {}).get('enabled', False) is True
257        return val

Whether the user wants this plugin to load.

def attempt_load_if_enabled(self) -> Plugin | None:
268    def attempt_load_if_enabled(self) -> Plugin | None:
269        """Possibly load the plugin and log any errors."""
270        from babase._general import getclass
271        from babase._language import Lstr
272
273        assert not self.attempted_load
274        assert self.plugin is None
275
276        if not self.enabled:
277            return None
278        self.attempted_load = True
279        if not self.loadable:
280            return None
281        try:
282            cls = getclass(self.class_path, Plugin)
283        except Exception as exc:
284            _babase.getsimplesound('error').play()
285            _babase.screenmessage(
286                Lstr(
287                    resource='pluginClassLoadErrorText',
288                    subs=[
289                        ('${PLUGIN}', self.class_path),
290                        ('${ERROR}', str(exc)),
291                    ],
292                ),
293                color=(1, 0, 0),
294            )
295            logging.exception(
296                "Error loading plugin class '%s'.", self.class_path
297            )
298            return None
299        try:
300            self.plugin = cls()
301            return self.plugin
302        except Exception as exc:
303            from babase import _error
304
305            _babase.getsimplesound('error').play()
306            _babase.screenmessage(
307                Lstr(
308                    resource='pluginInitErrorText',
309                    subs=[
310                        ('${PLUGIN}', self.class_path),
311                        ('${ERROR}', str(exc)),
312                    ],
313                ),
314                color=(1, 0, 0),
315            )
316            logging.exception(
317                "Error initing plugin class: '%s'.", self.class_path
318            )
319        return None

Possibly load the plugin and log any errors.

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:
1326def pushcall(
1327    call: Callable,
1328    from_other_thread: bool = False,
1329    suppress_other_thread_warning: bool = False,
1330    other_thread_use_fg_context: bool = False,
1331    raw: bool = False,
1332) -> None:
1333    """Push a call to the logic event-loop.
1334    Category: **General Utility Functions**
1335
1336    This call expects to be used in the logic thread, and will automatically
1337    save and restore the babase.Context to behave seamlessly.
1338
1339    If you want to push a call from outside of the logic thread,
1340    however, you can pass 'from_other_thread' as True. In this case
1341    the call will always run in the UI context_ref on the logic thread
1342    or whichever context_ref is in the foreground if
1343    other_thread_use_fg_context is True.
1344    Passing raw=True will disable thread checks and context_ref sets/restores.
1345    """
1346    return None

Push a call to the logic event-loop. Category: General Utility Functions

This call expects to be used in the logic thread, and will automatically save and restore the babase.Context to behave seamlessly.

If you want to push a call from outside of the logic thread, however, you can pass 'from_other_thread' as True. In this case the call will always run in the UI context_ref on the logic thread or whichever context_ref is in the foreground if other_thread_use_fg_context is True. Passing raw=True will disable thread checks and context_ref sets/restores.

def quit( confirm: bool = False, quit_type: QuitType | None = None) -> None:
1350def quit(
1351    confirm: bool = False, quit_type: babase.QuitType | None = None
1352) -> None:
1353    """Quit the app.
1354
1355    Category: **General Utility Functions**
1356
1357    If 'confirm' is True, a confirm dialog will be presented if conditions
1358    allow; otherwise the quit will still be immediate.
1359    See docs for babase.QuitType for explanations of the optional
1360    'quit_type' arg.
1361    """
1362    return None

Quit the app.

Category: General Utility Functions

If 'confirm' is True, a confirm dialog will be presented if conditions allow; otherwise the quit will still be immediate. See docs for babase.QuitType for explanations of the optional 'quit_type' arg.

class QuitType(enum.Enum):
42class QuitType(Enum):
43    """Types of input a controller can send to the game.
44
45    Category: Enums
46
47    'soft' may hide/reset the app but keep the process running, depending
48       on the platform.
49
50    'back' is a variant of 'soft' which may give 'back-button-pressed'
51       behavior depending on the platform. (returning to some previous
52       activity instead of dumping to the home screen, etc.)
53
54    'hard' leads to the process exiting. This generally should be avoided
55       on platforms such as mobile.
56    """
57
58    SOFT = 0
59    BACK = 1
60    HARD = 2

Types of input a controller can send to the game.

Category: Enums

'soft' may hide/reset the app but keep the process running, depending on the platform.

'back' is a variant of 'soft' which may give 'back-button-pressed' behavior depending on the platform. (returning to some previous activity instead of dumping to the home screen, etc.)

'hard' leads to the process exiting. This generally should be avoided on platforms such as mobile.

SOFT = <QuitType.SOFT: 0>
BACK = <QuitType.BACK: 1>
HARD = <QuitType.HARD: 2>
Inherited Members
enum.Enum
name
value
def rowwidget( edit: Widget | None = None, parent: Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, background: bool | None = None, selected_child: Widget | None = None, visible_child: Widget | None = None, claims_left_right: bool | None = None, claims_tab: bool | None = None, selection_loops_to_parent: bool | None = None) -> Widget:
436def rowwidget(
437    edit: bauiv1.Widget | None = None,
438    parent: bauiv1.Widget | None = None,
439    size: Sequence[float] | None = None,
440    position: Sequence[float] | None = None,
441    background: bool | None = None,
442    selected_child: bauiv1.Widget | None = None,
443    visible_child: bauiv1.Widget | None = None,
444    claims_left_right: bool | None = None,
445    claims_tab: bool | None = None,
446    selection_loops_to_parent: bool | None = None,
447) -> bauiv1.Widget:
448    """Create or edit a row widget.
449
450    Category: **User Interface Functions**
451
452    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
453    a new one is created and returned. Arguments that are not set to None
454    are applied to the Widget.
455    """
456    import bauiv1  # pylint: disable=cyclic-import
457
458    return bauiv1.Widget()

Create or edit a row widget.

Category: User Interface Functions

Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.

def safecolor( color: Sequence[float], target_intensity: float = 0.6) -> tuple[float, ...]:
1400def safecolor(
1401    color: Sequence[float], target_intensity: float = 0.6
1402) -> tuple[float, ...]:
1403    """Given a color tuple, return a color safe to display as text.
1404
1405    Category: **General Utility Functions**
1406
1407    Accepts tuples of length 3 or 4. This will slightly brighten very
1408    dark colors, etc.
1409    """
1410    return (0.0, 0.0, 0.0)

Given a color tuple, return a color safe to display as text.

Category: General Utility Functions

Accepts tuples of length 3 or 4. This will slightly brighten very dark colors, etc.

def screenmessage( message: str | Lstr, color: Optional[Sequence[float]] = None, log: bool = False) -> None:
1413def screenmessage(
1414    message: str | babase.Lstr,
1415    color: Sequence[float] | None = None,
1416    log: bool = False,
1417) -> None:
1418    """Print a message to the local client's screen, in a given color.
1419
1420    Category: **General Utility Functions**
1421
1422    Note that this version of the function is purely for local display.
1423    To broadcast screen messages in network play, look for methods such as
1424    broadcastmessage() provided by the scene-version packages.
1425    """
1426    return None

Print a message to the local client's screen, in a given color.

Category: General Utility Functions

Note that this version of the function is purely for local display. To broadcast screen messages in network play, look for methods such as broadcastmessage() provided by the scene-version packages.

def scrollwidget( edit: Widget | None = None, parent: Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, background: bool | None = None, selected_child: Widget | None = None, capture_arrows: bool = False, on_select_call: Optional[Callable] = None, center_small_content: 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, claims_tab: bool | None = None, autoselect: bool | None = None) -> Widget:
461def scrollwidget(
462    edit: bauiv1.Widget | None = None,
463    parent: bauiv1.Widget | None = None,
464    size: Sequence[float] | None = None,
465    position: Sequence[float] | None = None,
466    background: bool | None = None,
467    selected_child: bauiv1.Widget | None = None,
468    capture_arrows: bool = False,
469    on_select_call: Callable | None = None,
470    center_small_content: bool | None = None,
471    color: Sequence[float] | None = None,
472    highlight: bool | None = None,
473    border_opacity: float | None = None,
474    simple_culling_v: float | None = None,
475    selection_loops_to_parent: bool | None = None,
476    claims_left_right: bool | None = None,
477    claims_up_down: bool | None = None,
478    claims_tab: bool | None = None,
479    autoselect: bool | None = None,
480) -> bauiv1.Widget:
481    """Create or edit a scroll widget.
482
483    Category: **User Interface Functions**
484
485    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
486    a new one is created and returned. Arguments that are not set to None
487    are applied to the Widget.
488    """
489    import bauiv1  # pylint: disable=cyclic-import
490
491    return bauiv1.Widget()

Create or edit a scroll widget.

Category: User Interface Functions

Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.

def set_analytics_screen(screen: str) -> None:
1429def set_analytics_screen(screen: str) -> None:
1430    """Used for analytics to see where in the app players spend their time.
1431
1432    Category: **General Utility Functions**
1433
1434    Generally called when opening a new window or entering some UI.
1435    'screen' should be a string description of an app location
1436    ('Main Menu', etc.)
1437    """
1438    return None

Used for analytics to see where in the app players spend their time.

Category: General Utility Functions

Generally called when opening a new window or entering some UI. 'screen' should be a string description of an app location ('Main Menu', etc.)

class Sound:
57class Sound:
58    """Category: **User Interface Classes**"""
59
60    def play(self) -> None:
61        """Play the sound locally."""
62        return None
63
64    def stop(self) -> None:
65        """Stop the sound if it is playing."""
66        return None

Category: User Interface Classes

def play(self) -> None:
60    def play(self) -> None:
61        """Play the sound locally."""
62        return None

Play the sound locally.

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

Stop the sound if it is playing.

class SpecialChar(enum.Enum):
131class SpecialChar(Enum):
132    """Special characters the game can print.
133
134    Category: Enums
135    """
136
137    DOWN_ARROW = 0
138    UP_ARROW = 1
139    LEFT_ARROW = 2
140    RIGHT_ARROW = 3
141    TOP_BUTTON = 4
142    LEFT_BUTTON = 5
143    RIGHT_BUTTON = 6
144    BOTTOM_BUTTON = 7
145    DELETE = 8
146    SHIFT = 9
147    BACK = 10
148    LOGO_FLAT = 11
149    REWIND_BUTTON = 12
150    PLAY_PAUSE_BUTTON = 13
151    FAST_FORWARD_BUTTON = 14
152    DPAD_CENTER_BUTTON = 15
153    PLAY_STATION_CROSS_BUTTON = 16
154    PLAY_STATION_CIRCLE_BUTTON = 17
155    PLAY_STATION_TRIANGLE_BUTTON = 18
156    PLAY_STATION_SQUARE_BUTTON = 19
157    PLAY_BUTTON = 20
158    PAUSE_BUTTON = 21
159    OUYA_BUTTON_O = 22
160    OUYA_BUTTON_U = 23
161    OUYA_BUTTON_Y = 24
162    OUYA_BUTTON_A = 25
163    OUYA_LOGO = 26
164    LOGO = 27
165    TICKET = 28
166    GOOGLE_PLAY_GAMES_LOGO = 29
167    GAME_CENTER_LOGO = 30
168    DICE_BUTTON1 = 31
169    DICE_BUTTON2 = 32
170    DICE_BUTTON3 = 33
171    DICE_BUTTON4 = 34
172    GAME_CIRCLE_LOGO = 35
173    PARTY_ICON = 36
174    TEST_ACCOUNT = 37
175    TICKET_BACKING = 38
176    TROPHY1 = 39
177    TROPHY2 = 40
178    TROPHY3 = 41
179    TROPHY0A = 42
180    TROPHY0B = 43
181    TROPHY4 = 44
182    LOCAL_ACCOUNT = 45
183    EXPLODINARY_LOGO = 46
184    FLAG_UNITED_STATES = 47
185    FLAG_MEXICO = 48
186    FLAG_GERMANY = 49
187    FLAG_BRAZIL = 50
188    FLAG_RUSSIA = 51
189    FLAG_CHINA = 52
190    FLAG_UNITED_KINGDOM = 53
191    FLAG_CANADA = 54
192    FLAG_INDIA = 55
193    FLAG_JAPAN = 56
194    FLAG_FRANCE = 57
195    FLAG_INDONESIA = 58
196    FLAG_ITALY = 59
197    FLAG_SOUTH_KOREA = 60
198    FLAG_NETHERLANDS = 61
199    FEDORA = 62
200    HAL = 63
201    CROWN = 64
202    YIN_YANG = 65
203    EYE_BALL = 66
204    SKULL = 67
205    HEART = 68
206    DRAGON = 69
207    HELMET = 70
208    MUSHROOM = 71
209    NINJA_STAR = 72
210    VIKING_HELMET = 73
211    MOON = 74
212    SPIDER = 75
213    FIREBALL = 76
214    FLAG_UNITED_ARAB_EMIRATES = 77
215    FLAG_QATAR = 78
216    FLAG_EGYPT = 79
217    FLAG_KUWAIT = 80
218    FLAG_ALGERIA = 81
219    FLAG_SAUDI_ARABIA = 82
220    FLAG_MALAYSIA = 83
221    FLAG_CZECH_REPUBLIC = 84
222    FLAG_AUSTRALIA = 85
223    FLAG_SINGAPORE = 86
224    OCULUS_LOGO = 87
225    STEAM_LOGO = 88
226    NVIDIA_LOGO = 89
227    FLAG_IRAN = 90
228    FLAG_POLAND = 91
229    FLAG_ARGENTINA = 92
230    FLAG_PHILIPPINES = 93
231    FLAG_CHILE = 94
232    MIKIROG = 95
233    V2_LOGO = 96

Special characters the game can print.

Category: Enums

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>
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>
Inherited Members
enum.Enum
name
value
class Texture:
69class Texture:
70    """Category: **User Interface Classes**"""
71
72    pass

Category: User Interface Classes

def textwidget( edit: Widget | None = None, parent: 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: 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: 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: Widget | None = None, query_description: Widget | None = None, adapter_finished: bool | None = None, glow_type: str | None = None, allow_clear_button: bool | None = None) -> Widget:
504def textwidget(
505    edit: bauiv1.Widget | None = None,
506    parent: bauiv1.Widget | None = None,
507    size: Sequence[float] | None = None,
508    position: Sequence[float] | None = None,
509    text: str | bauiv1.Lstr | None = None,
510    v_align: str | None = None,
511    h_align: str | None = None,
512    editable: bool | None = None,
513    padding: float | None = None,
514    on_return_press_call: Callable[[], None] | None = None,
515    on_activate_call: Callable[[], None] | None = None,
516    selectable: bool | None = None,
517    query: bauiv1.Widget | None = None,
518    max_chars: int | None = None,
519    color: Sequence[float] | None = None,
520    click_activate: bool | None = None,
521    on_select_call: Callable[[], None] | None = None,
522    always_highlight: bool | None = None,
523    draw_controller: bauiv1.Widget | None = None,
524    scale: float | None = None,
525    corner_scale: float | None = None,
526    description: str | bauiv1.Lstr | None = None,
527    transition_delay: float | None = None,
528    maxwidth: float | None = None,
529    max_height: float | None = None,
530    flatness: float | None = None,
531    shadow: float | None = None,
532    autoselect: bool | None = None,
533    rotate: float | None = None,
534    enabled: bool | None = None,
535    force_internal_editing: bool | None = None,
536    always_show_carat: bool | None = None,
537    big: bool | None = None,
538    extra_touch_border_scale: float | None = None,
539    res_scale: float | None = None,
540    query_max_chars: bauiv1.Widget | None = None,
541    query_description: bauiv1.Widget | None = None,
542    adapter_finished: bool | None = None,
543    glow_type: str | None = None,
544    allow_clear_button: bool | None = None,
545) -> bauiv1.Widget:
546    """Create or edit a text widget.
547
548    Category: **User Interface Functions**
549
550    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
551    a new one is created and returned. Arguments that are not set to None
552    are applied to the Widget.
553    """
554    import bauiv1  # pylint: disable=cyclic-import
555
556    return bauiv1.Widget()

Create or edit a text widget.

Category: User Interface Functions

Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise a new one is created and returned. Arguments that are not set to None are applied to the Widget.

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    Category: **General Utility Functions**
22
23    Given a time value, returns a babase.Lstr with:
24    (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).
25
26    WARNING: the underlying Lstr value is somewhat large so don't use this
27    to rapidly update Node text values for an onscreen timer or you may
28    consume significant network bandwidth.  For that purpose you should
29    use a 'timedisplay' Node and attribute connections.
30
31    """
32    from babase._language import Lstr
33
34    # We take float seconds but operate on int milliseconds internally.
35    timeval = int(1000 * timeval)
36    bits = []
37    subs = []
38    hval = (timeval // 1000) // (60 * 60)
39    if hval != 0:
40        bits.append('${H}')
41        subs.append(
42            (
43                '${H}',
44                Lstr(
45                    resource='timeSuffixHoursText',
46                    subs=[('${COUNT}', str(hval))],
47                ),
48            )
49        )
50    mval = ((timeval // 1000) // 60) % 60
51    if mval != 0:
52        bits.append('${M}')
53        subs.append(
54            (
55                '${M}',
56                Lstr(
57                    resource='timeSuffixMinutesText',
58                    subs=[('${COUNT}', str(mval))],
59                ),
60            )
61        )
62
63    # We add seconds if its non-zero *or* we haven't added anything else.
64    if centi:
65        # pylint: disable=consider-using-f-string
66        sval = timeval / 1000.0 % 60.0
67        if sval >= 0.005 or not bits:
68            bits.append('${S}')
69            subs.append(
70                (
71                    '${S}',
72                    Lstr(
73                        resource='timeSuffixSecondsText',
74                        subs=[('${COUNT}', ('%.2f' % sval))],
75                    ),
76                )
77            )
78    else:
79        sval = timeval // 1000 % 60
80        if sval != 0 or not bits:
81            bits.append('${S}')
82            subs.append(
83                (
84                    '${S}',
85                    Lstr(
86                        resource='timeSuffixSecondsText',
87                        subs=[('${COUNT}', str(sval))],
88                    ),
89                )
90            )
91    return Lstr(value=' '.join(bits), subs=subs)

Generate a babase.Lstr for displaying a time value.

Category: General Utility Functions

Given a time value, returns a babase.Lstr with: (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).

WARNING: the underlying Lstr value is somewhat large so don't use this to rapidly update Node text values for an onscreen timer or you may consume significant network bandwidth. For that purpose you should use a 'timedisplay' Node and attribute connections.

def uicleanupcheck(obj: Any, widget: Widget) -> None:
173def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None:
174    """Add a check to ensure a widget-owning object gets cleaned up properly.
175
176    Category: User Interface Functions
177
178    This adds a check which will print an error message if the provided
179    object still exists ~5 seconds after the provided bauiv1.Widget dies.
180
181    This is a good sanity check for any sort of object that wraps or
182    controls a bauiv1.Widget. For instance, a 'Window' class instance has
183    no reason to still exist once its root container bauiv1.Widget has fully
184    transitioned out and been destroyed. Circular references or careless
185    strong referencing can lead to such objects never getting destroyed,
186    however, and this helps detect such cases to avoid memory leaks.
187    """
188    if DEBUG_UI_CLEANUP_CHECKS:
189        print(f'adding uicleanup to {obj}')
190    if not isinstance(widget, _bauiv1.Widget):
191        raise TypeError('widget arg is not a bauiv1.Widget')
192
193    if bool(False):
194
195        def foobar() -> None:
196            """Just testing."""
197            if DEBUG_UI_CLEANUP_CHECKS:
198                print('uicleanupcheck widget dying...')
199
200        widget.add_delete_callback(foobar)
201
202    assert babase.app.classic is not None
203    babase.app.ui_v1.cleanupchecks.append(
204        UICleanupCheck(
205            obj=weakref.ref(obj), widget=widget, widget_death_time=None
206        )
207    )

Add a check to ensure a widget-owning object gets cleaned up properly.

Category: User Interface Functions

This adds a check which will print an error message if the provided object still exists ~5 seconds after the provided bauiv1.Widget dies.

This is a good sanity check for any sort of object that wraps or controls a bauiv1.Widget. For instance, a 'Window' class instance has no reason to still exist once its root container bauiv1.Widget has fully transitioned out and been destroyed. Circular references or careless strong referencing can lead to such objects never getting destroyed, however, and this helps detect such cases to avoid memory leaks.

class UIScale(enum.Enum):
63class UIScale(Enum):
64    """The overall scale the UI is being rendered for. Note that this is
65    independent of pixel resolution. For example, a phone and a desktop PC
66    might render the game at similar pixel resolutions but the size they
67    display content at will vary significantly.
68
69    Category: Enums
70
71    'large' is used for devices such as desktop PCs where fine details can
72       be clearly seen. UI elements are generally smaller on the screen
73       and more content can be seen at once.
74
75    'medium' is used for devices such as tablets, TVs, or VR headsets.
76       This mode strikes a balance between clean readability and amount of
77       content visible.
78
79    'small' is used primarily for phones or other small devices where
80       content needs to be presented as large and clear in order to remain
81       readable from an average distance.
82    """
83
84    LARGE = 0
85    MEDIUM = 1
86    SMALL = 2

The overall scale the UI is being rendered for. Note that this is independent of pixel resolution. For example, a phone and a desktop PC might render the game at similar pixel resolutions but the size they display content at will vary significantly.

Category: Enums

'large' is used for devices such as desktop PCs where fine details can be clearly seen. UI elements are generally smaller on the screen and more content can be seen at once.

'medium' is used for devices such as tablets, TVs, or VR headsets. This mode strikes a balance between clean readability and amount of content visible.

'small' is used primarily for phones or other small devices where content needs to be presented as large and clear in order to remain readable from an average distance.

LARGE = <UIScale.LARGE: 0>
MEDIUM = <UIScale.MEDIUM: 1>
SMALL = <UIScale.SMALL: 2>
Inherited Members
enum.Enum
name
value
class UIV1Subsystem(babase._appsubsystem.AppSubsystem):
 23class UIV1Subsystem(babase.AppSubsystem):
 24    """Consolidated UI functionality for the app.
 25
 26    Category: **App Classes**
 27
 28    To use this class, access the single instance of it at 'ba.app.ui'.
 29    """
 30
 31    def __init__(self) -> None:
 32        super().__init__()
 33        env = babase.env()
 34
 35        self.controller: UIController | None = None
 36
 37        self._main_menu_window: bauiv1.Widget | None = None
 38        self._main_menu_location: str | None = None
 39        self.quit_window: bauiv1.Widget | None = None
 40
 41        # From classic.
 42        self.main_menu_resume_callbacks: list = []  # Can probably go away.
 43
 44        self._uiscale: babase.UIScale
 45
 46        interfacetype = babase.app.config.get('UI Scale', env['ui_scale'])
 47        if interfacetype == 'auto':
 48            interfacetype = env['ui_scale']
 49
 50        if interfacetype == 'large':
 51            self._uiscale = babase.UIScale.LARGE
 52        elif interfacetype == 'medium':
 53            self._uiscale = babase.UIScale.MEDIUM
 54        elif interfacetype == 'small':
 55            self._uiscale = babase.UIScale.SMALL
 56        else:
 57            raise RuntimeError(f'Invalid UIScale value: {interfacetype}')
 58
 59        self.window_states: dict[type, Any] = {}  # FIXME: Kill this.
 60        self.main_menu_selection: str | None = None  # FIXME: Kill this.
 61        self.have_party_queue_window = False
 62        self.cleanupchecks: list[UICleanupCheck] = []
 63        self.upkeeptimer: babase.AppTimer | None = None
 64        self.use_toolbars = _bauiv1.toolbar_test()
 65
 66        self.title_color = (0.72, 0.7, 0.75)
 67        self.heading_color = (0.72, 0.7, 0.75)
 68        self.infotextcolor = (0.7, 0.9, 0.7)
 69
 70        # Switch our overall game selection UI flow between Play and
 71        # Private-party playlist selection modes; should do this in
 72        # a more elegant way once we revamp high level UI stuff a bit.
 73        self.selecting_private_party_playlist: bool = False
 74
 75    @property
 76    def available(self) -> bool:
 77        """Can uiv1 currently be used?
 78
 79        Code that may run in headless mode, before the UI has been spun up,
 80        while other ui systems are active, etc. can check this to avoid
 81        likely erroring.
 82        """
 83        return _bauiv1.is_available()
 84
 85    @property
 86    def uiscale(self) -> babase.UIScale:
 87        """Current ui scale for the app."""
 88        return self._uiscale
 89
 90    @override
 91    def on_app_loading(self) -> None:
 92        from bauiv1._uitypes import UIController, ui_upkeep
 93
 94        # IMPORTANT: If tweaking UI stuff, make sure it behaves for small,
 95        # medium, and large UI modes. (doesn't run off screen, etc).
 96        # The overrides below can be used to test with different sizes.
 97        # Generally small is used on phones, medium is used on tablets/tvs,
 98        # and large is on desktop computers or perhaps large tablets. When
 99        # possible, run in windowed mode and resize the window to assure
100        # this holds true at all aspect ratios.
101
102        # UPDATE: A better way to test this is now by setting the environment
103        # variable BA_UI_SCALE to "small", "medium", or "large".
104        # This will affect system UIs not covered by the values below such
105        # as screen-messages. The below values remain functional, however,
106        # for cases such as Android where environment variables can't be set
107        # easily.
108
109        if bool(False):  # force-test ui scale
110            self._uiscale = babase.UIScale.SMALL
111            with babase.ContextRef.empty():
112                babase.pushcall(
113                    lambda: babase.screenmessage(
114                        f'FORCING UISCALE {self._uiscale.name} FOR TESTING',
115                        color=(1, 0, 1),
116                        log=True,
117                    )
118                )
119
120        self.controller = UIController()
121
122        # Kick off our periodic UI upkeep.
123        # FIXME: Can probably kill this if we do immediate UI death checks.
124        self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True)
125
126    def set_main_menu_window(
127        self,
128        window: bauiv1.Widget,
129        from_window: bauiv1.Widget | None | bool = True,
130    ) -> None:
131        """Set the current 'main' window, replacing any existing.
132
133        If 'from_window' is passed as a bauiv1.Widget or None, a warning
134        will be issued if it that value does not match the current main
135        window. This can help clean up flawed code that can lead to bad
136        UI states. A value of False will disable the check.
137        """
138
139        existing = self._main_menu_window
140
141        try:
142            if isinstance(from_window, bool):
143                # For default val True we warn that the arg wasn't
144                # passed. False can be explicitly passed to disable this
145                # check.
146                if from_window is True:
147                    caller_frame = inspect.stack()[1]
148                    caller_filename = caller_frame.filename
149                    caller_line_number = caller_frame.lineno
150                    logging.warning(
151                        'set_main_menu_window() should be passed a'
152                        " 'from_window' value to help ensure proper UI behavior"
153                        ' (%s line %i).',
154                        caller_filename,
155                        caller_line_number,
156                    )
157            else:
158                # For everything else, warn if what they passed wasn't
159                # the previous main menu widget.
160                if from_window is not existing:
161                    caller_frame = inspect.stack()[1]
162                    caller_filename = caller_frame.filename
163                    caller_line_number = caller_frame.lineno
164                    logging.warning(
165                        "set_main_menu_window() was passed 'from_window' %s"
166                        ' but existing main-menu-window is %s. (%s line %i).',
167                        from_window,
168                        existing,
169                        caller_filename,
170                        caller_line_number,
171                    )
172        except Exception:
173            # Prevent any bugs in these checks from causing problems.
174            logging.exception('Error checking from_window')
175
176        # Once the above code leads to us fixing all leftover window bugs
177        # at the source, we can kill the code below.
178
179        # Let's grab the location where we were called from to report
180        # if we have to force-kill the existing window (which normally
181        # should not happen).
182        frameline = None
183        try:
184            frame = inspect.currentframe()
185            if frame is not None:
186                frame = frame.f_back
187            if frame is not None:
188                frameinfo = inspect.getframeinfo(frame)
189                frameline = f'{frameinfo.filename} {frameinfo.lineno}'
190        except Exception:
191            logging.exception('Error calcing line for set_main_menu_window')
192
193        # With our legacy main-menu system, the caller is responsible for
194        # clearing out the old main menu window when assigning the new.
195        # However there are corner cases where that doesn't happen and we get
196        # old windows stuck under the new main one. So let's guard against
197        # that. However, we can't simply delete the existing main window when
198        # a new one is assigned because the user may transition the old out
199        # *after* the assignment. Sigh. So, as a happy medium, let's check in
200        # on the old after a short bit of time and kill it if its still alive.
201        # That will be a bit ugly on screen but at least should un-break
202        # things.
203        def _delay_kill() -> None:
204            import time
205
206            if existing:
207                print(
208                    f'Killing old main_menu_window'
209                    f' when called at: {frameline} t={time.time():.3f}'
210                )
211                existing.delete()
212
213        babase.apptimer(1.0, _delay_kill)
214        self._main_menu_window = window
215
216    def clear_main_menu_window(self, transition: str | None = None) -> None:
217        """Clear any existing 'main' window with the provided transition."""
218        assert transition is None or not transition.endswith('_in')
219        if self._main_menu_window:
220            if (
221                transition is not None
222                and not self._main_menu_window.transitioning_out
223            ):
224                _bauiv1.containerwidget(
225                    edit=self._main_menu_window, transition=transition
226                )
227            else:
228                self._main_menu_window.delete()
229            self._main_menu_window = None
230
231    def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None:
232        """(internal)"""
233
234        # If there's no main menu up, just call immediately.
235        if not self.has_main_menu_window():
236            with babase.ContextRef.empty():
237                call()
238        else:
239            self.main_menu_resume_callbacks.append(call)
240
241    def has_main_menu_window(self) -> bool:
242        """Return whether a main menu window is present."""
243        return bool(self._main_menu_window)
244
245    def set_main_menu_location(self, location: str) -> None:
246        """Set the location represented by the current main menu window."""
247        self._main_menu_location = location
248
249    def get_main_menu_location(self) -> str | None:
250        """Return the current named main menu location, if any."""
251        return self._main_menu_location

Consolidated UI functionality for the app.

Category: App Classes

To use this class, access the single instance of it at 'ba.app.ui'.

controller: bauiv1._uitypes.UIController | None
quit_window: Widget | None
main_menu_resume_callbacks: list
window_states: dict[type, typing.Any]
main_menu_selection: str | None
have_party_queue_window
cleanupchecks: list[bauiv1._uitypes.UICleanupCheck]
upkeeptimer: AppTimer | None
use_toolbars
title_color
heading_color
infotextcolor
selecting_private_party_playlist: bool
available: bool
75    @property
76    def available(self) -> bool:
77        """Can uiv1 currently be used?
78
79        Code that may run in headless mode, before the UI has been spun up,
80        while other ui systems are active, etc. can check this to avoid
81        likely erroring.
82        """
83        return _bauiv1.is_available()

Can uiv1 currently be used?

Code that may run in headless mode, before the UI has been spun up, while other ui systems are active, etc. can check this to avoid likely erroring.

uiscale: UIScale
85    @property
86    def uiscale(self) -> babase.UIScale:
87        """Current ui scale for the app."""
88        return self._uiscale

Current ui scale for the app.

@override
def on_app_loading(self) -> None:
 90    @override
 91    def on_app_loading(self) -> None:
 92        from bauiv1._uitypes import UIController, ui_upkeep
 93
 94        # IMPORTANT: If tweaking UI stuff, make sure it behaves for small,
 95        # medium, and large UI modes. (doesn't run off screen, etc).
 96        # The overrides below can be used to test with different sizes.
 97        # Generally small is used on phones, medium is used on tablets/tvs,
 98        # and large is on desktop computers or perhaps large tablets. When
 99        # possible, run in windowed mode and resize the window to assure
100        # this holds true at all aspect ratios.
101
102        # UPDATE: A better way to test this is now by setting the environment
103        # variable BA_UI_SCALE to "small", "medium", or "large".
104        # This will affect system UIs not covered by the values below such
105        # as screen-messages. The below values remain functional, however,
106        # for cases such as Android where environment variables can't be set
107        # easily.
108
109        if bool(False):  # force-test ui scale
110            self._uiscale = babase.UIScale.SMALL
111            with babase.ContextRef.empty():
112                babase.pushcall(
113                    lambda: babase.screenmessage(
114                        f'FORCING UISCALE {self._uiscale.name} FOR TESTING',
115                        color=(1, 0, 1),
116                        log=True,
117                    )
118                )
119
120        self.controller = UIController()
121
122        # Kick off our periodic UI upkeep.
123        # FIXME: Can probably kill this if we do immediate UI death checks.
124        self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True)

Called when the app reaches the loading state.

Note that subsystems created after the app switches to the loading state will not receive this callback. Subsystems created by plugins are an example of this.

def set_main_menu_window( self, window: Widget, from_window: Widget | None | bool = True) -> None:
126    def set_main_menu_window(
127        self,
128        window: bauiv1.Widget,
129        from_window: bauiv1.Widget | None | bool = True,
130    ) -> None:
131        """Set the current 'main' window, replacing any existing.
132
133        If 'from_window' is passed as a bauiv1.Widget or None, a warning
134        will be issued if it that value does not match the current main
135        window. This can help clean up flawed code that can lead to bad
136        UI states. A value of False will disable the check.
137        """
138
139        existing = self._main_menu_window
140
141        try:
142            if isinstance(from_window, bool):
143                # For default val True we warn that the arg wasn't
144                # passed. False can be explicitly passed to disable this
145                # check.
146                if from_window is True:
147                    caller_frame = inspect.stack()[1]
148                    caller_filename = caller_frame.filename
149                    caller_line_number = caller_frame.lineno
150                    logging.warning(
151                        'set_main_menu_window() should be passed a'
152                        " 'from_window' value to help ensure proper UI behavior"
153                        ' (%s line %i).',
154                        caller_filename,
155                        caller_line_number,
156                    )
157            else:
158                # For everything else, warn if what they passed wasn't
159                # the previous main menu widget.
160                if from_window is not existing:
161                    caller_frame = inspect.stack()[1]
162                    caller_filename = caller_frame.filename
163                    caller_line_number = caller_frame.lineno
164                    logging.warning(
165                        "set_main_menu_window() was passed 'from_window' %s"
166                        ' but existing main-menu-window is %s. (%s line %i).',
167                        from_window,
168                        existing,
169                        caller_filename,
170                        caller_line_number,
171                    )
172        except Exception:
173            # Prevent any bugs in these checks from causing problems.
174            logging.exception('Error checking from_window')
175
176        # Once the above code leads to us fixing all leftover window bugs
177        # at the source, we can kill the code below.
178
179        # Let's grab the location where we were called from to report
180        # if we have to force-kill the existing window (which normally
181        # should not happen).
182        frameline = None
183        try:
184            frame = inspect.currentframe()
185            if frame is not None:
186                frame = frame.f_back
187            if frame is not None:
188                frameinfo = inspect.getframeinfo(frame)
189                frameline = f'{frameinfo.filename} {frameinfo.lineno}'
190        except Exception:
191            logging.exception('Error calcing line for set_main_menu_window')
192
193        # With our legacy main-menu system, the caller is responsible for
194        # clearing out the old main menu window when assigning the new.
195        # However there are corner cases where that doesn't happen and we get
196        # old windows stuck under the new main one. So let's guard against
197        # that. However, we can't simply delete the existing main window when
198        # a new one is assigned because the user may transition the old out
199        # *after* the assignment. Sigh. So, as a happy medium, let's check in
200        # on the old after a short bit of time and kill it if its still alive.
201        # That will be a bit ugly on screen but at least should un-break
202        # things.
203        def _delay_kill() -> None:
204            import time
205
206            if existing:
207                print(
208                    f'Killing old main_menu_window'
209                    f' when called at: {frameline} t={time.time():.3f}'
210                )
211                existing.delete()
212
213        babase.apptimer(1.0, _delay_kill)
214        self._main_menu_window = window

Set the current 'main' window, replacing any existing.

If 'from_window' is passed as a bauiv1.Widget or None, a warning will be issued if it that value does not match the current main window. This can help clean up flawed code that can lead to bad UI states. A value of False will disable the check.

def clear_main_menu_window(self, transition: str | None = None) -> None:
216    def clear_main_menu_window(self, transition: str | None = None) -> None:
217        """Clear any existing 'main' window with the provided transition."""
218        assert transition is None or not transition.endswith('_in')
219        if self._main_menu_window:
220            if (
221                transition is not None
222                and not self._main_menu_window.transitioning_out
223            ):
224                _bauiv1.containerwidget(
225                    edit=self._main_menu_window, transition=transition
226                )
227            else:
228                self._main_menu_window.delete()
229            self._main_menu_window = None

Clear any existing 'main' window with the provided transition.

def has_main_menu_window(self) -> bool:
241    def has_main_menu_window(self) -> bool:
242        """Return whether a main menu window is present."""
243        return bool(self._main_menu_window)

Return whether a main menu window is present.

def set_main_menu_location(self, location: str) -> None:
245    def set_main_menu_location(self, location: str) -> None:
246        """Set the location represented by the current main menu window."""
247        self._main_menu_location = location

Set the location represented by the current main menu window.

def get_main_menu_location(self) -> str | None:
249    def get_main_menu_location(self) -> str | None:
250        """Return the current named main menu location, if any."""
251        return self._main_menu_location

Return the current named main menu location, if any.

Inherited Members
babase._appsubsystem.AppSubsystem
on_app_running
on_app_suspend
on_app_unsuspend
on_app_shutdown
on_app_shutdown_complete
do_apply_app_config
WeakCall = <class 'babase._general._WeakCall'>
def widget( edit: Widget | None = None, up_widget: Widget | None = None, down_widget: Widget | None = None, left_widget: Widget | None = None, right_widget: 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, autoselect: bool | None = None) -> None:
575def widget(
576    edit: bauiv1.Widget | None = None,
577    up_widget: bauiv1.Widget | None = None,
578    down_widget: bauiv1.Widget | None = None,
579    left_widget: bauiv1.Widget | None = None,
580    right_widget: bauiv1.Widget | None = None,
581    show_buffer_top: float | None = None,
582    show_buffer_bottom: float | None = None,
583    show_buffer_left: float | None = None,
584    show_buffer_right: float | None = None,
585    autoselect: bool | None = None,
586) -> None:
587    """Edit common attributes of any widget.
588
589    Category: **User Interface Functions**
590
591    Unlike other UI calls, this can only be used to edit, not to create.
592    """
593    return None

Edit common attributes of any widget.

Category: User Interface Functions

Unlike other UI calls, this can only be used to edit, not to create.

class Widget:
 75class Widget:
 76    """Internal type for low level UI elements; buttons, windows, etc.
 77
 78    Category: **User Interface Classes**
 79
 80    This class represents a weak reference to a widget object
 81    in the internal C++ layer. Currently, functions such as
 82    bauiv1.buttonwidget() must be used to instantiate or edit these.
 83    """
 84
 85    transitioning_out: bool
 86    """Whether this widget is in the process of dying (read only).
 87
 88       It can be useful to check this on a window's root widget to
 89       prevent multiple window actions from firing simultaneously,
 90       potentially leaving the UI in a broken state."""
 91
 92    def __bool__(self) -> bool:
 93        """Support for bool evaluation."""
 94        return bool(True)  # Slight obfuscation.
 95
 96    def activate(self) -> None:
 97        """Activates a widget; the same as if it had been clicked."""
 98        return None
 99
100    def add_delete_callback(self, call: Callable) -> None:
101        """Add a call to be run immediately after this widget is destroyed."""
102        return None
103
104    def delete(self, ignore_missing: bool = True) -> None:
105        """Delete the Widget. Ignores already-deleted Widgets if ignore_missing
106        is True; otherwise an Exception is thrown.
107        """
108        return None
109
110    def exists(self) -> bool:
111        """Returns whether the Widget still exists.
112        Most functionality will fail on a nonexistent widget.
113
114        Note that you can also use the boolean operator for this same
115        functionality, so a statement such as "if mywidget" will do
116        the right thing both for Widget objects and values of None.
117        """
118        return bool()
119
120    def get_children(self) -> list[bauiv1.Widget]:
121        """Returns any child Widgets of this Widget."""
122        import bauiv1
123
124        return [bauiv1.Widget()]
125
126    def get_screen_space_center(self) -> tuple[float, float]:
127        """Returns the coords of the bauiv1.Widget center relative to the center
128        of the screen. This can be useful for placing pop-up windows and other
129        special cases.
130        """
131        return (0.0, 0.0)
132
133    def get_selected_child(self) -> bauiv1.Widget | None:
134        """Returns the selected child Widget or None if nothing is selected."""
135        import bauiv1
136
137        return bauiv1.Widget()
138
139    def get_widget_type(self) -> str:
140        """Return the internal type of the Widget as a string. Note that this
141        is different from the Python bauiv1.Widget type, which is the same for
142        all widgets.
143        """
144        return str()

Internal type for low level UI elements; buttons, windows, etc.

Category: User Interface Classes

This class represents a weak reference to a widget object in the internal C++ layer. Currently, functions such as bauiv1.buttonwidget() must be used to instantiate or edit these.

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[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) -> 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:
27class Window:
28    """A basic window.
29
30    Category: User Interface Classes
31    """
32
33    def __init__(self, root_widget: bauiv1.Widget, cleanupcheck: bool = True):
34        self._root_widget = root_widget
35
36        # Complain if we outlive our root widget.
37        if cleanupcheck:
38            uicleanupcheck(self, root_widget)
39
40    def get_root_widget(self) -> bauiv1.Widget:
41        """Return the root widget."""
42        return self._root_widget

A basic window.

Category: User Interface Classes

Window(root_widget: Widget, cleanupcheck: bool = True)
33    def __init__(self, root_widget: bauiv1.Widget, cleanupcheck: bool = True):
34        self._root_widget = root_widget
35
36        # Complain if we outlive our root widget.
37        if cleanupcheck:
38            uicleanupcheck(self, root_widget)
def get_root_widget(self) -> Widget:
40    def get_root_widget(self) -> bauiv1.Widget:
41        """Return the root widget."""
42        return self._root_widget

Return the root widget.