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    can_toggle_fullscreen,
 35    charstr,
 36    clipboard_is_supported,
 37    clipboard_set_text,
 38    commit_app_config,
 39    ContextRef,
 40    displaytime,
 41    DisplayTime,
 42    displaytimer,
 43    DisplayTimer,
 44    do_once,
 45    fade_screen,
 46    get_display_resolution,
 47    get_ip_address_type,
 48    get_low_level_config_value,
 49    get_max_graphics_quality,
 50    get_remote_app_name,
 51    get_replays_dir,
 52    get_string_height,
 53    get_string_width,
 54    get_type_name,
 55    getclass,
 56    have_permission,
 57    in_logic_thread,
 58    increment_analytics_count,
 59    is_browser_likely_available,
 60    is_running_on_fire_tv,
 61    is_xcode_build,
 62    Keyboard,
 63    lock_all_input,
 64    LoginAdapter,
 65    Lstr,
 66    NotFoundError,
 67    Permission,
 68    Plugin,
 69    PluginSpec,
 70    pushcall,
 71    quit,
 72    request_permission,
 73    safecolor,
 74    screenmessage,
 75    set_analytics_screen,
 76    set_low_level_config_value,
 77    set_ui_input_device,
 78    SpecialChar,
 79    supports_max_fps,
 80    supports_vsync,
 81    timestring,
 82    UIScale,
 83    unlock_all_input,
 84    WeakCall,
 85    workspaces_in_use,
 86)
 87
 88from _bauiv1 import (
 89    buttonwidget,
 90    can_show_ad,
 91    checkboxwidget,
 92    columnwidget,
 93    containerwidget,
 94    get_qrcode_texture,
 95    get_special_widget,
 96    getmesh,
 97    getsound,
 98    gettexture,
 99    has_video_ads,
100    have_incentivized_ad,
101    hscrollwidget,
102    imagewidget,
103    is_party_icon_visible,
104    Mesh,
105    open_file_externally,
106    open_url,
107    rowwidget,
108    scrollwidget,
109    set_party_icon_always_visible,
110    set_party_window_open,
111    show_ad,
112    show_ad_2,
113    show_online_score_ui,
114    Sound,
115    Texture,
116    textwidget,
117    uibounds,
118    Widget,
119    widget,
120)
121from bauiv1._uitypes import Window, uicleanupcheck
122from bauiv1._subsystem import UIV1Subsystem
123
124__all__ = [
125    'add_clean_frame_callback',
126    'app',
127    'AppIntent',
128    'AppIntentDefault',
129    'AppIntentExec',
130    'AppMode',
131    'appname',
132    'appnameupper',
133    'appnameupper',
134    'apptime',
135    'AppTime',
136    'apptimer',
137    'AppTimer',
138    'buttonwidget',
139    'Call',
140    'can_show_ad',
141    'can_toggle_fullscreen',
142    'charstr',
143    'checkboxwidget',
144    'clipboard_is_supported',
145    'clipboard_set_text',
146    'columnwidget',
147    'commit_app_config',
148    'containerwidget',
149    'ContextRef',
150    'displaytime',
151    'DisplayTime',
152    'displaytimer',
153    'DisplayTimer',
154    'do_once',
155    'fade_screen',
156    'get_display_resolution',
157    'get_ip_address_type',
158    'get_low_level_config_value',
159    'get_max_graphics_quality',
160    'get_qrcode_texture',
161    'get_remote_app_name',
162    'get_replays_dir',
163    'get_special_widget',
164    'get_string_height',
165    'get_string_width',
166    'get_type_name',
167    'getclass',
168    'getmesh',
169    'getsound',
170    'gettexture',
171    'has_video_ads',
172    'have_incentivized_ad',
173    'have_permission',
174    'hscrollwidget',
175    'imagewidget',
176    'in_logic_thread',
177    'increment_analytics_count',
178    'is_browser_likely_available',
179    'is_party_icon_visible',
180    'is_running_on_fire_tv',
181    'is_xcode_build',
182    'Keyboard',
183    'lock_all_input',
184    'LoginAdapter',
185    'Lstr',
186    'Mesh',
187    'NotFoundError',
188    'open_file_externally',
189    'open_url',
190    'Permission',
191    'Plugin',
192    'PluginSpec',
193    'pushcall',
194    'quit',
195    'request_permission',
196    'rowwidget',
197    'safecolor',
198    'screenmessage',
199    'scrollwidget',
200    'set_analytics_screen',
201    'set_low_level_config_value',
202    'set_party_icon_always_visible',
203    'set_party_window_open',
204    'set_ui_input_device',
205    'show_ad',
206    'show_ad_2',
207    'show_online_score_ui',
208    'Sound',
209    'SpecialChar',
210    'supports_max_fps',
211    'supports_vsync',
212    'Texture',
213    'textwidget',
214    'timestring',
215    'uibounds',
216    'uicleanupcheck',
217    'UIScale',
218    'UIV1Subsystem',
219    'unlock_all_input',
220    'WeakCall',
221    'widget',
222    'Widget',
223    'Window',
224    'workspaces_in_use',
225]
226
227# We want stuff to show up as bauiv1.Foo instead of bauiv1._sub.Foo.
228set_canonical_module_names(globals())
229
230# Sanity check: we want to keep ballistica's dependencies and
231# bootstrapping order clearly defined; let's check a few particular
232# modules to make sure they never directly or indirectly import us
233# before their own execs complete.
234if __debug__:
235    for _mdl in 'babase', '_babase':
236        if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'):
237            logging.warning(
238                '%s was imported before %s finished importing;'
239                ' should not happen.',
240                __name__,
241                _mdl,
242            )
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        return cls._supports_intent(intent)
36
37    @classmethod
38    def _supports_intent(cls, intent: AppIntent) -> bool:
39        """Return whether our mode can handle the provided intent.
40
41        AppModes should override this to define what they can handle.
42        Note that AppExperience does not have to be considered here; that
43        is handled automatically by the can_handle_intent() call."""
44        raise NotImplementedError('AppMode subclasses must override this.')
45
46    def handle_intent(self, intent: AppIntent) -> None:
47        """Handle an intent."""
48        raise NotImplementedError('AppMode subclasses must override this.')
49
50    def on_activate(self) -> None:
51        """Called when the mode is being activated."""
52
53    def on_deactivate(self) -> None:
54        """Called when the mode is being deactivated."""

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        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:
46    def handle_intent(self, intent: AppIntent) -> None:
47        """Handle an intent."""
48        raise NotImplementedError('AppMode subclasses must override this.')

Handle an intent.

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

Called when the mode is being activated.

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

Called when the mode is being deactivated.

def apptime() -> AppTime:
539def apptime() -> babase.AppTime:
540    """Return the current app-time in seconds.
541
542    Category: **General Utility Functions**
543
544    App-time is a monotonic time value; it starts at 0.0 when the app
545    launches and will never jump by large amounts or go backwards, even if
546    the system time changes. Its progression will pause when the app is in
547    a suspended state.
548
549    Note that the AppTime returned here is simply float; it just has a
550    unique type in the type-checker's eyes to help prevent it from being
551    accidentally used with time functionality expecting other time types.
552    """
553    import babase  # pylint: disable=cyclic-import
554
555    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:
558def apptimer(time: float, call: Callable[[], Any]) -> None:
559    """Schedule a callable object to run based on app-time.
560
561    Category: **General Utility Functions**
562
563    This function creates a one-off timer which cannot be canceled or
564    modified once created. If you require the ability to do so, or need
565    a repeating timer, use the babase.AppTimer class instead.
566
567    ##### Arguments
568    ###### time (float)
569    > Length of time in seconds that the timer will wait before firing.
570
571    ###### call (Callable[[], Any])
572    > A callable Python object. Note that the timer will retain a
573    strong reference to the callable for as long as the timer exists, so you
574    may want to look into concepts such as babase.WeakCall if that is not
575    desired.
576
577    ##### Examples
578    Print some stuff through time:
579    >>> babase.screenmessage('hello from now!')
580    >>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
581                              'hello from the future!'))
582    >>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
583    ...                       'hello from the future 2!'))
584    """
585    return None

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

Category: General Utility Functions

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

Arguments
time (float)

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

call (Callable[[], Any])

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

Examples

Print some stuff through time:

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

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

Category: General Utility Classes

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

Arguments
time

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

call

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

repeat

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

Example

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

AppTimer(time: float, call: Callable[[], Any], repeat: bool = False)
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass
def buttonwidget( edit: Widget | None = None, parent: Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, on_activate_call: Optional[Callable] = None, label: str | Lstr | None = None, color: Optional[Sequence[float]] = None, down_widget: Widget | None = None, up_widget: Widget | None = None, left_widget: Widget | None = None, right_widget: Widget | None = None, texture: Texture | None = None, text_scale: float | None = None, textcolor: Optional[Sequence[float]] = None, enable_sound: bool | None = None, mesh_transparent: Mesh | None = None, mesh_opaque: Mesh | None = None, repeat: bool | None = None, scale: float | None = None, transition_delay: float | None = None, on_select_call: Optional[Callable] = None, button_type: str | None = None, extra_touch_border_scale: float | None = None, selectable: bool | None = None, show_buffer_top: float | None = None, icon: Texture | None = None, iconscale: float | None = None, icon_tint: float | None = None, icon_color: Optional[Sequence[float]] = None, autoselect: bool | None = None, mask_texture: Texture | None = None, tint_texture: Texture | None = None, tint_color: Optional[Sequence[float]] = None, tint2_color: Optional[Sequence[float]] = None, text_flatness: float | None = None, text_res_scale: float | None = None, enabled: bool | None = None) -> Widget:
145def buttonwidget(
146    edit: bauiv1.Widget | None = None,
147    parent: bauiv1.Widget | None = None,
148    size: Sequence[float] | None = None,
149    position: Sequence[float] | None = None,
150    on_activate_call: Callable | None = None,
151    label: str | bauiv1.Lstr | None = None,
152    color: Sequence[float] | None = None,
153    down_widget: bauiv1.Widget | None = None,
154    up_widget: bauiv1.Widget | None = None,
155    left_widget: bauiv1.Widget | None = None,
156    right_widget: bauiv1.Widget | None = None,
157    texture: bauiv1.Texture | None = None,
158    text_scale: float | None = None,
159    textcolor: Sequence[float] | None = None,
160    enable_sound: bool | None = None,
161    mesh_transparent: bauiv1.Mesh | None = None,
162    mesh_opaque: bauiv1.Mesh | None = None,
163    repeat: bool | None = None,
164    scale: float | None = None,
165    transition_delay: float | None = None,
166    on_select_call: Callable | None = None,
167    button_type: str | None = None,
168    extra_touch_border_scale: float | None = None,
169    selectable: bool | None = None,
170    show_buffer_top: float | None = None,
171    icon: bauiv1.Texture | None = None,
172    iconscale: float | None = None,
173    icon_tint: float | None = None,
174    icon_color: Sequence[float] | None = None,
175    autoselect: bool | None = None,
176    mask_texture: bauiv1.Texture | None = None,
177    tint_texture: bauiv1.Texture | None = None,
178    tint_color: Sequence[float] | None = None,
179    tint2_color: Sequence[float] | None = None,
180    text_flatness: float | None = None,
181    text_res_scale: float | None = None,
182    enabled: bool | None = None,
183) -> bauiv1.Widget:
184    """Create or edit a button widget.
185
186    Category: **User Interface Functions**
187
188    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
189    a new one is created and returned. Arguments that are not set to None
190    are applied to the Widget.
191    """
192    import bauiv1  # pylint: disable=cyclic-import
193
194    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:
598def charstr(char_id: babase.SpecialChar) -> str:
599    """Get a unicode string representing a special character.
600
601    Category: **General Utility Functions**
602
603    Note that these utilize the private-use block of unicode characters
604    (U+E000-U+F8FF) and are specific to the game; exporting or rendering
605    them elsewhere will be meaningless.
606
607    See babase.SpecialChar for the list of available characters.
608    """
609    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:
202def checkboxwidget(
203    edit: bauiv1.Widget | None = None,
204    parent: bauiv1.Widget | None = None,
205    size: Sequence[float] | None = None,
206    position: Sequence[float] | None = None,
207    text: str | bauiv1.Lstr | None = None,
208    value: bool | None = None,
209    on_value_change_call: Callable[[bool], None] | None = None,
210    on_select_call: Callable[[], None] | None = None,
211    text_scale: float | None = None,
212    textcolor: Sequence[float] | None = None,
213    scale: float | None = None,
214    is_radio_button: bool | None = None,
215    maxwidth: float | None = None,
216    autoselect: bool | None = None,
217    color: Sequence[float] | None = None,
218) -> bauiv1.Widget:
219    """Create or edit a check-box widget.
220
221    Category: **User Interface Functions**
222
223    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
224    a new one is created and returned. Arguments that are not set to None
225    are applied to the Widget.
226    """
227    import bauiv1  # pylint: disable=cyclic-import
228
229    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:
634def clipboard_is_supported() -> bool:
635    """Return whether this platform supports clipboard operations at all.
636
637    Category: **General Utility Functions**
638
639    If this returns False, UIs should not show 'copy to clipboard'
640    buttons, etc.
641    """
642    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:
645def clipboard_set_text(value: str) -> None:
646    """Copy a string to the system clipboard.
647
648    Category: **General Utility Functions**
649
650    Ensure that babase.clipboard_is_supported() returns True before adding
651     buttons/etc. that make use of this functionality.
652    """
653    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:
232def columnwidget(
233    edit: bauiv1.Widget | None = None,
234    parent: bauiv1.Widget | None = None,
235    size: Sequence[float] | None = None,
236    position: Sequence[float] | None = None,
237    background: bool | None = None,
238    selected_child: bauiv1.Widget | None = None,
239    visible_child: bauiv1.Widget | None = None,
240    single_depth: bool | None = None,
241    print_list_exit_instructions: bool | None = None,
242    left_border: float | None = None,
243    top_border: float | None = None,
244    bottom_border: float | None = None,
245    selection_loops_to_parent: bool | None = None,
246    border: float | None = None,
247    margin: float | None = None,
248    claims_left_right: bool | None = None,
249    claims_tab: bool | None = None,
250) -> bauiv1.Widget:
251    """Create or edit a column widget.
252
253    Category: **User Interface Functions**
254
255    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
256    a new one is created and returned. Arguments that are not set to None
257    are applied to the Widget.
258    """
259    import bauiv1  # pylint: disable=cyclic-import
260
261    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:
274def containerwidget(
275    edit: bauiv1.Widget | None = None,
276    parent: bauiv1.Widget | None = None,
277    size: Sequence[float] | None = None,
278    position: Sequence[float] | None = None,
279    background: bool | None = None,
280    selected_child: bauiv1.Widget | None = None,
281    transition: str | None = None,
282    cancel_button: bauiv1.Widget | None = None,
283    start_button: bauiv1.Widget | None = None,
284    root_selectable: bool | None = None,
285    on_activate_call: Callable[[], None] | None = None,
286    claims_left_right: bool | None = None,
287    claims_tab: bool | None = None,
288    selection_loops: bool | None = None,
289    selection_loops_to_parent: bool | None = None,
290    scale: float | None = None,
291    on_outside_click_call: Callable[[], None] | None = None,
292    single_depth: bool | None = None,
293    visible_child: bauiv1.Widget | None = None,
294    stack_offset: Sequence[float] | None = None,
295    color: Sequence[float] | None = None,
296    on_cancel_call: Callable[[], None] | None = None,
297    print_list_exit_instructions: bool | None = None,
298    click_activate: bool | None = None,
299    always_highlight: bool | None = None,
300    selectable: bool | None = None,
301    scale_origin_stack_offset: Sequence[float] | None = None,
302    toolbar_visibility: str | None = None,
303    on_select_call: Callable[[], None] | None = None,
304    claim_outside_clicks: bool | None = None,
305    claims_up_down: bool | None = None,
306) -> bauiv1.Widget:
307    """Create or edit a container widget.
308
309    Category: **User Interface Functions**
310
311    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
312    a new one is created and returned. Arguments that are not set to None
313    are applied to the Widget.
314    """
315    import bauiv1  # pylint: disable=cyclic-import
316
317    return bauiv1.Widget()

Create or edit a container widget.

Category: User Interface Functions

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

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

Store or use a ballistica context.

Category: General Utility Classes

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

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

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

Usage

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

Example

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

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

Return a ContextRef pointing to no context.

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

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

Whether the context was created as empty.

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

Whether the context has expired.

def displaytime() -> DisplayTime:
694def displaytime() -> babase.DisplayTime:
695    """Return the current display-time in seconds.
696
697    Category: **General Utility Functions**
698
699    Display-time is a time value intended to be used for animation and other
700    visual purposes. It will generally increment by a consistent amount each
701    frame. It will pass at an overall similar rate to AppTime, but trades
702    accuracy for smoothness.
703
704    Note that the value returned here is simply a float; it just has a
705    unique type in the type-checker's eyes to help prevent it from being
706    accidentally used with time functionality expecting other time types.
707    """
708    import babase  # pylint: disable=cyclic-import
709
710    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:
713def displaytimer(time: float, call: Callable[[], Any]) -> None:
714    """Schedule a callable object to run based on display-time.
715
716    Category: **General Utility Functions**
717
718    This function creates a one-off timer which cannot be canceled or
719    modified once created. If you require the ability to do so, or need
720    a repeating timer, use the babase.DisplayTimer class instead.
721
722    Display-time is a time value intended to be used for animation and other
723    visual purposes. It will generally increment by a consistent amount each
724    frame. It will pass at an overall similar rate to AppTime, but trades
725    accuracy for smoothness.
726
727    ##### Arguments
728    ###### time (float)
729    > Length of time in seconds that the timer will wait before firing.
730
731    ###### call (Callable[[], Any])
732    > A callable Python object. Note that the timer will retain a
733    strong reference to the callable for as long as the timer exists, so you
734    may want to look into concepts such as babase.WeakCall if that is not
735    desired.
736
737    ##### Examples
738    Print some stuff through time:
739    >>> babase.screenmessage('hello from now!')
740    >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
741    ...                       'hello from the future!'))
742    >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
743    ...                       'hello from the future 2!'))
744    """
745    return None

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

Category: General Utility Functions

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

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

Arguments
time (float)

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

call (Callable[[], Any])

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

Examples

Print some stuff through time:

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

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

Category: General Utility Classes

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

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

Arguments
time

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

call

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

repeat

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

Example

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

DisplayTimer(time: float, call: Callable[[], Any], repeat: bool = False)
265    def __init__(
266        self, time: float, call: Callable[[], Any], repeat: bool = False
267    ) -> None:
268        pass
def do_once() -> bool:
753def do_once() -> bool:
754    """Return whether this is the first time running a line of code.
755
756    Category: **General Utility Functions**
757
758    This is used by 'print_once()' type calls to keep from overflowing
759    logs. The call functions by registering the filename and line where
760    The call is made from.  Returns True if this location has not been
761    registered already, and False if it has.
762
763    ##### Example
764    This print will only fire for the first loop iteration:
765    >>> for i in range(10):
766    ... if babase.do_once():
767    ...     print('HelloWorld once from loop!')
768    """
769    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_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:
328def get_qrcode_texture(url: str) -> bauiv1.Texture:
329    """Return a QR code texture.
330
331    The provided url must be 64 bytes or less.
332    """
333    import bauiv1  # pylint: disable=cyclic-import
334
335    return bauiv1.Texture()

Return a QR code texture.

The provided url must be 64 bytes or less.

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

Return a full type name including module for a class.

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

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

Category: General Utility Functions

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

def getmesh(name: str) -> Mesh:
345def getmesh(name: str) -> bauiv1.Mesh:
346    """Load a mesh for use solely in the local user interface."""
347    import bauiv1  # pylint: disable=cyclic-import
348
349    return bauiv1.Mesh()

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

def getsound(name: str) -> Sound:
352def getsound(name: str) -> bauiv1.Sound:
353    """Load a sound for use in the ui."""
354    import bauiv1  # pylint: disable=cyclic-import
355
356    return bauiv1.Sound()

Load a sound for use in the ui.

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

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

Attempt an explicit 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:
276    def is_back_end_active(self) -> bool:
277        """Is this adapter's back-end currently active?"""
278        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:
280    def get_sign_in_token(
281        self, completion_cb: Callable[[str | None], None]
282    ) -> None:
283        """Get a sign-in token from the adapter back end.
284
285        This token is then passed to the master-server to complete the
286        login process.
287        The adapter can use this opportunity to bring up account creation
288        UI, call its internal sign_in function, etc. as needed.
289        The provided completion_cb should then be called with either a token
290        or None if sign in failed or was cancelled.
291        """
292        from babase._general import Call
293
294        # Default implementation simply fails immediately.
295        _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 login 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:
36    @dataclass
37    class SignInResult:
38        """Describes the final result of a sign-in attempt."""
39
40        credentials: str

Describes the final result of a sign-in attempt.

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

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

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

Category: User Interface Classes

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

Exception raised when a referenced object does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
def open_url(address: str, force_internal: bool = False) -> None:
453def open_url(address: str, force_internal: bool = False) -> None:
454    """Open a provided URL.
455
456    Category: **General Utility Functions**
457
458    Open the provided url in a web-browser, or display the URL
459    string in a window if that isn't possible (or if force_internal
460    is True).
461    """
462    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):
101class Permission(Enum):
102    """Permissions that can be requested from the OS.
103
104    Category: Enums
105    """
106
107    STORAGE = 0

Permissions that can be requested from the OS.

Category: Enums

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

Called when the app reaches the running state.

def on_app_pause(self) -> None:
331    def on_app_pause(self) -> None:
332        """Called when the app is switching to a paused state."""

Called when the app is switching to a paused state.

def on_app_resume(self) -> None:
334    def on_app_resume(self) -> None:
335        """Called when the app is resuming from a paused state."""

Called when the app is resuming from a paused state.

def on_app_shutdown(self) -> None:
337    def on_app_shutdown(self) -> None:
338        """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:
340    def on_app_shutdown_complete(self) -> None:
341        """Called when the app has completed the shutdown process."""

Called when the app has completed the shutdown process.

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

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

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

Called to show our settings UI.

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

Represents a plugin the engine knows about.

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)
240    def __init__(self, class_path: str, loadable: bool):
241        self.class_path = class_path
242        self.loadable = loadable
243        self.attempted_load = False
244        self.plugin: Plugin | None = None
class_path
loadable
attempted_load
plugin: Plugin | None
enabled: bool

Whether the user wants this plugin to load.

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

Possibly load the plugin and log any errors.

def pushcall( call: Callable, from_other_thread: bool = False, suppress_other_thread_warning: bool = False, other_thread_use_fg_context: bool = False, raw: bool = False) -> None:
1208def pushcall(
1209    call: Callable,
1210    from_other_thread: bool = False,
1211    suppress_other_thread_warning: bool = False,
1212    other_thread_use_fg_context: bool = False,
1213    raw: bool = False,
1214) -> None:
1215    """Push a call to the logic event-loop.
1216    Category: **General Utility Functions**
1217
1218    This call expects to be used in the logic thread, and will automatically
1219    save and restore the babase.Context to behave seamlessly.
1220
1221    If you want to push a call from outside of the logic thread,
1222    however, you can pass 'from_other_thread' as True. In this case
1223    the call will always run in the UI context_ref on the logic thread
1224    or whichever context_ref is in the foreground if
1225    other_thread_use_fg_context is True.
1226    Passing raw=True will disable thread checks and context_ref sets/restores.
1227    """
1228    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(soft: bool = True, back: bool = False) -> None:
1232def quit(soft: bool = True, back: bool = False) -> None:
1233    """Quit the app.
1234
1235    Category: **General Utility Functions**
1236
1237    On platforms such as mobile, a 'soft' quit may background and/or reset
1238    the app but keep it running. A 'back' quit is a special form of soft
1239    quit that may trigger different behavior in the OS. On Android, for
1240    example, a back-quit may simply jump to the previous Android activity,
1241    while a regular soft quit may just exit the current activity and dump
1242    the user at their home screen.
1243    """
1244    return None

Quit the app.

Category: General Utility Functions

On platforms such as mobile, a 'soft' quit may background and/or reset the app but keep it running. A 'back' quit is a special form of soft quit that may trigger different behavior in the OS. On Android, for example, a back-quit may simply jump to the previous Android activity, while a regular soft quit may just exit the current activity and dump the user at their home screen.

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:
465def rowwidget(
466    edit: bauiv1.Widget | None = None,
467    parent: bauiv1.Widget | None = None,
468    size: Sequence[float] | None = None,
469    position: Sequence[float] | None = None,
470    background: bool | None = None,
471    selected_child: bauiv1.Widget | None = None,
472    visible_child: bauiv1.Widget | None = None,
473    claims_left_right: bool | None = None,
474    claims_tab: bool | None = None,
475    selection_loops_to_parent: bool | None = None,
476) -> bauiv1.Widget:
477    """Create or edit a row widget.
478
479    Category: **User Interface Functions**
480
481    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
482    a new one is created and returned. Arguments that are not set to None
483    are applied to the Widget.
484    """
485    import bauiv1  # pylint: disable=cyclic-import
486
487    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, ...]:
1282def safecolor(
1283    color: Sequence[float], target_intensity: float = 0.6
1284) -> tuple[float, ...]:
1285    """Given a color tuple, return a color safe to display as text.
1286
1287    Category: **General Utility Functions**
1288
1289    Accepts tuples of length 3 or 4. This will slightly brighten very
1290    dark colors, etc.
1291    """
1292    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:
1295def screenmessage(
1296    message: str | babase.Lstr,
1297    color: Sequence[float] | None = None,
1298    log: bool = False,
1299) -> None:
1300    """Print a message to the local client's screen, in a given color.
1301
1302    Category: **General Utility Functions**
1303
1304    Note that this version of the function is purely for local display.
1305    To broadcast screen messages in network play, look for methods such as
1306    broadcastmessage() provided by the scene-version packages.
1307    """
1308    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:
490def scrollwidget(
491    edit: bauiv1.Widget | None = None,
492    parent: bauiv1.Widget | None = None,
493    size: Sequence[float] | None = None,
494    position: Sequence[float] | None = None,
495    background: bool | None = None,
496    selected_child: bauiv1.Widget | None = None,
497    capture_arrows: bool = False,
498    on_select_call: Callable | None = None,
499    center_small_content: bool | None = None,
500    color: Sequence[float] | None = None,
501    highlight: bool | None = None,
502    border_opacity: float | None = None,
503    simple_culling_v: float | None = None,
504    selection_loops_to_parent: bool | None = None,
505    claims_left_right: bool | None = None,
506    claims_up_down: bool | None = None,
507    claims_tab: bool | None = None,
508    autoselect: bool | None = None,
509) -> bauiv1.Widget:
510    """Create or edit a scroll widget.
511
512    Category: **User Interface Functions**
513
514    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
515    a new one is created and returned. Arguments that are not set to None
516    are applied to the Widget.
517    """
518    import bauiv1  # pylint: disable=cyclic-import
519
520    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:
1311def set_analytics_screen(screen: str) -> None:
1312    """Used for analytics to see where in the app players spend their time.
1313
1314    Category: **General Utility Functions**
1315
1316    Generally called when opening a new window or entering some UI.
1317    'screen' should be a string description of an app location
1318    ('Main Menu', etc.)
1319    """
1320    return None

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

Category: General Utility Functions

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

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

Category: User Interface Classes

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

Play the sound locally.

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

Stop the sound if it is playing.

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

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>
OUYA_BUTTON_O = <SpecialChar.OUYA_BUTTON_O: 16>
OUYA_BUTTON_U = <SpecialChar.OUYA_BUTTON_U: 17>
OUYA_BUTTON_Y = <SpecialChar.OUYA_BUTTON_Y: 18>
OUYA_BUTTON_A = <SpecialChar.OUYA_BUTTON_A: 19>
TICKET = <SpecialChar.TICKET: 22>
DICE_BUTTON1 = <SpecialChar.DICE_BUTTON1: 25>
DICE_BUTTON2 = <SpecialChar.DICE_BUTTON2: 26>
DICE_BUTTON3 = <SpecialChar.DICE_BUTTON3: 27>
DICE_BUTTON4 = <SpecialChar.DICE_BUTTON4: 28>
PARTY_ICON = <SpecialChar.PARTY_ICON: 30>
TEST_ACCOUNT = <SpecialChar.TEST_ACCOUNT: 31>
TICKET_BACKING = <SpecialChar.TICKET_BACKING: 32>
TROPHY1 = <SpecialChar.TROPHY1: 33>
TROPHY2 = <SpecialChar.TROPHY2: 34>
TROPHY3 = <SpecialChar.TROPHY3: 35>
TROPHY0A = <SpecialChar.TROPHY0A: 36>
TROPHY0B = <SpecialChar.TROPHY0B: 37>
TROPHY4 = <SpecialChar.TROPHY4: 38>
LOCAL_ACCOUNT = <SpecialChar.LOCAL_ACCOUNT: 39>
FLAG_UNITED_STATES = <SpecialChar.FLAG_UNITED_STATES: 41>
FLAG_MEXICO = <SpecialChar.FLAG_MEXICO: 42>
FLAG_GERMANY = <SpecialChar.FLAG_GERMANY: 43>
FLAG_BRAZIL = <SpecialChar.FLAG_BRAZIL: 44>
FLAG_RUSSIA = <SpecialChar.FLAG_RUSSIA: 45>
FLAG_CHINA = <SpecialChar.FLAG_CHINA: 46>
FLAG_UNITED_KINGDOM = <SpecialChar.FLAG_UNITED_KINGDOM: 47>
FLAG_CANADA = <SpecialChar.FLAG_CANADA: 48>
FLAG_INDIA = <SpecialChar.FLAG_INDIA: 49>
FLAG_JAPAN = <SpecialChar.FLAG_JAPAN: 50>
FLAG_FRANCE = <SpecialChar.FLAG_FRANCE: 51>
FLAG_INDONESIA = <SpecialChar.FLAG_INDONESIA: 52>
FLAG_ITALY = <SpecialChar.FLAG_ITALY: 53>
FLAG_SOUTH_KOREA = <SpecialChar.FLAG_SOUTH_KOREA: 54>
FLAG_NETHERLANDS = <SpecialChar.FLAG_NETHERLANDS: 55>
FEDORA = <SpecialChar.FEDORA: 56>
HAL = <SpecialChar.HAL: 57>
CROWN = <SpecialChar.CROWN: 58>
YIN_YANG = <SpecialChar.YIN_YANG: 59>
EYE_BALL = <SpecialChar.EYE_BALL: 60>
SKULL = <SpecialChar.SKULL: 61>
HEART = <SpecialChar.HEART: 62>
DRAGON = <SpecialChar.DRAGON: 63>
HELMET = <SpecialChar.HELMET: 64>
MUSHROOM = <SpecialChar.MUSHROOM: 65>
NINJA_STAR = <SpecialChar.NINJA_STAR: 66>
VIKING_HELMET = <SpecialChar.VIKING_HELMET: 67>
MOON = <SpecialChar.MOON: 68>
SPIDER = <SpecialChar.SPIDER: 69>
FIREBALL = <SpecialChar.FIREBALL: 70>
FLAG_UNITED_ARAB_EMIRATES = <SpecialChar.FLAG_UNITED_ARAB_EMIRATES: 71>
FLAG_QATAR = <SpecialChar.FLAG_QATAR: 72>
FLAG_EGYPT = <SpecialChar.FLAG_EGYPT: 73>
FLAG_KUWAIT = <SpecialChar.FLAG_KUWAIT: 74>
FLAG_ALGERIA = <SpecialChar.FLAG_ALGERIA: 75>
FLAG_SAUDI_ARABIA = <SpecialChar.FLAG_SAUDI_ARABIA: 76>
FLAG_MALAYSIA = <SpecialChar.FLAG_MALAYSIA: 77>
FLAG_CZECH_REPUBLIC = <SpecialChar.FLAG_CZECH_REPUBLIC: 78>
FLAG_AUSTRALIA = <SpecialChar.FLAG_AUSTRALIA: 79>
FLAG_SINGAPORE = <SpecialChar.FLAG_SINGAPORE: 80>
FLAG_IRAN = <SpecialChar.FLAG_IRAN: 84>
FLAG_POLAND = <SpecialChar.FLAG_POLAND: 85>
FLAG_ARGENTINA = <SpecialChar.FLAG_ARGENTINA: 86>
FLAG_PHILIPPINES = <SpecialChar.FLAG_PHILIPPINES: 87>
FLAG_CHILE = <SpecialChar.FLAG_CHILE: 88>
MIKIROG = <SpecialChar.MIKIROG: 89>
Inherited Members
enum.Enum
name
value
class Texture:
69class Texture:
70    """Category: **User Interface Classes**"""
71
72    pass

Category: User Interface Classes

def textwidget( edit: Widget | None = None, parent: Widget | None = None, size: Optional[Sequence[float]] = None, position: Optional[Sequence[float]] = None, text: str | Lstr | None = None, v_align: str | None = None, h_align: str | None = None, editable: bool | None = None, padding: float | None = None, on_return_press_call: Optional[Callable[[], NoneType]] = None, on_activate_call: Optional[Callable[[], NoneType]] = None, selectable: bool | None = None, query: Widget | None = None, max_chars: int | None = None, color: Optional[Sequence[float]] = None, click_activate: bool | None = None, on_select_call: Optional[Callable[[], NoneType]] = None, always_highlight: bool | None = None, draw_controller: Widget | None = None, scale: float | None = None, corner_scale: float | None = None, description: str | Lstr | None = None, transition_delay: float | None = None, maxwidth: float | None = None, max_height: float | None = None, flatness: float | None = None, shadow: float | None = None, autoselect: bool | None = None, rotate: float | None = None, enabled: bool | None = None, force_internal_editing: bool | None = None, always_show_carat: bool | None = None, big: bool | None = None, extra_touch_border_scale: float | None = None, res_scale: float | None = None, query_max_chars: Widget | None = None, query_description: Widget | None = None, adapter_finished: bool | None = None) -> Widget:
556def textwidget(
557    edit: bauiv1.Widget | None = None,
558    parent: bauiv1.Widget | None = None,
559    size: Sequence[float] | None = None,
560    position: Sequence[float] | None = None,
561    text: str | bauiv1.Lstr | None = None,
562    v_align: str | None = None,
563    h_align: str | None = None,
564    editable: bool | None = None,
565    padding: float | None = None,
566    on_return_press_call: Callable[[], None] | None = None,
567    on_activate_call: Callable[[], None] | None = None,
568    selectable: bool | None = None,
569    query: bauiv1.Widget | None = None,
570    max_chars: int | None = None,
571    color: Sequence[float] | None = None,
572    click_activate: bool | None = None,
573    on_select_call: Callable[[], None] | None = None,
574    always_highlight: bool | None = None,
575    draw_controller: bauiv1.Widget | None = None,
576    scale: float | None = None,
577    corner_scale: float | None = None,
578    description: str | bauiv1.Lstr | None = None,
579    transition_delay: float | None = None,
580    maxwidth: float | None = None,
581    max_height: float | None = None,
582    flatness: float | None = None,
583    shadow: float | None = None,
584    autoselect: bool | None = None,
585    rotate: float | None = None,
586    enabled: bool | None = None,
587    force_internal_editing: bool | None = None,
588    always_show_carat: bool | None = None,
589    big: bool | None = None,
590    extra_touch_border_scale: float | None = None,
591    res_scale: float | None = None,
592    query_max_chars: bauiv1.Widget | None = None,
593    query_description: bauiv1.Widget | None = None,
594    adapter_finished: bool | None = None,
595) -> bauiv1.Widget:
596    """Create or edit a text widget.
597
598    Category: **User Interface Functions**
599
600    Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise
601    a new one is created and returned. Arguments that are not set to None
602    are applied to the Widget.
603    """
604    import bauiv1  # pylint: disable=cyclic-import
605
606    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:
175def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None:
176    """Add a check to ensure a widget-owning object gets cleaned up properly.
177
178    Category: User Interface Functions
179
180    This adds a check which will print an error message if the provided
181    object still exists ~5 seconds after the provided bauiv1.Widget dies.
182
183    This is a good sanity check for any sort of object that wraps or
184    controls a bauiv1.Widget. For instance, a 'Window' class instance has
185    no reason to still exist once its root container bauiv1.Widget has fully
186    transitioned out and been destroyed. Circular references or careless
187    strong referencing can lead to such objects never getting destroyed,
188    however, and this helps detect such cases to avoid memory leaks.
189    """
190    if DEBUG_UI_CLEANUP_CHECKS:
191        print(f'adding uicleanup to {obj}')
192    if not isinstance(widget, _bauiv1.Widget):
193        raise TypeError('widget arg is not a bauiv1.Widget')
194
195    if bool(False):
196
197        def foobar() -> None:
198            """Just testing."""
199            if DEBUG_UI_CLEANUP_CHECKS:
200                print('uicleanupcheck widget dying...')
201
202        widget.add_delete_callback(foobar)
203
204    assert babase.app.classic is not None
205    babase.app.ui_v1.cleanupchecks.append(
206        UICleanupCheck(
207            obj=weakref.ref(obj), widget=widget, widget_death_time=None
208        )
209    )

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):
42class UIScale(Enum):
43    """The overall scale the UI is being rendered for. Note that this is
44    independent of pixel resolution. For example, a phone and a desktop PC
45    might render the game at similar pixel resolutions but the size they
46    display content at will vary significantly.
47
48    Category: Enums
49
50    'large' is used for devices such as desktop PCs where fine details can
51       be clearly seen. UI elements are generally smaller on the screen
52       and more content can be seen at once.
53
54    'medium' is used for devices such as tablets, TVs, or VR headsets.
55       This mode strikes a balance between clean readability and amount of
56       content visible.
57
58    'small' is used primarily for phones or other small devices where
59       content needs to be presented as large and clear in order to remain
60       readable from an average distance.
61    """
62
63    LARGE = 0
64    MEDIUM = 1
65    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):
 21class UIV1Subsystem(babase.AppSubsystem):
 22    """Consolidated UI functionality for the app.
 23
 24    Category: **App Classes**
 25
 26    To use this class, access the single instance of it at 'ba.app.ui'.
 27    """
 28
 29    def __init__(self) -> None:
 30        super().__init__()
 31        env = babase.env()
 32
 33        self.controller: UIController | None = None
 34
 35        self._main_menu_window: bauiv1.Widget | None = None
 36        self._main_menu_location: str | None = None
 37        self.quit_window: bauiv1.Widget | None = None
 38
 39        # From classic.
 40        self.main_menu_resume_callbacks: list = []  # Can probably go away.
 41
 42        self._uiscale: babase.UIScale
 43
 44        interfacetype = env['ui_scale']
 45        if interfacetype == 'large':
 46            self._uiscale = babase.UIScale.LARGE
 47        elif interfacetype == 'medium':
 48            self._uiscale = babase.UIScale.MEDIUM
 49        elif interfacetype == 'small':
 50            self._uiscale = babase.UIScale.SMALL
 51        else:
 52            raise RuntimeError(f'Invalid UIScale value: {interfacetype}')
 53
 54        self.window_states: dict[type, Any] = {}  # FIXME: Kill this.
 55        self.main_menu_selection: str | None = None  # FIXME: Kill this.
 56        self.have_party_queue_window = False
 57        self.cleanupchecks: list[UICleanupCheck] = []
 58        self.upkeeptimer: babase.AppTimer | None = None
 59        self.use_toolbars = _bauiv1.toolbar_test()
 60
 61        self.title_color = (0.72, 0.7, 0.75)
 62        self.heading_color = (0.72, 0.7, 0.75)
 63        self.infotextcolor = (0.7, 0.9, 0.7)
 64
 65        # Switch our overall game selection UI flow between Play and
 66        # Private-party playlist selection modes; should do this in
 67        # a more elegant way once we revamp high level UI stuff a bit.
 68        self.selecting_private_party_playlist: bool = False
 69
 70    @property
 71    def uiscale(self) -> babase.UIScale:
 72        """Current ui scale for the app."""
 73        return self._uiscale
 74
 75    def on_app_loading(self) -> None:
 76        from bauiv1._uitypes import UIController, ui_upkeep
 77
 78        # IMPORTANT: If tweaking UI stuff, make sure it behaves for small,
 79        # medium, and large UI modes. (doesn't run off screen, etc).
 80        # The overrides below can be used to test with different sizes.
 81        # Generally small is used on phones, medium is used on tablets/tvs,
 82        # and large is on desktop computers or perhaps large tablets. When
 83        # possible, run in windowed mode and resize the window to assure
 84        # this holds true at all aspect ratios.
 85
 86        # UPDATE: A better way to test this is now by setting the environment
 87        # variable BA_UI_SCALE to "small", "medium", or "large".
 88        # This will affect system UIs not covered by the values below such
 89        # as screen-messages. The below values remain functional, however,
 90        # for cases such as Android where environment variables can't be set
 91        # easily.
 92
 93        if bool(False):  # force-test ui scale
 94            self._uiscale = babase.UIScale.SMALL
 95            with babase.ContextRef.empty():
 96                babase.pushcall(
 97                    lambda: babase.screenmessage(
 98                        f'FORCING UISCALE {self._uiscale.name} FOR TESTING',
 99                        color=(1, 0, 1),
100                        log=True,
101                    )
102                )
103
104        self.controller = UIController()
105
106        # Kick off our periodic UI upkeep.
107        # FIXME: Can probably kill this if we do immediate UI death checks.
108        self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True)
109
110    def set_main_menu_window(self, window: bauiv1.Widget) -> None:
111        """Set the current 'main' window, replacing any existing."""
112        existing = self._main_menu_window
113        from inspect import currentframe, getframeinfo
114
115        # Let's grab the location where we were called from to report
116        # if we have to force-kill the existing window (which normally
117        # should not happen).
118        frameline = None
119        try:
120            frame = currentframe()
121            if frame is not None:
122                frame = frame.f_back
123            if frame is not None:
124                frameinfo = getframeinfo(frame)
125                frameline = f'{frameinfo.filename} {frameinfo.lineno}'
126        except Exception:
127            logging.exception('Error calcing line for set_main_menu_window')
128
129        # With our legacy main-menu system, the caller is responsible for
130        # clearing out the old main menu window when assigning the new.
131        # However there are corner cases where that doesn't happen and we get
132        # old windows stuck under the new main one. So let's guard against
133        # that. However, we can't simply delete the existing main window when
134        # a new one is assigned because the user may transition the old out
135        # *after* the assignment. Sigh. So, as a happy medium, let's check in
136        # on the old after a short bit of time and kill it if its still alive.
137        # That will be a bit ugly on screen but at least should un-break
138        # things.
139        def _delay_kill() -> None:
140            import time
141
142            if existing:
143                print(
144                    f'Killing old main_menu_window'
145                    f' when called at: {frameline} t={time.time():.3f}'
146                )
147                existing.delete()
148
149        babase.apptimer(1.0, _delay_kill)
150        self._main_menu_window = window
151
152    def clear_main_menu_window(self, transition: str | None = None) -> None:
153        """Clear any existing 'main' window with the provided transition."""
154        if self._main_menu_window:
155            if transition is not None:
156                _bauiv1.containerwidget(
157                    edit=self._main_menu_window, transition=transition
158                )
159            else:
160                self._main_menu_window.delete()
161
162    def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None:
163        """(internal)"""
164
165        # If there's no main menu up, just call immediately.
166        if not self.has_main_menu_window():
167            with babase.ContextRef.empty():
168                call()
169        else:
170            self.main_menu_resume_callbacks.append(call)
171
172    def has_main_menu_window(self) -> bool:
173        """Return whether a main menu window is present."""
174        return bool(self._main_menu_window)
175
176    def set_main_menu_location(self, location: str) -> None:
177        """Set the location represented by the current main menu window."""
178        self._main_menu_location = location
179
180    def get_main_menu_location(self) -> str | None:
181        """Return the current named main menu location, if any."""
182        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
uiscale: UIScale

Current ui scale for the app.

def on_app_loading(self) -> None:
 75    def on_app_loading(self) -> None:
 76        from bauiv1._uitypes import UIController, ui_upkeep
 77
 78        # IMPORTANT: If tweaking UI stuff, make sure it behaves for small,
 79        # medium, and large UI modes. (doesn't run off screen, etc).
 80        # The overrides below can be used to test with different sizes.
 81        # Generally small is used on phones, medium is used on tablets/tvs,
 82        # and large is on desktop computers or perhaps large tablets. When
 83        # possible, run in windowed mode and resize the window to assure
 84        # this holds true at all aspect ratios.
 85
 86        # UPDATE: A better way to test this is now by setting the environment
 87        # variable BA_UI_SCALE to "small", "medium", or "large".
 88        # This will affect system UIs not covered by the values below such
 89        # as screen-messages. The below values remain functional, however,
 90        # for cases such as Android where environment variables can't be set
 91        # easily.
 92
 93        if bool(False):  # force-test ui scale
 94            self._uiscale = babase.UIScale.SMALL
 95            with babase.ContextRef.empty():
 96                babase.pushcall(
 97                    lambda: babase.screenmessage(
 98                        f'FORCING UISCALE {self._uiscale.name} FOR TESTING',
 99                        color=(1, 0, 1),
100                        log=True,
101                    )
102                )
103
104        self.controller = UIController()
105
106        # Kick off our periodic UI upkeep.
107        # FIXME: Can probably kill this if we do immediate UI death checks.
108        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) -> None:
110    def set_main_menu_window(self, window: bauiv1.Widget) -> None:
111        """Set the current 'main' window, replacing any existing."""
112        existing = self._main_menu_window
113        from inspect import currentframe, getframeinfo
114
115        # Let's grab the location where we were called from to report
116        # if we have to force-kill the existing window (which normally
117        # should not happen).
118        frameline = None
119        try:
120            frame = currentframe()
121            if frame is not None:
122                frame = frame.f_back
123            if frame is not None:
124                frameinfo = getframeinfo(frame)
125                frameline = f'{frameinfo.filename} {frameinfo.lineno}'
126        except Exception:
127            logging.exception('Error calcing line for set_main_menu_window')
128
129        # With our legacy main-menu system, the caller is responsible for
130        # clearing out the old main menu window when assigning the new.
131        # However there are corner cases where that doesn't happen and we get
132        # old windows stuck under the new main one. So let's guard against
133        # that. However, we can't simply delete the existing main window when
134        # a new one is assigned because the user may transition the old out
135        # *after* the assignment. Sigh. So, as a happy medium, let's check in
136        # on the old after a short bit of time and kill it if its still alive.
137        # That will be a bit ugly on screen but at least should un-break
138        # things.
139        def _delay_kill() -> None:
140            import time
141
142            if existing:
143                print(
144                    f'Killing old main_menu_window'
145                    f' when called at: {frameline} t={time.time():.3f}'
146                )
147                existing.delete()
148
149        babase.apptimer(1.0, _delay_kill)
150        self._main_menu_window = window

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

def clear_main_menu_window(self, transition: str | None = None) -> None:
152    def clear_main_menu_window(self, transition: str | None = None) -> None:
153        """Clear any existing 'main' window with the provided transition."""
154        if self._main_menu_window:
155            if transition is not None:
156                _bauiv1.containerwidget(
157                    edit=self._main_menu_window, transition=transition
158                )
159            else:
160                self._main_menu_window.delete()

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

def has_main_menu_window(self) -> bool:
172    def has_main_menu_window(self) -> bool:
173        """Return whether a main menu window is present."""
174        return bool(self._main_menu_window)

Return whether a main menu window is present.

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

Set the location represented by the current main menu window.

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

Return the current named main menu location, if any.

Inherited Members
babase._appsubsystem.AppSubsystem
on_app_running
on_app_pause
on_app_resume
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:
625def widget(
626    edit: bauiv1.Widget | None = None,
627    up_widget: bauiv1.Widget | None = None,
628    down_widget: bauiv1.Widget | None = None,
629    left_widget: bauiv1.Widget | None = None,
630    right_widget: bauiv1.Widget | None = None,
631    show_buffer_top: float | None = None,
632    show_buffer_bottom: float | None = None,
633    show_buffer_left: float | None = None,
634    show_buffer_right: float | None = None,
635    autoselect: bool | None = None,
636) -> None:
637    """Edit common attributes of any widget.
638
639    Category: **User Interface Functions**
640
641    Unlike other UI calls, this can only be used to edit, not to create.
642    """
643    return None

Edit common attributes of any widget.

Category: User Interface Functions

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

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

def activate(self) -> None:
89    def activate(self) -> None:
90        """Activates a widget; the same as if it had been clicked."""
91        return None

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

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

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

def delete(self, ignore_missing: bool = True) -> None:
 97    def delete(self, ignore_missing: bool = True) -> None:
 98        """Delete the Widget. Ignores already-deleted Widgets if ignore_missing
 99        is True; otherwise an Exception is thrown.
100        """
101        return None

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

def exists(self) -> bool:
103    def exists(self) -> bool:
104        """Returns whether the Widget still exists.
105        Most functionality will fail on a nonexistent widget.
106
107        Note that you can also use the boolean operator for this same
108        functionality, so a statement such as "if mywidget" will do
109        the right thing both for Widget objects and values of None.
110        """
111        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]:
113    def get_children(self) -> list[bauiv1.Widget]:
114        """Returns any child Widgets of this Widget."""
115        import bauiv1
116
117        return [bauiv1.Widget()]

Returns any child Widgets of this Widget.

def get_screen_space_center(self) -> tuple[float, float]:
119    def get_screen_space_center(self) -> tuple[float, float]:
120        """Returns the coords of the bauiv1.Widget center relative to the center
121        of the screen. This can be useful for placing pop-up windows and other
122        special cases.
123        """
124        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:
126    def get_selected_child(self) -> bauiv1.Widget | None:
127        """Returns the selected child Widget or None if nothing is selected."""
128        import bauiv1
129
130        return bauiv1.Widget()

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

def get_widget_type(self) -> str:
132    def get_widget_type(self) -> str:
133        """Return the internal type of the Widget as a string. Note that this
134        is different from the Python bauiv1.Widget type, which is the same for
135        all widgets.
136        """
137        return str()

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

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

A basic window.

Category: User Interface Classes

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

Return the root widget.