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:
549def apptime() -> babase.AppTime:
550    """Return the current app-time in seconds.
551
552    Category: **General Utility Functions**
553
554    App-time is a monotonic time value; it starts at 0.0 when the app
555    launches and will never jump by large amounts or go backwards, even if
556    the system time changes. Its progression will pause when the app is in
557    a suspended state.
558
559    Note that the AppTime returned here is simply float; it just has a
560    unique type in the type-checker's eyes to help prevent it from being
561    accidentally used with time functionality expecting other time types.
562    """
563    import babase  # pylint: disable=cyclic-import
564
565    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:
568def apptimer(time: float, call: Callable[[], Any]) -> None:
569    """Schedule a callable object to run based on app-time.
570
571    Category: **General Utility Functions**
572
573    This function creates a one-off timer which cannot be canceled or
574    modified once created. If you require the ability to do so, or need
575    a repeating timer, use the babase.AppTimer class instead.
576
577    ##### Arguments
578    ###### time (float)
579    > Length of time in seconds that the timer will wait before firing.
580
581    ###### call (Callable[[], Any])
582    > A callable Python object. Note that the timer will retain a
583    strong reference to the callable for as long as the timer exists, so you
584    may want to look into concepts such as babase.WeakCall if that is not
585    desired.
586
587    ##### Examples
588    Print some stuff through time:
589    >>> babase.screenmessage('hello from now!')
590    >>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
591                              'hello from the future!'))
592    >>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
593    ...                       'hello from the future 2!'))
594    """
595    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:
55class AppTimer:
56    """Timers are used to run code at later points in time.
57
58    Category: **General Utility Classes**
59
60    This class encapsulates a timer based on app-time.
61    The underlying timer will be destroyed when this object is no longer
62    referenced. If you do not want to worry about keeping a reference to
63    your timer around, use the babase.apptimer() function instead to get a
64    one-off timer.
65
66    ##### Arguments
67    ###### time
68    > Length of time in seconds that the timer will wait before firing.
69
70    ###### call
71    > A callable Python object. Remember that the timer will retain a
72    strong reference to the callable for as long as it exists, so you
73    may want to look into concepts such as babase.WeakCall if that is not
74    desired.
75
76    ###### repeat
77    > If True, the timer will fire repeatedly, with each successive
78    firing having the same delay as the first.
79
80    ##### Example
81
82    Use a Timer object to print repeatedly for a few seconds:
83    ... def say_it():
84    ...     babase.screenmessage('BADGER!')
85    ... def stop_saying_it():
86    ...     global g_timer
87    ...     g_timer = None
88    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
89    ... # Create our timer; it will run as long as we have the self.t ref.
90    ... g_timer = babase.AppTimer(0.3, say_it, repeat=True)
91    ... # Now fire off a one-shot timer to kill it.
92    ... babase.apptimer(3.89, stop_saying_it)
93    """
94
95    def __init__(
96        self, time: float, call: Callable[[], Any], repeat: bool = False
97    ) -> None:
98        pass

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

Category: General Utility Classes

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

Arguments
time

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

call

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

repeat

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

Example

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

AppTimer(time: float, call: Callable[[], Any], repeat: bool = False)
95    def __init__(
96        self, time: float, call: Callable[[], Any], repeat: bool = False
97    ) -> None:
98        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:
154def buttonwidget(
155    edit: bauiv1.Widget | None = None,
156    parent: bauiv1.Widget | None = None,
157    size: Sequence[float] | None = None,
158    position: Sequence[float] | None = None,
159    on_activate_call: Callable | None = None,
160    label: str | bauiv1.Lstr | None = None,
161    color: Sequence[float] | None = None,
162    down_widget: bauiv1.Widget | None = None,
163    up_widget: bauiv1.Widget | None = None,
164    left_widget: bauiv1.Widget | None = None,
165    right_widget: bauiv1.Widget | None = None,
166    texture: bauiv1.Texture | None = None,
167    text_scale: float | None = None,
168    textcolor: Sequence[float] | None = None,
169    enable_sound: bool | None = None,
170    mesh_transparent: bauiv1.Mesh | None = None,
171    mesh_opaque: bauiv1.Mesh | None = None,
172    repeat: bool | None = None,
173    scale: float | None = None,
174    transition_delay: float | None = None,
175    on_select_call: Callable | None = None,
176    button_type: str | None = None,
177    extra_touch_border_scale: float | None = None,
178    selectable: bool | None = None,
179    show_buffer_top: float | None = None,
180    icon: bauiv1.Texture | None = None,
181    iconscale: float | None = None,
182    icon_tint: float | None = None,
183    icon_color: Sequence[float] | None = None,
184    autoselect: bool | None = None,
185    mask_texture: bauiv1.Texture | None = None,
186    tint_texture: bauiv1.Texture | None = None,
187    tint_color: Sequence[float] | None = None,
188    tint2_color: Sequence[float] | None = None,
189    text_flatness: float | None = None,
190    text_res_scale: float | None = None,
191    enabled: bool | None = None,
192) -> bauiv1.Widget:
193    """Create or edit a button widget.
194
195    Category: **User Interface Functions**
196
197    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
198    a new one is created and returned. Arguments that are not set to None
199    are applied to the Widget.
200    """
201    import bauiv1  # pylint: disable=cyclic-import
202
203    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:
618def charstr(char_id: babase.SpecialChar) -> str:
619    """Get a unicode string representing a special character.
620
621    Category: **General Utility Functions**
622
623    Note that these utilize the private-use block of unicode characters
624    (U+E000-U+F8FF) and are specific to the game; exporting or rendering
625    them elsewhere will be meaningless.
626
627    See babase.SpecialChar for the list of available characters.
628    """
629    return str()

Get a unicode string representing a special character.

Category: General Utility Functions

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

See babase.SpecialChar for the list of available characters.

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:
206def checkboxwidget(
207    edit: bauiv1.Widget | None = None,
208    parent: bauiv1.Widget | None = None,
209    size: Sequence[float] | None = None,
210    position: Sequence[float] | None = None,
211    text: str | bauiv1.Lstr | None = None,
212    value: bool | None = None,
213    on_value_change_call: Callable[[bool], None] | None = None,
214    on_select_call: Callable[[], None] | None = None,
215    text_scale: float | None = None,
216    textcolor: Sequence[float] | None = None,
217    scale: float | None = None,
218    is_radio_button: bool | None = None,
219    maxwidth: float | None = None,
220    autoselect: bool | None = None,
221    color: Sequence[float] | None = None,
222) -> bauiv1.Widget:
223    """Create or edit a check-box widget.
224
225    Category: **User Interface Functions**
226
227    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
228    a new one is created and returned. Arguments that are not set to None
229    are applied to the Widget.
230    """
231    import bauiv1  # pylint: disable=cyclic-import
232
233    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:
654def clipboard_is_supported() -> bool:
655    """Return whether this platform supports clipboard operations at all.
656
657    Category: **General Utility Functions**
658
659    If this returns False, UIs should not show 'copy to clipboard'
660    buttons, etc.
661    """
662    return bool()

Return whether this platform supports clipboard operations at all.

Category: General Utility Functions

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

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

Copy a string to the system clipboard.

Category: General Utility Functions

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

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:
236def columnwidget(
237    edit: bauiv1.Widget | None = None,
238    parent: bauiv1.Widget | None = None,
239    size: Sequence[float] | None = None,
240    position: Sequence[float] | None = None,
241    background: bool | None = None,
242    selected_child: bauiv1.Widget | None = None,
243    visible_child: bauiv1.Widget | None = None,
244    single_depth: bool | None = None,
245    print_list_exit_instructions: bool | None = None,
246    left_border: float | None = None,
247    top_border: float | None = None,
248    bottom_border: float | None = None,
249    selection_loops_to_parent: bool | None = None,
250    border: float | None = None,
251    margin: float | None = None,
252    claims_left_right: bool | None = None,
253    claims_tab: bool | None = None,
254) -> bauiv1.Widget:
255    """Create or edit a column widget.
256
257    Category: **User Interface Functions**
258
259    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
260    a new one is created and returned. Arguments that are not set to None
261    are applied to the Widget.
262    """
263    import bauiv1  # pylint: disable=cyclic-import
264
265    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:
268def containerwidget(
269    edit: bauiv1.Widget | None = None,
270    parent: bauiv1.Widget | None = None,
271    size: Sequence[float] | None = None,
272    position: Sequence[float] | None = None,
273    background: bool | None = None,
274    selected_child: bauiv1.Widget | None = None,
275    transition: str | None = None,
276    cancel_button: bauiv1.Widget | None = None,
277    start_button: bauiv1.Widget | None = None,
278    root_selectable: bool | None = None,
279    on_activate_call: Callable[[], None] | None = None,
280    claims_left_right: bool | None = None,
281    claims_tab: bool | None = None,
282    selection_loops: bool | None = None,
283    selection_loops_to_parent: bool | None = None,
284    scale: float | None = None,
285    on_outside_click_call: Callable[[], None] | None = None,
286    single_depth: bool | None = None,
287    visible_child: bauiv1.Widget | None = None,
288    stack_offset: Sequence[float] | None = None,
289    color: Sequence[float] | None = None,
290    on_cancel_call: Callable[[], None] | None = None,
291    print_list_exit_instructions: bool | None = None,
292    click_activate: bool | None = None,
293    always_highlight: bool | None = None,
294    selectable: bool | None = None,
295    scale_origin_stack_offset: Sequence[float] | None = None,
296    toolbar_visibility: str | None = None,
297    on_select_call: Callable[[], None] | None = None,
298    claim_outside_clicks: bool | None = None,
299    claims_up_down: bool | None = None,
300) -> bauiv1.Widget:
301    """Create or edit a container widget.
302
303    Category: **User Interface Functions**
304
305    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
306    a new one is created and returned. Arguments that are not set to None
307    are applied to the Widget.
308    """
309    import bauiv1  # pylint: disable=cyclic-import
310
311    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:
150class ContextRef:
151    """Store or use a ballistica context.
152
153    Category: **General Utility Classes**
154
155    Many operations such as bascenev1.newnode() or bascenev1.gettexture()
156    operate implicitly on a current 'context'. A context is some sort of
157    state that functionality can implicitly use. Context determines, for
158    example, which scene nodes or textures get added to without having to
159    specify it explicitly in the newnode()/gettexture() call. Contexts can
160    also affect object lifecycles; for example a babase.ContextCall will
161    become a no-op when the context it was created in is destroyed.
162
163    In general, if you are a modder, you should not need to worry about
164    contexts; mod code should mostly be getting run in the correct
165    context and timers and other callbacks will take care of saving
166    and restoring contexts automatically. There may be rare cases,
167    however, where you need to deal directly with contexts, and that is
168    where this class comes in.
169
170    Creating a babase.ContextRef() will capture a reference to the current
171    context. Other modules may provide ways to access their contexts; for
172    example a bascenev1.Activity instance has a 'context' attribute. You
173    can also use babase.ContextRef.empty() to create a reference to *no*
174    context. Some code such as UI calls may expect this and may complain
175    if you try to use them within a context.
176
177    ##### Usage
178    ContextRefs are generally used with the Python 'with' statement, which
179    sets the context they point to as current on entry and resets it to
180    the previous value on exit.
181
182    ##### Example
183    Explicitly create a few UI bits with no context set.
184    (UI stuff may complain if called within a context):
185    >>> with bui.ContextRef.empty():
186    ...     my_container = bui.containerwidget()
187    """
188
189    def __init__(
190        self,
191    ) -> None:
192        pass
193
194    def __enter__(self) -> None:
195        """Support for "with" statement."""
196        pass
197
198    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
199        """Support for "with" statement."""
200        pass
201
202    @classmethod
203    def empty(cls) -> ContextRef:
204        """Return a ContextRef pointing to no context.
205
206        This is useful when code should be run free of a context.
207        For example, UI code generally insists on being run this way.
208        Otherwise, callbacks set on the UI could inadvertently stop working
209        due to a game activity ending, which would be unintuitive behavior.
210        """
211        return ContextRef()
212
213    def is_empty(self) -> bool:
214        """Whether the context was created as empty."""
215        return bool()
216
217    def is_expired(self) -> bool:
218        """Whether the context has expired."""
219        return bool()

Store or use a ballistica context.

Category: General Utility Classes

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

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

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

Usage

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

Example

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

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

Return a ContextRef pointing to no context.

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

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

Whether the context was created as empty.

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

Whether the context has expired.

def displaytime() -> DisplayTime:
758def displaytime() -> babase.DisplayTime:
759    """Return the current display-time in seconds.
760
761    Category: **General Utility Functions**
762
763    Display-time is a time value intended to be used for animation and other
764    visual purposes. It will generally increment by a consistent amount each
765    frame. It will pass at an overall similar rate to AppTime, but trades
766    accuracy for smoothness.
767
768    Note that the value returned here is simply a float; it just has a
769    unique type in the type-checker's eyes to help prevent it from being
770    accidentally used with time functionality expecting other time types.
771    """
772    import babase  # pylint: disable=cyclic-import
773
774    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:
777def displaytimer(time: float, call: Callable[[], Any]) -> None:
778    """Schedule a callable object to run based on display-time.
779
780    Category: **General Utility Functions**
781
782    This function creates a one-off timer which cannot be canceled or
783    modified once created. If you require the ability to do so, or need
784    a repeating timer, use the babase.DisplayTimer class instead.
785
786    Display-time is a time value intended to be used for animation and other
787    visual purposes. It will generally increment by a consistent amount each
788    frame. It will pass at an overall similar rate to AppTime, but trades
789    accuracy for smoothness.
790
791    ##### Arguments
792    ###### time (float)
793    > Length of time in seconds that the timer will wait before firing.
794
795    ###### call (Callable[[], Any])
796    > A callable Python object. Note that the timer will retain a
797    strong reference to the callable for as long as the timer exists, so you
798    may want to look into concepts such as babase.WeakCall if that is not
799    desired.
800
801    ##### Examples
802    Print some stuff through time:
803    >>> babase.screenmessage('hello from now!')
804    >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
805    ...                       'hello from the future!'))
806    >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
807    ...                       'hello from the future 2!'))
808    """
809    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:
222class DisplayTimer:
223    """Timers are used to run code at later points in time.
224
225    Category: **General Utility Classes**
226
227    This class encapsulates a timer based on display-time.
228    The underlying timer will be destroyed when this object is no longer
229    referenced. If you do not want to worry about keeping a reference to
230    your timer around, use the babase.displaytimer() function instead to get a
231    one-off timer.
232
233    Display-time is a time value intended to be used for animation and
234    other visual purposes. It will generally increment by a consistent
235    amount each frame. It will pass at an overall similar rate to AppTime,
236    but trades accuracy for smoothness.
237
238    ##### Arguments
239    ###### time
240    > Length of time in seconds that the timer will wait before firing.
241
242    ###### call
243    > A callable Python object. Remember that the timer will retain a
244    strong reference to the callable for as long as it exists, so you
245    may want to look into concepts such as babase.WeakCall if that is not
246    desired.
247
248    ###### repeat
249    > If True, the timer will fire repeatedly, with each successive
250    firing having the same delay as the first.
251
252    ##### Example
253
254    Use a Timer object to print repeatedly for a few seconds:
255    ... def say_it():
256    ...     babase.screenmessage('BADGER!')
257    ... def stop_saying_it():
258    ...     global g_timer
259    ...     g_timer = None
260    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
261    ... # Create our timer; it will run as long as we have the self.t ref.
262    ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True)
263    ... # Now fire off a one-shot timer to kill it.
264    ... babase.displaytimer(3.89, stop_saying_it)
265    """
266
267    def __init__(
268        self, time: float, call: Callable[[], Any], repeat: bool = False
269    ) -> None:
270        pass

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

Category: General Utility Classes

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

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

Arguments
time

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

call

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

repeat

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

Example

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

DisplayTimer(time: float, call: Callable[[], Any], repeat: bool = False)
267    def __init__(
268        self, time: float, call: Callable[[], Any], repeat: bool = False
269    ) -> None:
270        pass
def do_once() -> bool:
817def do_once() -> bool:
818    """Return whether this is the first time running a line of code.
819
820    Category: **General Utility Functions**
821
822    This is used by 'print_once()' type calls to keep from overflowing
823    logs. The call functions by registering the filename and line where
824    The call is made from.  Returns True if this location has not been
825    registered already, and False if it has.
826
827    ##### Example
828    This print will only fire for the first loop iteration:
829    >>> for i in range(10):
830    ... if babase.do_once():
831    ...     print('HelloWorld once from loop!')
832    """
833    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:
982def get_input_idle_time() -> float:
983    """Return seconds since any local input occurred (touch, keypress, etc.)."""
984    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:
314def get_qrcode_texture(url: str) -> bauiv1.Texture:
315    """Return a QR code texture.
316
317    The provided url must be 64 bytes or less.
318    """
319    import bauiv1  # pylint: disable=cyclic-import
320
321    return bauiv1.Texture()

Return a QR code texture.

The provided url must be 64 bytes or less.

def get_type_name(cls: type) -> str:
108def get_type_name(cls: type) -> str:
109    """Return a full type name including module for a class."""
110    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]:
71def getclass(name: str, subclassof: type[T]) -> type[T]:
72    """Given a full class name such as foo.bar.MyClass, return the class.
73
74    Category: **General Utility Functions**
75
76    The class will be checked to make sure it is a subclass of the provided
77    'subclassof' class, and a TypeError will be raised if not.
78    """
79    import importlib
80
81    splits = name.split('.')
82    modulename = '.'.join(splits[:-1])
83    classname = splits[-1]
84    module = importlib.import_module(modulename)
85    cls: type = getattr(module, classname)
86
87    if not issubclass(cls, subclassof):
88        raise TypeError(f'{name} is not a subclass of {subclassof}.')
89    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:
331def getmesh(name: str) -> bauiv1.Mesh:
332    """Load a mesh for use solely in the local user interface."""
333    import bauiv1  # pylint: disable=cyclic-import
334
335    return bauiv1.Mesh()

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

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

Load a sound for use in the ui.

def gettexture(name: str) -> Texture:
345def gettexture(name: str) -> bauiv1.Texture:
346    """Load a texture for use in the ui."""
347    import bauiv1  # pylint: disable=cyclic-import
348
349    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:
352def hscrollwidget(
353    edit: bauiv1.Widget | None = None,
354    parent: bauiv1.Widget | None = None,
355    size: Sequence[float] | None = None,
356    position: Sequence[float] | None = None,
357    background: bool | None = None,
358    selected_child: bauiv1.Widget | None = None,
359    capture_arrows: bool | None = None,
360    on_select_call: Callable[[], None] | None = None,
361    center_small_content: bool | None = None,
362    color: Sequence[float] | None = None,
363    highlight: bool | None = None,
364    border_opacity: float | None = None,
365    simple_culling_h: float | None = None,
366    claims_left_right: bool | None = None,
367    claims_up_down: bool | None = None,
368    claims_tab: bool | None = None,
369) -> bauiv1.Widget:
370    """Create or edit a horizontal scroll widget.
371
372    Category: **User Interface Functions**
373
374    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
375    a new one is created and returned. Arguments that are not set to None
376    are applied to the Widget.
377    """
378    import bauiv1  # pylint: disable=cyclic-import
379
380    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:
383def imagewidget(
384    edit: bauiv1.Widget | None = None,
385    parent: bauiv1.Widget | None = None,
386    size: Sequence[float] | None = None,
387    position: Sequence[float] | None = None,
388    color: Sequence[float] | None = None,
389    texture: bauiv1.Texture | None = None,
390    opacity: float | None = None,
391    mesh_transparent: bauiv1.Mesh | None = None,
392    mesh_opaque: bauiv1.Mesh | None = None,
393    has_alpha_channel: bool = True,
394    tint_texture: bauiv1.Texture | None = None,
395    tint_color: Sequence[float] | None = None,
396    transition_delay: float | None = None,
397    draw_controller: bauiv1.Widget | None = None,
398    tint2_color: Sequence[float] | None = None,
399    tilt_scale: float | None = None,
400    mask_texture: bauiv1.Texture | None = None,
401    radial_amount: float | None = None,
402) -> bauiv1.Widget:
403    """Create or edit an image widget.
404
405    Category: **User Interface Functions**
406
407    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
408    a new one is created and returned. Arguments that are not set to None
409    are applied to the Widget.
410    """
411    import bauiv1  # pylint: disable=cyclic-import
412
413    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:
28def is_browser_likely_available() -> bool:
29    """Return whether a browser likely exists on the current device.
30
31    category: General Utility Functions
32
33    If this returns False you may want to avoid calling babase.show_url()
34    with any lengthy addresses. (ba.show_url() will display an address
35    as a string in a window if unable to bring up a browser, but that
36    is only useful for simple URLs.)
37    """
38    app = _babase.app
39
40    if app.classic is None:
41        logging.warning(
42            'is_browser_likely_available() needs to be updated'
43            ' to work without classic.'
44        )
45        return True
46
47    platform = app.classic.platform
48    hastouchscreen = _babase.hastouchscreen()
49
50    # If we're on a vr device or an android device with no touchscreen,
51    # assume no browser.
52    # FIXME: Might not be the case anymore; should make this definable
53    #  at the platform level.
54    if app.env.vr or (platform == 'android' and not hastouchscreen):
55        return False
56
57    # Anywhere else assume we've got one.
58    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:
 32class LoginAdapter:
 33    """Allows using implicit login types in an explicit way.
 34
 35    Some login types such as Google Play Game Services or Game Center are
 36    basically always present and often do not provide a way to log out
 37    from within a running app, so this adapter exists to use them in a
 38    flexible manner by 'attaching' and 'detaching' from an always-present
 39    login, allowing for its use alongside other login types. It also
 40    provides common functionality for server-side account verification and
 41    other handy bits.
 42    """
 43
 44    @dataclass
 45    class SignInResult:
 46        """Describes the final result of a sign-in attempt."""
 47
 48        credentials: str
 49
 50    @dataclass
 51    class ImplicitLoginState:
 52        """Describes the current state of an implicit login."""
 53
 54        login_id: str
 55        display_name: str
 56
 57    def __init__(self, login_type: LoginType):
 58        assert _babase.in_logic_thread()
 59        self.login_type = login_type
 60        self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = (
 61            None
 62        )
 63        self._on_app_loading_called = False
 64        self._implicit_login_state_dirty = False
 65        self._back_end_active = False
 66
 67        # Which login of our type (if any) is associated with the
 68        # current active primary account.
 69        self._active_login_id: str | None = None
 70
 71        self._last_sign_in_time: float | None = None
 72        self._last_sign_in_desc: str | None = None
 73
 74    def on_app_loading(self) -> None:
 75        """Should be called for each adapter in on_app_loading."""
 76
 77        assert not self._on_app_loading_called
 78        self._on_app_loading_called = True
 79
 80        # Any implicit state we received up until now needs to be pushed
 81        # to the app account subsystem.
 82        self._update_implicit_login_state()
 83
 84    def set_implicit_login_state(
 85        self, state: ImplicitLoginState | None
 86    ) -> None:
 87        """Keep the adapter informed of implicit login states.
 88
 89        This should be called by the adapter back-end when an account
 90        of their associated type gets logged in or out.
 91        """
 92        assert _babase.in_logic_thread()
 93
 94        # Ignore redundant sets.
 95        if state == self._implicit_login_state:
 96            return
 97
 98        if DEBUG_LOG:
 99            if state is None:
100                logging.debug(
101                    'LoginAdapter: %s implicit state changed;'
102                    ' now signed out.',
103                    self.login_type.name,
104                )
105            else:
106                logging.debug(
107                    'LoginAdapter: %s implicit state changed;'
108                    ' now signed in as %s.',
109                    self.login_type.name,
110                    state.display_name,
111                )
112
113        self._implicit_login_state = state
114        self._implicit_login_state_dirty = True
115
116        # (possibly) push it to the app for handling.
117        self._update_implicit_login_state()
118
119        # This might affect whether we consider that back-end as 'active'.
120        self._update_back_end_active()
121
122    def set_active_logins(self, logins: dict[LoginType, str]) -> None:
123        """Keep the adapter informed of actively used logins.
124
125        This should be called by the app's account subsystem to
126        keep adapters up to date on the full set of logins attached
127        to the currently-in-use account.
128        Note that the logins dict passed in should be immutable as
129        only a reference to it is stored, not a copy.
130        """
131        assert _babase.in_logic_thread()
132        if DEBUG_LOG:
133            logging.debug(
134                'LoginAdapter: %s adapter got active logins %s.',
135                self.login_type.name,
136                {k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
137            )
138
139        self._active_login_id = logins.get(self.login_type)
140        self._update_back_end_active()
141
142    def on_back_end_active_change(self, active: bool) -> None:
143        """Called when active state for the back-end is (possibly) changing.
144
145        Meant to be overridden by subclasses.
146        Being active means that the implicit login provided by the back-end
147        is actually being used by the app. It should therefore register
148        unlocked achievements, leaderboard scores, allow viewing native
149        UIs, etc. When not active it should ignore everything and behave
150        as if signed out, even if it technically is still signed in.
151        """
152        assert _babase.in_logic_thread()
153        del active  # Unused.
154
155    @final
156    def sign_in(
157        self,
158        result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
159        description: str,
160    ) -> None:
161        """Attempt to sign in via this adapter.
162
163        This can be called even if the back-end is not implicitly signed in;
164        the adapter will attempt to sign in if possible. An exception will
165        be returned if the sign-in attempt fails.
166        """
167        assert _babase.in_logic_thread()
168        from babase._general import Call
169
170        # Have been seeing multiple sign-in attempts come through
171        # nearly simultaneously which can be problematic server-side.
172        # Let's error if a sign-in attempt is made within a few seconds
173        # of the last one to try and address this.
174        now = time.monotonic()
175        appnow = _babase.apptime()
176        if self._last_sign_in_time is not None:
177            since_last = now - self._last_sign_in_time
178            if since_last < 1.0:
179                logging.warning(
180                    'LoginAdapter: %s adapter sign_in() called too soon'
181                    ' (%.2fs) after last; this-desc="%s", last-desc="%s",'
182                    ' ba-app-time=%.2f.',
183                    self.login_type.name,
184                    since_last,
185                    description,
186                    self._last_sign_in_desc,
187                    appnow,
188                )
189                _babase.pushcall(
190                    Call(
191                        result_cb,
192                        self,
193                        RuntimeError('sign_in called too soon after last.'),
194                    )
195                )
196                return
197
198        self._last_sign_in_desc = description
199        self._last_sign_in_time = now
200
201        if DEBUG_LOG:
202            logging.debug(
203                'LoginAdapter: %s adapter sign_in() called;'
204                ' fetching sign-in-token...',
205                self.login_type.name,
206            )
207
208        def _got_sign_in_token_result(result: str | None) -> None:
209            import bacommon.cloud
210
211            # Failed to get a sign-in-token.
212            if result is None:
213                if DEBUG_LOG:
214                    logging.debug(
215                        'LoginAdapter: %s adapter sign-in-token fetch failed;'
216                        ' aborting sign-in.',
217                        self.login_type.name,
218                    )
219                _babase.pushcall(
220                    Call(
221                        result_cb,
222                        self,
223                        RuntimeError('fetch-sign-in-token failed.'),
224                    )
225                )
226                return
227
228            # Got a sign-in token! Now pass it to the cloud which will use
229            # it to verify our identity and give us app credentials on
230            # success.
231            if DEBUG_LOG:
232                logging.debug(
233                    'LoginAdapter: %s adapter sign-in-token fetch succeeded;'
234                    ' passing to cloud for verification...',
235                    self.login_type.name,
236                )
237
238            def _got_sign_in_response(
239                response: bacommon.cloud.SignInResponse | Exception,
240            ) -> None:
241                # This likely means we couldn't communicate with the server.
242                if isinstance(response, Exception):
243                    if DEBUG_LOG:
244                        logging.debug(
245                            'LoginAdapter: %s adapter got error'
246                            ' sign-in response: %s',
247                            self.login_type.name,
248                            response,
249                        )
250                    _babase.pushcall(Call(result_cb, self, response))
251                else:
252                    # This means our credentials were explicitly rejected.
253                    if response.credentials is None:
254                        result2: LoginAdapter.SignInResult | Exception = (
255                            RuntimeError('Sign-in-token was rejected.')
256                        )
257                    else:
258                        if DEBUG_LOG:
259                            logging.debug(
260                                'LoginAdapter: %s adapter got successful'
261                                ' sign-in response',
262                                self.login_type.name,
263                            )
264                        result2 = self.SignInResult(
265                            credentials=response.credentials
266                        )
267                    _babase.pushcall(Call(result_cb, self, result2))
268
269            assert _babase.app.plus is not None
270            _babase.app.plus.cloud.send_message_cb(
271                bacommon.cloud.SignInMessage(
272                    self.login_type,
273                    result,
274                    description=description,
275                    apptime=appnow,
276                ),
277                on_response=_got_sign_in_response,
278            )
279
280        # Kick off the sign-in process by fetching a sign-in token.
281        self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
282
283    def is_back_end_active(self) -> bool:
284        """Is this adapter's back-end currently active?"""
285        return self._back_end_active
286
287    def get_sign_in_token(
288        self, completion_cb: Callable[[str | None], None]
289    ) -> None:
290        """Get a sign-in token from the adapter back end.
291
292        This token is then passed to the master-server to complete the
293        sign-in process. The adapter can use this opportunity to bring
294        up account creation UI, call its internal sign_in function, etc.
295        as needed. The provided completion_cb should then be called with
296        either a token or None if sign in failed or was cancelled.
297        """
298        from babase._general import Call
299
300        # Default implementation simply fails immediately.
301        _babase.pushcall(Call(completion_cb, None))
302
303    def _update_implicit_login_state(self) -> None:
304        # If we've received an implicit login state, schedule it to be
305        # sent along to the app. We wait until on-app-loading has been
306        # called so that account-client-v2 has had a chance to load
307        # any existing state so it can properly respond to this.
308        if self._implicit_login_state_dirty and self._on_app_loading_called:
309            from babase._general import Call
310
311            if DEBUG_LOG:
312                logging.debug(
313                    'LoginAdapter: %s adapter sending'
314                    ' implicit-state-changed to app.',
315                    self.login_type.name,
316                )
317
318            assert _babase.app.plus is not None
319            _babase.pushcall(
320                Call(
321                    _babase.app.plus.accounts.on_implicit_login_state_changed,
322                    self.login_type,
323                    self._implicit_login_state,
324                )
325            )
326            self._implicit_login_state_dirty = False
327
328    def _update_back_end_active(self) -> None:
329        was_active = self._back_end_active
330        if self._implicit_login_state is None:
331            is_active = False
332        else:
333            is_active = (
334                self._implicit_login_state.login_id == self._active_login_id
335            )
336        if was_active != is_active:
337            if DEBUG_LOG:
338                logging.debug(
339                    'LoginAdapter: %s adapter back-end-active is now %s.',
340                    self.login_type.name,
341                    is_active,
342                )
343            self.on_back_end_active_change(is_active)
344            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)
57    def __init__(self, login_type: LoginType):
58        assert _babase.in_logic_thread()
59        self.login_type = login_type
60        self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = (
61            None
62        )
63        self._on_app_loading_called = False
64        self._implicit_login_state_dirty = False
65        self._back_end_active = False
66
67        # Which login of our type (if any) is associated with the
68        # current active primary account.
69        self._active_login_id: str | None = None
70
71        self._last_sign_in_time: float | None = None
72        self._last_sign_in_desc: str | None = None
login_type
def on_app_loading(self) -> None:
74    def on_app_loading(self) -> None:
75        """Should be called for each adapter in on_app_loading."""
76
77        assert not self._on_app_loading_called
78        self._on_app_loading_called = True
79
80        # Any implicit state we received up until now needs to be pushed
81        # to the app account subsystem.
82        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:
 84    def set_implicit_login_state(
 85        self, state: ImplicitLoginState | None
 86    ) -> None:
 87        """Keep the adapter informed of implicit login states.
 88
 89        This should be called by the adapter back-end when an account
 90        of their associated type gets logged in or out.
 91        """
 92        assert _babase.in_logic_thread()
 93
 94        # Ignore redundant sets.
 95        if state == self._implicit_login_state:
 96            return
 97
 98        if DEBUG_LOG:
 99            if state is None:
100                logging.debug(
101                    'LoginAdapter: %s implicit state changed;'
102                    ' now signed out.',
103                    self.login_type.name,
104                )
105            else:
106                logging.debug(
107                    'LoginAdapter: %s implicit state changed;'
108                    ' now signed in as %s.',
109                    self.login_type.name,
110                    state.display_name,
111                )
112
113        self._implicit_login_state = state
114        self._implicit_login_state_dirty = True
115
116        # (possibly) push it to the app for handling.
117        self._update_implicit_login_state()
118
119        # This might affect whether we consider that back-end as 'active'.
120        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:
122    def set_active_logins(self, logins: dict[LoginType, str]) -> None:
123        """Keep the adapter informed of actively used logins.
124
125        This should be called by the app's account subsystem to
126        keep adapters up to date on the full set of logins attached
127        to the currently-in-use account.
128        Note that the logins dict passed in should be immutable as
129        only a reference to it is stored, not a copy.
130        """
131        assert _babase.in_logic_thread()
132        if DEBUG_LOG:
133            logging.debug(
134                'LoginAdapter: %s adapter got active logins %s.',
135                self.login_type.name,
136                {k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
137            )
138
139        self._active_login_id = logins.get(self.login_type)
140        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:
142    def on_back_end_active_change(self, active: bool) -> None:
143        """Called when active state for the back-end is (possibly) changing.
144
145        Meant to be overridden by subclasses.
146        Being active means that the implicit login provided by the back-end
147        is actually being used by the app. It should therefore register
148        unlocked achievements, leaderboard scores, allow viewing native
149        UIs, etc. When not active it should ignore everything and behave
150        as if signed out, even if it technically is still signed in.
151        """
152        assert _babase.in_logic_thread()
153        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:
155    @final
156    def sign_in(
157        self,
158        result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
159        description: str,
160    ) -> None:
161        """Attempt to sign in via this adapter.
162
163        This can be called even if the back-end is not implicitly signed in;
164        the adapter will attempt to sign in if possible. An exception will
165        be returned if the sign-in attempt fails.
166        """
167        assert _babase.in_logic_thread()
168        from babase._general import Call
169
170        # Have been seeing multiple sign-in attempts come through
171        # nearly simultaneously which can be problematic server-side.
172        # Let's error if a sign-in attempt is made within a few seconds
173        # of the last one to try and address this.
174        now = time.monotonic()
175        appnow = _babase.apptime()
176        if self._last_sign_in_time is not None:
177            since_last = now - self._last_sign_in_time
178            if since_last < 1.0:
179                logging.warning(
180                    'LoginAdapter: %s adapter sign_in() called too soon'
181                    ' (%.2fs) after last; this-desc="%s", last-desc="%s",'
182                    ' ba-app-time=%.2f.',
183                    self.login_type.name,
184                    since_last,
185                    description,
186                    self._last_sign_in_desc,
187                    appnow,
188                )
189                _babase.pushcall(
190                    Call(
191                        result_cb,
192                        self,
193                        RuntimeError('sign_in called too soon after last.'),
194                    )
195                )
196                return
197
198        self._last_sign_in_desc = description
199        self._last_sign_in_time = now
200
201        if DEBUG_LOG:
202            logging.debug(
203                'LoginAdapter: %s adapter sign_in() called;'
204                ' fetching sign-in-token...',
205                self.login_type.name,
206            )
207
208        def _got_sign_in_token_result(result: str | None) -> None:
209            import bacommon.cloud
210
211            # Failed to get a sign-in-token.
212            if result is None:
213                if DEBUG_LOG:
214                    logging.debug(
215                        'LoginAdapter: %s adapter sign-in-token fetch failed;'
216                        ' aborting sign-in.',
217                        self.login_type.name,
218                    )
219                _babase.pushcall(
220                    Call(
221                        result_cb,
222                        self,
223                        RuntimeError('fetch-sign-in-token failed.'),
224                    )
225                )
226                return
227
228            # Got a sign-in token! Now pass it to the cloud which will use
229            # it to verify our identity and give us app credentials on
230            # success.
231            if DEBUG_LOG:
232                logging.debug(
233                    'LoginAdapter: %s adapter sign-in-token fetch succeeded;'
234                    ' passing to cloud for verification...',
235                    self.login_type.name,
236                )
237
238            def _got_sign_in_response(
239                response: bacommon.cloud.SignInResponse | Exception,
240            ) -> None:
241                # This likely means we couldn't communicate with the server.
242                if isinstance(response, Exception):
243                    if DEBUG_LOG:
244                        logging.debug(
245                            'LoginAdapter: %s adapter got error'
246                            ' sign-in response: %s',
247                            self.login_type.name,
248                            response,
249                        )
250                    _babase.pushcall(Call(result_cb, self, response))
251                else:
252                    # This means our credentials were explicitly rejected.
253                    if response.credentials is None:
254                        result2: LoginAdapter.SignInResult | Exception = (
255                            RuntimeError('Sign-in-token was rejected.')
256                        )
257                    else:
258                        if DEBUG_LOG:
259                            logging.debug(
260                                'LoginAdapter: %s adapter got successful'
261                                ' sign-in response',
262                                self.login_type.name,
263                            )
264                        result2 = self.SignInResult(
265                            credentials=response.credentials
266                        )
267                    _babase.pushcall(Call(result_cb, self, result2))
268
269            assert _babase.app.plus is not None
270            _babase.app.plus.cloud.send_message_cb(
271                bacommon.cloud.SignInMessage(
272                    self.login_type,
273                    result,
274                    description=description,
275                    apptime=appnow,
276                ),
277                on_response=_got_sign_in_response,
278            )
279
280        # Kick off the sign-in process by fetching a sign-in token.
281        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:
283    def is_back_end_active(self) -> bool:
284        """Is this adapter's back-end currently active?"""
285        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:
287    def get_sign_in_token(
288        self, completion_cb: Callable[[str | None], None]
289    ) -> None:
290        """Get a sign-in token from the adapter back end.
291
292        This token is then passed to the master-server to complete the
293        sign-in process. The adapter can use this opportunity to bring
294        up account creation UI, call its internal sign_in function, etc.
295        as needed. The provided completion_cb should then be called with
296        either a token or None if sign in failed or was cancelled.
297        """
298        from babase._general import Call
299
300        # Default implementation simply fails immediately.
301        _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:
44    @dataclass
45    class SignInResult:
46        """Describes the final result of a sign-in attempt."""
47
48        credentials: str

Describes the final result of a sign-in attempt.

LoginAdapter.SignInResult(credentials: str)
credentials: str
@dataclass
class LoginAdapter.ImplicitLoginState:
50    @dataclass
51    class ImplicitLoginState:
52        """Describes the current state of an implicit login."""
53
54        login_id: str
55        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:
25@dataclass
26class LoginInfo:
27    """Basic info about a login available in the app.plus.accounts section."""
28
29    name: str

Basic info about a login available in the app.plus.accounts section.

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

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

class Mesh:
53class Mesh:
54    """Category: **User Interface Classes**"""
55
56    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:
426def open_url(address: str, force_internal: bool = False) -> None:
427    """Open a provided URL.
428
429    Category: **General Utility Functions**
430
431    Open the provided url in a web-browser, or display the URL
432    string in a window if that isn't possible (or if force_internal
433    is True).
434    """
435    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:
324class Plugin:
325    """A plugin to alter app behavior in some way.
326
327    Category: **App Classes**
328
329    Plugins are discoverable by the meta-tag system
330    and the user can select which ones they want to enable.
331    Enabled plugins are then called at specific times as the
332    app is running in order to modify its behavior in some way.
333    """
334
335    def on_app_running(self) -> None:
336        """Called when the app reaches the running state."""
337
338    def on_app_suspend(self) -> None:
339        """Called when the app enters the suspended state."""
340
341    def on_app_unsuspend(self) -> None:
342        """Called when the app exits the suspended state."""
343
344    def on_app_shutdown(self) -> None:
345        """Called when the app is beginning the shutdown process."""
346
347    def on_app_shutdown_complete(self) -> None:
348        """Called when the app has completed the shutdown process."""
349
350    def has_settings_ui(self) -> bool:
351        """Called to ask if we have settings UI we can show."""
352        return False
353
354    def show_settings_ui(self, source_widget: Any | None) -> None:
355        """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:
335    def on_app_running(self) -> None:
336        """Called when the app reaches the running state."""

Called when the app reaches the running state.

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

Called when the app enters the suspended state.

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

Called when the app exits the suspended state.

def on_app_shutdown(self) -> None:
344    def on_app_shutdown(self) -> None:
345        """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:
347    def on_app_shutdown_complete(self) -> None:
348        """Called when the app has completed the shutdown process."""

Called when the app has completed the shutdown process.

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

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

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

Called to show our settings UI.

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

Whether the user wants this plugin to load.

def attempt_load_if_enabled(self) -> Plugin | None:
270    def attempt_load_if_enabled(self) -> Plugin | None:
271        """Possibly load the plugin and log any errors."""
272        from babase._general import getclass
273        from babase._language import Lstr
274
275        assert not self.attempted_load
276        assert self.plugin is None
277
278        if not self.enabled:
279            return None
280        self.attempted_load = True
281        if not self.loadable:
282            return None
283        try:
284            cls = getclass(self.class_path, Plugin)
285        except Exception as exc:
286            _babase.getsimplesound('error').play()
287            _babase.screenmessage(
288                Lstr(
289                    resource='pluginClassLoadErrorText',
290                    subs=[
291                        ('${PLUGIN}', self.class_path),
292                        ('${ERROR}', str(exc)),
293                    ],
294                ),
295                color=(1, 0, 0),
296            )
297            logging.exception(
298                "Error loading plugin class '%s'.", self.class_path
299            )
300            return None
301        try:
302            self.plugin = cls()
303            return self.plugin
304        except Exception as exc:
305            from babase import _error
306
307            _babase.getsimplesound('error').play()
308            _babase.screenmessage(
309                Lstr(
310                    resource='pluginInitErrorText',
311                    subs=[
312                        ('${PLUGIN}', self.class_path),
313                        ('${ERROR}', str(exc)),
314                    ],
315                ),
316                color=(1, 0, 0),
317            )
318            logging.exception(
319                "Error initing plugin class: '%s'.", self.class_path
320            )
321        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:
1328def pushcall(
1329    call: Callable,
1330    from_other_thread: bool = False,
1331    suppress_other_thread_warning: bool = False,
1332    other_thread_use_fg_context: bool = False,
1333    raw: bool = False,
1334) -> None:
1335    """Push a call to the logic event-loop.
1336    Category: **General Utility Functions**
1337
1338    This call expects to be used in the logic thread, and will automatically
1339    save and restore the babase.Context to behave seamlessly.
1340
1341    If you want to push a call from outside of the logic thread,
1342    however, you can pass 'from_other_thread' as True. In this case
1343    the call will always run in the UI context_ref on the logic thread
1344    or whichever context_ref is in the foreground if
1345    other_thread_use_fg_context is True.
1346    Passing raw=True will disable thread checks and context_ref sets/restores.
1347    """
1348    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:
1352def quit(
1353    confirm: bool = False, quit_type: babase.QuitType | None = None
1354) -> None:
1355    """Quit the app.
1356
1357    Category: **General Utility Functions**
1358
1359    If 'confirm' is True, a confirm dialog will be presented if conditions
1360    allow; otherwise the quit will still be immediate.
1361    See docs for babase.QuitType for explanations of the optional
1362    'quit_type' arg.
1363    """
1364    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:
438def rowwidget(
439    edit: bauiv1.Widget | None = None,
440    parent: bauiv1.Widget | None = None,
441    size: Sequence[float] | None = None,
442    position: Sequence[float] | None = None,
443    background: bool | None = None,
444    selected_child: bauiv1.Widget | None = None,
445    visible_child: bauiv1.Widget | None = None,
446    claims_left_right: bool | None = None,
447    claims_tab: bool | None = None,
448    selection_loops_to_parent: bool | None = None,
449) -> bauiv1.Widget:
450    """Create or edit a row widget.
451
452    Category: **User Interface Functions**
453
454    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
455    a new one is created and returned. Arguments that are not set to None
456    are applied to the Widget.
457    """
458    import bauiv1  # pylint: disable=cyclic-import
459
460    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, ...]:
1402def safecolor(
1403    color: Sequence[float], target_intensity: float = 0.6
1404) -> tuple[float, ...]:
1405    """Given a color tuple, return a color safe to display as text.
1406
1407    Category: **General Utility Functions**
1408
1409    Accepts tuples of length 3 or 4. This will slightly brighten very
1410    dark colors, etc.
1411    """
1412    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:
1415def screenmessage(
1416    message: str | babase.Lstr,
1417    color: Sequence[float] | None = None,
1418    log: bool = False,
1419) -> None:
1420    """Print a message to the local client's screen, in a given color.
1421
1422    Category: **General Utility Functions**
1423
1424    Note that this version of the function is purely for local display.
1425    To broadcast screen messages in network play, look for methods such as
1426    broadcastmessage() provided by the scene-version packages.
1427    """
1428    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:
463def scrollwidget(
464    edit: bauiv1.Widget | None = None,
465    parent: bauiv1.Widget | None = None,
466    size: Sequence[float] | None = None,
467    position: Sequence[float] | None = None,
468    background: bool | None = None,
469    selected_child: bauiv1.Widget | None = None,
470    capture_arrows: bool = False,
471    on_select_call: Callable | None = None,
472    center_small_content: bool | None = None,
473    color: Sequence[float] | None = None,
474    highlight: bool | None = None,
475    border_opacity: float | None = None,
476    simple_culling_v: float | None = None,
477    selection_loops_to_parent: bool | None = None,
478    claims_left_right: bool | None = None,
479    claims_up_down: bool | None = None,
480    claims_tab: bool | None = None,
481    autoselect: bool | None = None,
482) -> bauiv1.Widget:
483    """Create or edit a scroll widget.
484
485    Category: **User Interface Functions**
486
487    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
488    a new one is created and returned. Arguments that are not set to None
489    are applied to the Widget.
490    """
491    import bauiv1  # pylint: disable=cyclic-import
492
493    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:
1431def set_analytics_screen(screen: str) -> None:
1432    """Used for analytics to see where in the app players spend their time.
1433
1434    Category: **General Utility Functions**
1435
1436    Generally called when opening a new window or entering some UI.
1437    'screen' should be a string description of an app location
1438    ('Main Menu', etc.)
1439    """
1440    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:
59class Sound:
60    """Category: **User Interface Classes**"""
61
62    def play(self) -> None:
63        """Play the sound locally."""
64        return None
65
66    def stop(self) -> None:
67        """Stop the sound if it is playing."""
68        return None

Category: User Interface Classes

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

Play the sound locally.

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

Stop the sound if it is playing.

class SpecialChar(enum.Enum):
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:
71class Texture:
72    """Category: **User Interface Classes**"""
73
74    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) -> Widget:
506def textwidget(
507    edit: bauiv1.Widget | None = None,
508    parent: bauiv1.Widget | None = None,
509    size: Sequence[float] | None = None,
510    position: Sequence[float] | None = None,
511    text: str | bauiv1.Lstr | None = None,
512    v_align: str | None = None,
513    h_align: str | None = None,
514    editable: bool | None = None,
515    padding: float | None = None,
516    on_return_press_call: Callable[[], None] | None = None,
517    on_activate_call: Callable[[], None] | None = None,
518    selectable: bool | None = None,
519    query: bauiv1.Widget | None = None,
520    max_chars: int | None = None,
521    color: Sequence[float] | None = None,
522    click_activate: bool | None = None,
523    on_select_call: Callable[[], None] | None = None,
524    always_highlight: bool | None = None,
525    draw_controller: bauiv1.Widget | None = None,
526    scale: float | None = None,
527    corner_scale: float | None = None,
528    description: str | bauiv1.Lstr | None = None,
529    transition_delay: float | None = None,
530    maxwidth: float | None = None,
531    max_height: float | None = None,
532    flatness: float | None = None,
533    shadow: float | None = None,
534    autoselect: bool | None = None,
535    rotate: float | None = None,
536    enabled: bool | None = None,
537    force_internal_editing: bool | None = None,
538    always_show_carat: bool | None = None,
539    big: bool | None = None,
540    extra_touch_border_scale: float | None = None,
541    res_scale: float | None = None,
542    query_max_chars: bauiv1.Widget | None = None,
543    query_description: bauiv1.Widget | None = None,
544    adapter_finished: bool | None = None,
545    glow_type: str | None = None,
546) -> bauiv1.Widget:
547    """Create or edit a text widget.
548
549    Category: **User Interface Functions**
550
551    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
552    a new one is created and returned. Arguments that are not set to None
553    are applied to the Widget.
554    """
555    import bauiv1  # pylint: disable=cyclic-import
556
557    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:
176def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None:
177    """Add a check to ensure a widget-owning object gets cleaned up properly.
178
179    Category: User Interface Functions
180
181    This adds a check which will print an error message if the provided
182    object still exists ~5 seconds after the provided bauiv1.Widget dies.
183
184    This is a good sanity check for any sort of object that wraps or
185    controls a bauiv1.Widget. For instance, a 'Window' class instance has
186    no reason to still exist once its root container bauiv1.Widget has fully
187    transitioned out and been destroyed. Circular references or careless
188    strong referencing can lead to such objects never getting destroyed,
189    however, and this helps detect such cases to avoid memory leaks.
190    """
191    if DEBUG_UI_CLEANUP_CHECKS:
192        print(f'adding uicleanup to {obj}')
193    if not isinstance(widget, _bauiv1.Widget):
194        raise TypeError('widget arg is not a bauiv1.Widget')
195
196    if bool(False):
197
198        def foobar() -> None:
199            """Just testing."""
200            if DEBUG_UI_CLEANUP_CHECKS:
201                print('uicleanupcheck widget dying...')
202
203        widget.add_delete_callback(foobar)
204
205    assert babase.app.classic is not None
206    babase.app.ui_v1.cleanupchecks.append(
207        UICleanupCheck(
208            obj=weakref.ref(obj), widget=widget, widget_death_time=None
209        )
210    )

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

Current ui scale for the app.

@override
def on_app_loading(self) -> None:
 88    @override
 89    def on_app_loading(self) -> None:
 90        from bauiv1._uitypes import UIController, ui_upkeep
 91
 92        # IMPORTANT: If tweaking UI stuff, make sure it behaves for small,
 93        # medium, and large UI modes. (doesn't run off screen, etc).
 94        # The overrides below can be used to test with different sizes.
 95        # Generally small is used on phones, medium is used on tablets/tvs,
 96        # and large is on desktop computers or perhaps large tablets. When
 97        # possible, run in windowed mode and resize the window to assure
 98        # this holds true at all aspect ratios.
 99
100        # UPDATE: A better way to test this is now by setting the environment
101        # variable BA_UI_SCALE to "small", "medium", or "large".
102        # This will affect system UIs not covered by the values below such
103        # as screen-messages. The below values remain functional, however,
104        # for cases such as Android where environment variables can't be set
105        # easily.
106
107        if bool(False):  # force-test ui scale
108            self._uiscale = babase.UIScale.SMALL
109            with babase.ContextRef.empty():
110                babase.pushcall(
111                    lambda: babase.screenmessage(
112                        f'FORCING UISCALE {self._uiscale.name} FOR TESTING',
113                        color=(1, 0, 1),
114                        log=True,
115                    )
116                )
117
118        self.controller = UIController()
119
120        # Kick off our periodic UI upkeep.
121        # FIXME: Can probably kill this if we do immediate UI death checks.
122        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:
124    def set_main_menu_window(
125        self,
126        window: bauiv1.Widget,
127        from_window: bauiv1.Widget | None | bool = True,
128    ) -> None:
129        """Set the current 'main' window, replacing any existing.
130
131        If 'from_window' is passed as a bauiv1.Widget or None, a warning
132        will be issued if it that value does not match the current main
133        window. This can help clean up flawed code that can lead to bad
134        UI states. A value of False will disable the check.
135        """
136
137        existing = self._main_menu_window
138
139        try:
140            if isinstance(from_window, bool):
141                # For default val True we warn that the arg wasn't
142                # passed. False can be explicitly passed to disable this
143                # check.
144                if from_window is True:
145                    caller_frame = inspect.stack()[1]
146                    caller_filename = caller_frame.filename
147                    caller_line_number = caller_frame.lineno
148                    logging.warning(
149                        'set_main_menu_window() should be passed a'
150                        " 'from_window' value to help ensure proper UI behavior"
151                        ' (%s line %i).',
152                        caller_filename,
153                        caller_line_number,
154                    )
155            else:
156                # For everything else, warn if what they passed wasn't
157                # the previous main menu widget.
158                if from_window is not existing:
159                    caller_frame = inspect.stack()[1]
160                    caller_filename = caller_frame.filename
161                    caller_line_number = caller_frame.lineno
162                    logging.warning(
163                        "set_main_menu_window() was passed 'from_window' %s"
164                        ' but existing main-menu-window is %s. (%s line %i).',
165                        from_window,
166                        existing,
167                        caller_filename,
168                        caller_line_number,
169                    )
170        except Exception:
171            # Prevent any bugs in these checks from causing problems.
172            logging.exception('Error checking from_window')
173
174        # Once the above code leads to us fixing all leftover window bugs
175        # at the source, we can kill the code below.
176
177        # Let's grab the location where we were called from to report
178        # if we have to force-kill the existing window (which normally
179        # should not happen).
180        frameline = None
181        try:
182            frame = inspect.currentframe()
183            if frame is not None:
184                frame = frame.f_back
185            if frame is not None:
186                frameinfo = inspect.getframeinfo(frame)
187                frameline = f'{frameinfo.filename} {frameinfo.lineno}'
188        except Exception:
189            logging.exception('Error calcing line for set_main_menu_window')
190
191        # With our legacy main-menu system, the caller is responsible for
192        # clearing out the old main menu window when assigning the new.
193        # However there are corner cases where that doesn't happen and we get
194        # old windows stuck under the new main one. So let's guard against
195        # that. However, we can't simply delete the existing main window when
196        # a new one is assigned because the user may transition the old out
197        # *after* the assignment. Sigh. So, as a happy medium, let's check in
198        # on the old after a short bit of time and kill it if its still alive.
199        # That will be a bit ugly on screen but at least should un-break
200        # things.
201        def _delay_kill() -> None:
202            import time
203
204            if existing:
205                print(
206                    f'Killing old main_menu_window'
207                    f' when called at: {frameline} t={time.time():.3f}'
208                )
209                existing.delete()
210
211        babase.apptimer(1.0, _delay_kill)
212        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:
214    def clear_main_menu_window(self, transition: str | None = None) -> None:
215        """Clear any existing 'main' window with the provided transition."""
216        assert transition is None or not transition.endswith('_in')
217        if self._main_menu_window:
218            if (
219                transition is not None
220                and not self._main_menu_window.transitioning_out
221            ):
222                _bauiv1.containerwidget(
223                    edit=self._main_menu_window, transition=transition
224                )
225            else:
226                self._main_menu_window.delete()
227            self._main_menu_window = None

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

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

Return whether a main menu window is present.

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

Set the location represented by the current main menu window.

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

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

Category: User Interface Classes

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

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:
 98    def activate(self) -> None:
 99        """Activates a widget; the same as if it had been clicked."""
100        return None

Activates a widget; the same as if it had been clicked.

def add_delete_callback(self, call: Callable) -> None:
102    def add_delete_callback(self, call: Callable) -> None:
103        """Add a call to be run immediately after this widget is destroyed."""
104        return None

Add a call to be run immediately after this widget is destroyed.

def delete(self, ignore_missing: bool = True) -> None:
106    def delete(self, ignore_missing: bool = True) -> None:
107        """Delete the Widget. Ignores already-deleted Widgets if ignore_missing
108        is True; otherwise an Exception is thrown.
109        """
110        return None

Delete the Widget. Ignores already-deleted Widgets if ignore_missing is True; otherwise an Exception is thrown.

def exists(self) -> bool:
112    def exists(self) -> bool:
113        """Returns whether the Widget still exists.
114        Most functionality will fail on a nonexistent widget.
115
116        Note that you can also use the boolean operator for this same
117        functionality, so a statement such as "if mywidget" will do
118        the right thing both for Widget objects and values of None.
119        """
120        return bool()

Returns whether the Widget still exists. Most functionality will fail on a nonexistent widget.

Note that you can also use the boolean operator for this same functionality, so a statement such as "if mywidget" will do the right thing both for Widget objects and values of None.

def get_children(self) -> list[Widget]:
122    def get_children(self) -> list[bauiv1.Widget]:
123        """Returns any child Widgets of this Widget."""
124        import bauiv1
125
126        return [bauiv1.Widget()]

Returns any child Widgets of this Widget.

def get_screen_space_center(self) -> tuple[float, float]:
128    def get_screen_space_center(self) -> tuple[float, float]:
129        """Returns the coords of the bauiv1.Widget center relative to the center
130        of the screen. This can be useful for placing pop-up windows and other
131        special cases.
132        """
133        return (0.0, 0.0)

Returns the coords of the bauiv1.Widget center relative to the center of the screen. This can be useful for placing pop-up windows and other special cases.

def get_selected_child(self) -> Widget | None:
135    def get_selected_child(self) -> bauiv1.Widget | None:
136        """Returns the selected child Widget or None if nothing is selected."""
137        import bauiv1
138
139        return bauiv1.Widget()

Returns the selected child Widget or None if nothing is selected.

def get_widget_type(self) -> str:
141    def get_widget_type(self) -> str:
142        """Return the internal type of the Widget as a string. Note that this
143        is different from the Python bauiv1.Widget type, which is the same for
144        all widgets.
145        """
146        return str()

Return the internal type of the Widget as a string. Note that this is different from the Python bauiv1.Widget type, which is the same for all widgets.

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

A basic window.

Category: User Interface Classes

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

Return the root widget.