bauiv1lib.settings.advanced

UI functionality for advanced settings.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""UI functionality for advanced settings."""
  4
  5from __future__ import annotations
  6
  7import os
  8import logging
  9from typing import TYPE_CHECKING
 10
 11from bauiv1lib.popup import PopupMenu
 12import bauiv1 as bui
 13
 14if TYPE_CHECKING:
 15    from typing import Any
 16
 17
 18class AdvancedSettingsWindow(bui.Window):
 19    """Window for editing advanced app settings."""
 20
 21    def __init__(
 22        self,
 23        transition: str = 'in_right',
 24        origin_widget: bui.Widget | None = None,
 25    ):
 26        # pylint: disable=too-many-statements
 27        import threading
 28
 29        if bui.app.classic is None:
 30            raise RuntimeError('This requires classic support.')
 31
 32        # Preload some modules we use in a background thread so we won't
 33        # have a visual hitch when the user taps them.
 34        threading.Thread(target=self._preload_modules).start()
 35
 36        app = bui.app
 37        assert app.classic is not None
 38
 39        # If they provided an origin-widget, scale up from that.
 40        scale_origin: tuple[float, float] | None
 41        if origin_widget is not None:
 42            self._transition_out = 'out_scale'
 43            scale_origin = origin_widget.get_screen_space_center()
 44            transition = 'in_scale'
 45        else:
 46            self._transition_out = 'out_right'
 47            scale_origin = None
 48
 49        uiscale = bui.app.ui_v1.uiscale
 50        self._width = 970.0 if uiscale is bui.UIScale.SMALL else 670.0
 51        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 52        self._height = (
 53            390.0
 54            if uiscale is bui.UIScale.SMALL
 55            else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0
 56        )
 57        self._lang_status_text: bui.Widget | None = None
 58
 59        self._spacing = 32
 60        self._menu_open = False
 61        top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
 62
 63        super().__init__(
 64            root_widget=bui.containerwidget(
 65                size=(self._width, self._height + top_extra),
 66                transition=transition,
 67                toolbar_visibility='menu_minimal',
 68                scale_origin_stack_offset=scale_origin,
 69                scale=(
 70                    2.06
 71                    if uiscale is bui.UIScale.SMALL
 72                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 73                ),
 74                stack_offset=(
 75                    (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0)
 76                ),
 77            )
 78        )
 79
 80        self._prev_lang = ''
 81        self._prev_lang_list: list[str] = []
 82        self._complete_langs_list: list | None = None
 83        self._complete_langs_error = False
 84        self._language_popup: PopupMenu | None = None
 85
 86        # In vr-mode, the internal keyboard is currently the *only* option,
 87        # so no need to show this.
 88        self._show_always_use_internal_keyboard = not app.env.vr
 89
 90        self._scroll_width = self._width - (100 + 2 * x_inset)
 91        self._scroll_height = self._height - 115.0
 92        self._sub_width = self._scroll_width * 0.95
 93        self._sub_height = 870.0
 94
 95        if self._show_always_use_internal_keyboard:
 96            self._sub_height += 62
 97
 98        self._show_disable_gyro = app.classic.platform in {'ios', 'android'}
 99        if self._show_disable_gyro:
100            self._sub_height += 42
101
102        self._do_vr_test_button = app.env.vr
103        self._do_net_test_button = True
104        self._extra_button_spacing = self._spacing * 2.5
105
106        if self._do_vr_test_button:
107            self._sub_height += self._extra_button_spacing
108        if self._do_net_test_button:
109            self._sub_height += self._extra_button_spacing
110        self._sub_height += self._spacing * 2.0  # plugins
111        self._sub_height += self._spacing * 2.0  # dev tools
112
113        self._r = 'settingsWindowAdvanced'
114
115        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
116            bui.containerwidget(
117                edit=self._root_widget, on_cancel_call=self._do_back
118            )
119            self._back_button = None
120        else:
121            self._back_button = bui.buttonwidget(
122                parent=self._root_widget,
123                position=(53 + x_inset, self._height - 60),
124                size=(140, 60),
125                scale=0.8,
126                autoselect=True,
127                label=bui.Lstr(resource='backText'),
128                button_type='back',
129                on_activate_call=self._do_back,
130            )
131            bui.containerwidget(
132                edit=self._root_widget, cancel_button=self._back_button
133            )
134
135        self._title_text = bui.textwidget(
136            parent=self._root_widget,
137            position=(0, self._height - 52),
138            size=(self._width, 25),
139            text=bui.Lstr(resource=f'{self._r}.titleText'),
140            color=app.ui_v1.title_color,
141            h_align='center',
142            v_align='top',
143        )
144
145        if self._back_button is not None:
146            bui.buttonwidget(
147                edit=self._back_button,
148                button_type='backSmall',
149                size=(60, 60),
150                label=bui.charstr(bui.SpecialChar.BACK),
151            )
152
153        self._scrollwidget = bui.scrollwidget(
154            parent=self._root_widget,
155            position=(50 + x_inset, 50),
156            simple_culling_v=20.0,
157            highlight=False,
158            size=(self._scroll_width, self._scroll_height),
159            selection_loops_to_parent=True,
160        )
161        bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
162        self._subcontainer = bui.containerwidget(
163            parent=self._scrollwidget,
164            size=(self._sub_width, self._sub_height),
165            background=False,
166            selection_loops_to_parent=True,
167        )
168
169        self._rebuild()
170
171        # Rebuild periodically to pick up language changes/additions/etc.
172        self._rebuild_timer = bui.AppTimer(
173            1.0, bui.WeakCall(self._rebuild), repeat=True
174        )
175
176        # Fetch the list of completed languages.
177        bui.app.classic.master_server_v1_get(
178            'bsLangGetCompleted',
179            {'b': app.env.engine_build_number},
180            callback=bui.WeakCall(self._completed_langs_cb),
181        )
182
183    # noinspection PyUnresolvedReferences
184    @staticmethod
185    def _preload_modules() -> None:
186        """Preload modules we use; avoids hitches (called in bg thread)."""
187        from babase import modutils as _unused2
188        from bauiv1lib import config as _unused1
189        from bauiv1lib.settings import vrtesting as _unused3
190        from bauiv1lib.settings import nettesting as _unused4
191        from bauiv1lib import appinvite as _unused5
192        from bauiv1lib import account as _unused6
193        from bauiv1lib import sendinfo as _unused7
194        from bauiv1lib import debug as _unused8
195        from bauiv1lib.settings import plugins as _unused9
196        from bauiv1lib.settings import devtools as _unused10
197
198    def _update_lang_status(self) -> None:
199        if self._complete_langs_list is not None:
200            up_to_date = bui.app.lang.language in self._complete_langs_list
201            bui.textwidget(
202                edit=self._lang_status_text,
203                text=(
204                    ''
205                    if bui.app.lang.language == 'Test'
206                    else (
207                        bui.Lstr(
208                            resource=f'{self._r}.translationNoUpdateNeededText'
209                        )
210                        if up_to_date
211                        else bui.Lstr(
212                            resource=f'{self._r}.translationUpdateNeededText'
213                        )
214                    )
215                ),
216                color=(
217                    (0.2, 1.0, 0.2, 0.8) if up_to_date else (1.0, 0.2, 0.2, 0.8)
218                ),
219            )
220        else:
221            bui.textwidget(
222                edit=self._lang_status_text,
223                text=(
224                    bui.Lstr(resource=f'{self._r}.translationFetchErrorText')
225                    if self._complete_langs_error
226                    else bui.Lstr(
227                        resource=f'{self._r}.translationFetchingStatusText'
228                    )
229                ),
230                color=(
231                    (1.0, 0.5, 0.2)
232                    if self._complete_langs_error
233                    else (0.7, 0.7, 0.7)
234                ),
235            )
236
237    def _rebuild(self) -> None:
238        # pylint: disable=too-many-statements
239        # pylint: disable=too-many-branches
240        # pylint: disable=too-many-locals
241
242        from bauiv1lib.config import ConfigCheckBox
243        from babase.modutils import show_user_scripts
244
245        plus = bui.app.plus
246        assert plus is not None
247
248        available_languages = bui.app.lang.available_languages
249
250        # Don't rebuild if the menu is open or if our language and
251        # language-list hasn't changed.
252
253        # NOTE - although we now support widgets updating their own
254        # translations, we still change the label formatting on the language
255        # menu based on the language so still need this. ...however we could
256        # make this more limited to it only rebuilds that one menu instead
257        # of everything.
258        if self._menu_open or (
259            self._prev_lang == bui.app.config.get('Lang', None)
260            and self._prev_lang_list == available_languages
261        ):
262            return
263        self._prev_lang = bui.app.config.get('Lang', None)
264        self._prev_lang_list = available_languages
265
266        # Clear out our sub-container.
267        children = self._subcontainer.get_children()
268        for child in children:
269            child.delete()
270
271        v = self._sub_height - 35
272
273        v -= self._spacing * 1.2
274
275        # Update our existing back button and title.
276        if self._back_button is not None:
277            bui.buttonwidget(
278                edit=self._back_button, label=bui.Lstr(resource='backText')
279            )
280            bui.buttonwidget(
281                edit=self._back_button, label=bui.charstr(bui.SpecialChar.BACK)
282            )
283
284        bui.textwidget(
285            edit=self._title_text,
286            text=bui.Lstr(resource=f'{self._r}.titleText'),
287        )
288
289        this_button_width = 410
290
291        assert bui.app.classic is not None
292        bui.textwidget(
293            parent=self._subcontainer,
294            position=(70, v + 10),
295            size=(0, 0),
296            text=bui.Lstr(resource=f'{self._r}.languageText'),
297            maxwidth=150,
298            scale=1.2,
299            color=bui.app.ui_v1.title_color,
300            h_align='left',
301            v_align='center',
302        )
303
304        languages = bui.app.lang.available_languages
305        cur_lang = bui.app.config.get('Lang', None)
306        if cur_lang is None:
307            cur_lang = 'Auto'
308
309        # We have a special dict of language names in that language
310        # so we don't have to go digging through each full language.
311        try:
312            import json
313
314            with open(
315                os.path.join(
316                    bui.app.env.data_directory,
317                    'ba_data',
318                    'data',
319                    'langdata.json',
320                ),
321                encoding='utf-8',
322            ) as infile:
323                lang_names_translated = json.loads(infile.read())[
324                    'lang_names_translated'
325                ]
326        except Exception:
327            logging.exception('Error reading lang data.')
328            lang_names_translated = {}
329
330        langs_translated = {}
331        for lang in languages:
332            langs_translated[lang] = lang_names_translated.get(lang, lang)
333
334        langs_full = {}
335        for lang in languages:
336            lang_translated = bui.Lstr(translate=('languages', lang)).evaluate()
337            if langs_translated[lang] == lang_translated:
338                langs_full[lang] = lang_translated
339            else:
340                langs_full[lang] = (
341                    langs_translated[lang] + ' (' + lang_translated + ')'
342                )
343
344        self._language_popup = PopupMenu(
345            parent=self._subcontainer,
346            position=(210, v - 19),
347            width=150,
348            opening_call=bui.WeakCall(self._on_menu_open),
349            closing_call=bui.WeakCall(self._on_menu_close),
350            autoselect=False,
351            on_value_change_call=bui.WeakCall(self._on_menu_choice),
352            choices=['Auto'] + languages,
353            button_size=(250, 60),
354            choices_display=(
355                [
356                    bui.Lstr(
357                        value=(
358                            bui.Lstr(resource='autoText').evaluate()
359                            + ' ('
360                            + bui.Lstr(
361                                translate=(
362                                    'languages',
363                                    bui.app.lang.default_language,
364                                )
365                            ).evaluate()
366                            + ')'
367                        )
368                    )
369                ]
370                + [bui.Lstr(value=langs_full[l]) for l in languages]
371            ),
372            current_choice=cur_lang,
373        )
374
375        v -= self._spacing * 1.8
376
377        bui.textwidget(
378            parent=self._subcontainer,
379            position=(90, v + 10),
380            size=(0, 0),
381            text=bui.Lstr(
382                resource=f'{self._r}.helpTranslateText',
383                subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
384            ),
385            maxwidth=self._sub_width * 0.9,
386            max_height=55,
387            flatness=1.0,
388            scale=0.65,
389            color=(0.4, 0.9, 0.4, 0.8),
390            h_align='left',
391            v_align='center',
392        )
393        v -= self._spacing * 1.9
394        this_button_width = 410
395        self._translation_editor_button = bui.buttonwidget(
396            parent=self._subcontainer,
397            position=(self._sub_width / 2 - this_button_width / 2, v - 24),
398            size=(this_button_width, 60),
399            label=bui.Lstr(
400                resource=f'{self._r}.translationEditorButtonText',
401                subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
402            ),
403            autoselect=True,
404            on_activate_call=bui.Call(
405                bui.open_url, 'https://legacy.ballistica.net/translate'
406            ),
407        )
408
409        self._lang_status_text = bui.textwidget(
410            parent=self._subcontainer,
411            position=(self._sub_width * 0.5, v - 40),
412            size=(0, 0),
413            text='',
414            flatness=1.0,
415            scale=0.63,
416            h_align='center',
417            v_align='center',
418            maxwidth=400.0,
419        )
420        self._update_lang_status()
421        v -= 50
422
423        lang_inform = plus.get_v1_account_misc_val('langInform', False)
424
425        self._language_inform_checkbox = cbw = bui.checkboxwidget(
426            parent=self._subcontainer,
427            position=(50, v - 50),
428            size=(self._sub_width - 100, 30),
429            autoselect=True,
430            maxwidth=430,
431            textcolor=(0.8, 0.8, 0.8),
432            value=lang_inform,
433            text=bui.Lstr(resource=f'{self._r}.translationInformMe'),
434            on_value_change_call=bui.WeakCall(
435                self._on_lang_inform_value_change
436            ),
437        )
438
439        bui.widget(
440            edit=self._translation_editor_button,
441            down_widget=cbw,
442            up_widget=self._language_popup.get_button(),
443        )
444
445        v -= self._spacing * 3.0
446
447        self._kick_idle_players_check_box = ConfigCheckBox(
448            parent=self._subcontainer,
449            position=(50, v),
450            size=(self._sub_width - 100, 30),
451            configkey='Kick Idle Players',
452            displayname=bui.Lstr(resource=f'{self._r}.kickIdlePlayersText'),
453            scale=1.0,
454            maxwidth=430,
455        )
456
457        v -= 42
458        self._show_game_ping_check_box = ConfigCheckBox(
459            parent=self._subcontainer,
460            position=(50, v),
461            size=(self._sub_width - 100, 30),
462            configkey='Show Ping',
463            displayname=bui.Lstr(resource=f'{self._r}.showInGamePingText'),
464            scale=1.0,
465            maxwidth=430,
466        )
467
468        v -= 42
469        self._show_demos_when_idle_check_box = ConfigCheckBox(
470            parent=self._subcontainer,
471            position=(50, v),
472            size=(self._sub_width - 100, 30),
473            configkey='Show Demos When Idle',
474            displayname=bui.Lstr(resource=f'{self._r}.showDemosWhenIdleText'),
475            scale=1.0,
476            maxwidth=430,
477        )
478
479        v -= 42
480        self._show_deprecated_login_types_check_box = ConfigCheckBox(
481            parent=self._subcontainer,
482            position=(50, v),
483            size=(self._sub_width - 100, 30),
484            configkey='Show Deprecated Login Types',
485            displayname=bui.Lstr(
486                resource=f'{self._r}.showDeprecatedLoginTypesText'
487            ),
488            scale=1.0,
489            maxwidth=430,
490        )
491
492        v -= 42
493        self._disable_camera_shake_check_box = ConfigCheckBox(
494            parent=self._subcontainer,
495            position=(50, v),
496            size=(self._sub_width - 100, 30),
497            configkey='Disable Camera Shake',
498            displayname=bui.Lstr(resource=f'{self._r}.disableCameraShakeText'),
499            scale=1.0,
500            maxwidth=430,
501        )
502
503        self._disable_gyro_check_box: ConfigCheckBox | None = None
504        if self._show_disable_gyro:
505            v -= 42
506            self._disable_gyro_check_box = ConfigCheckBox(
507                parent=self._subcontainer,
508                position=(50, v),
509                size=(self._sub_width - 100, 30),
510                configkey='Disable Camera Gyro',
511                displayname=bui.Lstr(
512                    resource=f'{self._r}.disableCameraGyroscopeMotionText'
513                ),
514                scale=1.0,
515                maxwidth=430,
516            )
517
518        self._always_use_internal_keyboard_check_box: ConfigCheckBox | None
519        if self._show_always_use_internal_keyboard:
520            v -= 42
521            self._always_use_internal_keyboard_check_box = ConfigCheckBox(
522                parent=self._subcontainer,
523                position=(50, v),
524                size=(self._sub_width - 100, 30),
525                configkey='Always Use Internal Keyboard',
526                autoselect=True,
527                displayname=bui.Lstr(
528                    resource=f'{self._r}.alwaysUseInternalKeyboardText'
529                ),
530                scale=1.0,
531                maxwidth=430,
532            )
533            bui.textwidget(
534                parent=self._subcontainer,
535                position=(90, v - 10),
536                size=(0, 0),
537                text=bui.Lstr(
538                    resource=(
539                        f'{self._r}.alwaysUseInternalKeyboardDescriptionText'
540                    )
541                ),
542                maxwidth=400,
543                flatness=1.0,
544                scale=0.65,
545                color=(0.4, 0.9, 0.4, 0.8),
546                h_align='left',
547                v_align='center',
548            )
549            v -= 20
550        else:
551            self._always_use_internal_keyboard_check_box = None
552
553        v -= self._spacing * 2.1
554
555        this_button_width = 410
556        self._modding_guide_button = bui.buttonwidget(
557            parent=self._subcontainer,
558            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
559            size=(this_button_width, 60),
560            autoselect=True,
561            label=bui.Lstr(resource=f'{self._r}.moddingGuideText'),
562            text_scale=1.0,
563            on_activate_call=bui.Call(
564                bui.open_url, 'https://ballistica.net/wiki/modding-guide'
565            ),
566        )
567
568        v -= self._spacing * 2.0
569
570        self._dev_tools_button = bui.buttonwidget(
571            parent=self._subcontainer,
572            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
573            size=(this_button_width, 60),
574            autoselect=True,
575            label=bui.Lstr(resource=f'{self._r}.devToolsText'),
576            text_scale=1.0,
577            on_activate_call=self._on_dev_tools_button_press,
578        )
579
580        if self._show_always_use_internal_keyboard:
581            assert self._always_use_internal_keyboard_check_box is not None
582            bui.widget(
583                edit=self._always_use_internal_keyboard_check_box.widget,
584                down_widget=self._modding_guide_button,
585            )
586            bui.widget(
587                edit=self._modding_guide_button,
588                up_widget=self._always_use_internal_keyboard_check_box.widget,
589            )
590        else:
591            # ew.
592            next_widget_up = (
593                self._disable_gyro_check_box.widget
594                if self._disable_gyro_check_box is not None
595                else self._disable_camera_shake_check_box.widget
596            )
597            bui.widget(
598                edit=self._modding_guide_button,
599                up_widget=next_widget_up,
600            )
601            bui.widget(
602                edit=next_widget_up,
603                down_widget=self._modding_guide_button,
604            )
605
606        v -= self._spacing * 2.0
607
608        self._show_user_mods_button = bui.buttonwidget(
609            parent=self._subcontainer,
610            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
611            size=(this_button_width, 60),
612            autoselect=True,
613            label=bui.Lstr(resource=f'{self._r}.showUserModsText'),
614            text_scale=1.0,
615            on_activate_call=show_user_scripts,
616        )
617
618        v -= self._spacing * 2.0
619
620        self._plugins_button = bui.buttonwidget(
621            parent=self._subcontainer,
622            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
623            size=(this_button_width, 60),
624            autoselect=True,
625            label=bui.Lstr(resource='pluginsText'),
626            text_scale=1.0,
627            on_activate_call=self._on_plugins_button_press,
628        )
629
630        v -= self._spacing * 0.6
631
632        self._vr_test_button: bui.Widget | None
633        if self._do_vr_test_button:
634            v -= self._extra_button_spacing
635            self._vr_test_button = bui.buttonwidget(
636                parent=self._subcontainer,
637                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
638                size=(this_button_width, 60),
639                autoselect=True,
640                label=bui.Lstr(resource=f'{self._r}.vrTestingText'),
641                text_scale=1.0,
642                on_activate_call=self._on_vr_test_press,
643            )
644        else:
645            self._vr_test_button = None
646
647        self._net_test_button: bui.Widget | None
648        if self._do_net_test_button:
649            v -= self._extra_button_spacing
650            self._net_test_button = bui.buttonwidget(
651                parent=self._subcontainer,
652                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
653                size=(this_button_width, 60),
654                autoselect=True,
655                label=bui.Lstr(resource=f'{self._r}.netTestingText'),
656                text_scale=1.0,
657                on_activate_call=self._on_net_test_press,
658            )
659        else:
660            self._net_test_button = None
661
662        v -= 70
663        self._benchmarks_button = bui.buttonwidget(
664            parent=self._subcontainer,
665            position=(self._sub_width / 2 - this_button_width / 2, v - 14),
666            size=(this_button_width, 60),
667            autoselect=True,
668            label=bui.Lstr(resource=f'{self._r}.benchmarksText'),
669            text_scale=1.0,
670            on_activate_call=self._on_benchmark_press,
671        )
672
673        v -= 100
674        self._send_info_button = bui.buttonwidget(
675            parent=self._subcontainer,
676            position=(self._sub_width / 2 - this_button_width / 2, v - 14),
677            size=(this_button_width, 60),
678            autoselect=True,
679            label=bui.Lstr(resource=f'{self._r}.sendInfoText'),
680            text_scale=1.0,
681            on_activate_call=self._on_send_info_press,
682        )
683
684        for child in self._subcontainer.get_children():
685            bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20)
686
687        if bui.app.ui_v1.use_toolbars:
688            pbtn = bui.get_special_widget('party_button')
689            bui.widget(edit=self._scrollwidget, right_widget=pbtn)
690            if self._back_button is None:
691                bui.widget(
692                    edit=self._scrollwidget,
693                    left_widget=bui.get_special_widget('back_button'),
694                )
695
696        self._restore_state()
697
698    def _show_restart_needed(self, value: Any) -> None:
699        del value  # Unused.
700        bui.screenmessage(
701            bui.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0)
702        )
703
704    def _on_lang_inform_value_change(self, val: bool) -> None:
705        plus = bui.app.plus
706        assert plus is not None
707        plus.add_v1_account_transaction(
708            {'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val}
709        )
710        plus.run_v1_account_transactions()
711
712    def _on_vr_test_press(self) -> None:
713        from bauiv1lib.settings.vrtesting import VRTestingWindow
714
715        # no-op if our underlying widget is dead or on its way out.
716        if not self._root_widget or self._root_widget.transitioning_out:
717            return
718
719        self._save_state()
720        bui.containerwidget(edit=self._root_widget, transition='out_left')
721        assert bui.app.classic is not None
722        bui.app.ui_v1.set_main_menu_window(
723            VRTestingWindow(transition='in_right').get_root_widget(),
724            from_window=self._root_widget,
725        )
726
727    def _on_net_test_press(self) -> None:
728        plus = bui.app.plus
729        assert plus is not None
730        from bauiv1lib.settings.nettesting import NetTestingWindow
731
732        # no-op if our underlying widget is dead or on its way out.
733        if not self._root_widget or self._root_widget.transitioning_out:
734            return
735
736        self._save_state()
737        bui.containerwidget(edit=self._root_widget, transition='out_left')
738        assert bui.app.classic is not None
739        bui.app.ui_v1.set_main_menu_window(
740            NetTestingWindow(transition='in_right').get_root_widget(),
741            from_window=self._root_widget,
742        )
743
744    def _on_friend_promo_code_press(self) -> None:
745        from bauiv1lib import appinvite
746        from bauiv1lib import account
747
748        plus = bui.app.plus
749        assert plus is not None
750
751        if plus.get_v1_account_state() != 'signed_in':
752            account.show_sign_in_prompt()
753            return
754        appinvite.handle_app_invites_press()
755
756    def _on_plugins_button_press(self) -> None:
757        from bauiv1lib.settings.plugins import PluginWindow
758
759        # no-op if our underlying widget is dead or on its way out.
760        if not self._root_widget or self._root_widget.transitioning_out:
761            return
762
763        self._save_state()
764        bui.containerwidget(edit=self._root_widget, transition='out_left')
765        assert bui.app.classic is not None
766        bui.app.ui_v1.set_main_menu_window(
767            PluginWindow(origin_widget=self._plugins_button).get_root_widget(),
768            from_window=self._root_widget,
769        )
770
771    def _on_dev_tools_button_press(self) -> None:
772        # pylint: disable=cyclic-import
773        from bauiv1lib.settings.devtools import DevToolsWindow
774
775        # no-op if our underlying widget is dead or on its way out.
776        if not self._root_widget or self._root_widget.transitioning_out:
777            return
778
779        self._save_state()
780        bui.containerwidget(edit=self._root_widget, transition='out_left')
781        assert bui.app.classic is not None
782        bui.app.ui_v1.set_main_menu_window(
783            DevToolsWindow(
784                origin_widget=self._dev_tools_button
785            ).get_root_widget(),
786            from_window=self._root_widget,
787        )
788
789    def _on_send_info_press(self) -> None:
790        from bauiv1lib.sendinfo import SendInfoWindow
791
792        # no-op if our underlying widget is dead or on its way out.
793        if not self._root_widget or self._root_widget.transitioning_out:
794            return
795
796        plus = bui.app.plus
797        assert plus is not None
798
799        self._save_state()
800        bui.containerwidget(edit=self._root_widget, transition='out_left')
801        assert bui.app.classic is not None
802        bui.app.ui_v1.set_main_menu_window(
803            SendInfoWindow(
804                origin_widget=self._send_info_button
805            ).get_root_widget(),
806            from_window=self._root_widget,
807        )
808
809    def _on_benchmark_press(self) -> None:
810        from bauiv1lib.debug import DebugWindow
811
812        # no-op if our underlying widget is dead or on its way out.
813        if not self._root_widget or self._root_widget.transitioning_out:
814            return
815
816        self._save_state()
817        bui.containerwidget(edit=self._root_widget, transition='out_left')
818        assert bui.app.classic is not None
819        bui.app.ui_v1.set_main_menu_window(
820            DebugWindow(transition='in_right').get_root_widget(),
821            from_window=self._root_widget,
822        )
823
824    def _save_state(self) -> None:
825        # pylint: disable=too-many-branches
826        # pylint: disable=too-many-statements
827        try:
828            sel = self._root_widget.get_selected_child()
829            if sel == self._scrollwidget:
830                sel = self._subcontainer.get_selected_child()
831                if sel == self._vr_test_button:
832                    sel_name = 'VRTest'
833                elif sel == self._net_test_button:
834                    sel_name = 'NetTest'
835                elif sel == self._send_info_button:
836                    sel_name = 'SendInfo'
837                elif sel == self._benchmarks_button:
838                    sel_name = 'Benchmarks'
839                elif sel == self._kick_idle_players_check_box.widget:
840                    sel_name = 'KickIdlePlayers'
841                elif sel == self._show_demos_when_idle_check_box.widget:
842                    sel_name = 'ShowDemosWhenIdle'
843                elif sel == self._show_deprecated_login_types_check_box.widget:
844                    sel_name = 'ShowDeprecatedLoginTypes'
845                elif sel == self._show_game_ping_check_box.widget:
846                    sel_name = 'ShowPing'
847                elif sel == self._disable_camera_shake_check_box.widget:
848                    sel_name = 'DisableCameraShake'
849                elif (
850                    self._always_use_internal_keyboard_check_box is not None
851                    and sel
852                    == self._always_use_internal_keyboard_check_box.widget
853                ):
854                    sel_name = 'AlwaysUseInternalKeyboard'
855                elif (
856                    self._disable_gyro_check_box is not None
857                    and sel == self._disable_gyro_check_box.widget
858                ):
859                    sel_name = 'DisableGyro'
860                elif (
861                    self._language_popup is not None
862                    and sel == self._language_popup.get_button()
863                ):
864                    sel_name = 'Languages'
865                elif sel == self._translation_editor_button:
866                    sel_name = 'TranslationEditor'
867                elif sel == self._show_user_mods_button:
868                    sel_name = 'ShowUserMods'
869                elif sel == self._plugins_button:
870                    sel_name = 'Plugins'
871                elif sel == self._dev_tools_button:
872                    sel_name = 'DevTools'
873                elif sel == self._modding_guide_button:
874                    sel_name = 'ModdingGuide'
875                elif sel == self._language_inform_checkbox:
876                    sel_name = 'LangInform'
877                else:
878                    raise ValueError(f'unrecognized selection \'{sel}\'')
879            elif sel == self._back_button:
880                sel_name = 'Back'
881            else:
882                raise ValueError(f'unrecognized selection \'{sel}\'')
883            assert bui.app.classic is not None
884            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
885
886        except Exception:
887            logging.exception('Error saving state for %s.', self)
888
889    def _restore_state(self) -> None:
890        # pylint: disable=too-many-branches
891        try:
892            assert bui.app.classic is not None
893            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
894                'sel_name'
895            )
896            if sel_name == 'Back':
897                sel = self._back_button
898            else:
899                bui.containerwidget(
900                    edit=self._root_widget, selected_child=self._scrollwidget
901                )
902                if sel_name == 'VRTest':
903                    sel = self._vr_test_button
904                elif sel_name == 'NetTest':
905                    sel = self._net_test_button
906                elif sel_name == 'SendInfo':
907                    sel = self._send_info_button
908                elif sel_name == 'Benchmarks':
909                    sel = self._benchmarks_button
910                elif sel_name == 'KickIdlePlayers':
911                    sel = self._kick_idle_players_check_box.widget
912                elif sel_name == 'ShowDemosWhenIdle':
913                    sel = self._show_demos_when_idle_check_box.widget
914                elif sel_name == 'ShowDeprecatedLoginTypes':
915                    sel = self._show_deprecated_login_types_check_box.widget
916                elif sel_name == 'ShowPing':
917                    sel = self._show_game_ping_check_box.widget
918                elif sel_name == 'DisableCameraShake':
919                    sel = self._disable_camera_shake_check_box.widget
920                elif (
921                    sel_name == 'AlwaysUseInternalKeyboard'
922                    and self._always_use_internal_keyboard_check_box is not None
923                ):
924                    sel = self._always_use_internal_keyboard_check_box.widget
925                elif (
926                    sel_name == 'DisableGyro'
927                    and self._disable_gyro_check_box is not None
928                ):
929                    sel = self._disable_gyro_check_box.widget
930                elif (
931                    sel_name == 'Languages' and self._language_popup is not None
932                ):
933                    sel = self._language_popup.get_button()
934                elif sel_name == 'TranslationEditor':
935                    sel = self._translation_editor_button
936                elif sel_name == 'ShowUserMods':
937                    sel = self._show_user_mods_button
938                elif sel_name == 'Plugins':
939                    sel = self._plugins_button
940                elif sel_name == 'DevTools':
941                    sel = self._dev_tools_button
942                elif sel_name == 'ModdingGuide':
943                    sel = self._modding_guide_button
944                elif sel_name == 'LangInform':
945                    sel = self._language_inform_checkbox
946                else:
947                    sel = None
948                if sel is not None:
949                    bui.containerwidget(
950                        edit=self._subcontainer,
951                        selected_child=sel,
952                        visible_child=sel,
953                    )
954        except Exception:
955            logging.exception('Error restoring state for %s.', self)
956
957    def _on_menu_open(self) -> None:
958        self._menu_open = True
959
960    def _on_menu_close(self) -> None:
961        self._menu_open = False
962
963    def _on_menu_choice(self, choice: str) -> None:
964        bui.app.lang.setlanguage(None if choice == 'Auto' else choice)
965        self._save_state()
966        bui.apptimer(0.1, bui.WeakCall(self._rebuild))
967
968    def _completed_langs_cb(self, results: dict[str, Any] | None) -> None:
969        if results is not None and results['langs'] is not None:
970            self._complete_langs_list = results['langs']
971            self._complete_langs_error = False
972        else:
973            self._complete_langs_list = None
974            self._complete_langs_error = True
975        bui.apptimer(0.001, bui.WeakCall(self._update_lang_status))
976
977    def _do_back(self) -> None:
978        from bauiv1lib.settings.allsettings import AllSettingsWindow
979
980        # no-op if our underlying widget is dead or on its way out.
981        if not self._root_widget or self._root_widget.transitioning_out:
982            return
983
984        self._save_state()
985        bui.containerwidget(
986            edit=self._root_widget, transition=self._transition_out
987        )
988        assert bui.app.classic is not None
989        bui.app.ui_v1.set_main_menu_window(
990            AllSettingsWindow(transition='in_left').get_root_widget(),
991            from_window=self._root_widget,
992        )
class AdvancedSettingsWindow(bauiv1._uitypes.Window):
 19class AdvancedSettingsWindow(bui.Window):
 20    """Window for editing advanced app settings."""
 21
 22    def __init__(
 23        self,
 24        transition: str = 'in_right',
 25        origin_widget: bui.Widget | None = None,
 26    ):
 27        # pylint: disable=too-many-statements
 28        import threading
 29
 30        if bui.app.classic is None:
 31            raise RuntimeError('This requires classic support.')
 32
 33        # Preload some modules we use in a background thread so we won't
 34        # have a visual hitch when the user taps them.
 35        threading.Thread(target=self._preload_modules).start()
 36
 37        app = bui.app
 38        assert app.classic is not None
 39
 40        # If they provided an origin-widget, scale up from that.
 41        scale_origin: tuple[float, float] | None
 42        if origin_widget is not None:
 43            self._transition_out = 'out_scale'
 44            scale_origin = origin_widget.get_screen_space_center()
 45            transition = 'in_scale'
 46        else:
 47            self._transition_out = 'out_right'
 48            scale_origin = None
 49
 50        uiscale = bui.app.ui_v1.uiscale
 51        self._width = 970.0 if uiscale is bui.UIScale.SMALL else 670.0
 52        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 53        self._height = (
 54            390.0
 55            if uiscale is bui.UIScale.SMALL
 56            else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0
 57        )
 58        self._lang_status_text: bui.Widget | None = None
 59
 60        self._spacing = 32
 61        self._menu_open = False
 62        top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
 63
 64        super().__init__(
 65            root_widget=bui.containerwidget(
 66                size=(self._width, self._height + top_extra),
 67                transition=transition,
 68                toolbar_visibility='menu_minimal',
 69                scale_origin_stack_offset=scale_origin,
 70                scale=(
 71                    2.06
 72                    if uiscale is bui.UIScale.SMALL
 73                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 74                ),
 75                stack_offset=(
 76                    (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0)
 77                ),
 78            )
 79        )
 80
 81        self._prev_lang = ''
 82        self._prev_lang_list: list[str] = []
 83        self._complete_langs_list: list | None = None
 84        self._complete_langs_error = False
 85        self._language_popup: PopupMenu | None = None
 86
 87        # In vr-mode, the internal keyboard is currently the *only* option,
 88        # so no need to show this.
 89        self._show_always_use_internal_keyboard = not app.env.vr
 90
 91        self._scroll_width = self._width - (100 + 2 * x_inset)
 92        self._scroll_height = self._height - 115.0
 93        self._sub_width = self._scroll_width * 0.95
 94        self._sub_height = 870.0
 95
 96        if self._show_always_use_internal_keyboard:
 97            self._sub_height += 62
 98
 99        self._show_disable_gyro = app.classic.platform in {'ios', 'android'}
100        if self._show_disable_gyro:
101            self._sub_height += 42
102
103        self._do_vr_test_button = app.env.vr
104        self._do_net_test_button = True
105        self._extra_button_spacing = self._spacing * 2.5
106
107        if self._do_vr_test_button:
108            self._sub_height += self._extra_button_spacing
109        if self._do_net_test_button:
110            self._sub_height += self._extra_button_spacing
111        self._sub_height += self._spacing * 2.0  # plugins
112        self._sub_height += self._spacing * 2.0  # dev tools
113
114        self._r = 'settingsWindowAdvanced'
115
116        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
117            bui.containerwidget(
118                edit=self._root_widget, on_cancel_call=self._do_back
119            )
120            self._back_button = None
121        else:
122            self._back_button = bui.buttonwidget(
123                parent=self._root_widget,
124                position=(53 + x_inset, self._height - 60),
125                size=(140, 60),
126                scale=0.8,
127                autoselect=True,
128                label=bui.Lstr(resource='backText'),
129                button_type='back',
130                on_activate_call=self._do_back,
131            )
132            bui.containerwidget(
133                edit=self._root_widget, cancel_button=self._back_button
134            )
135
136        self._title_text = bui.textwidget(
137            parent=self._root_widget,
138            position=(0, self._height - 52),
139            size=(self._width, 25),
140            text=bui.Lstr(resource=f'{self._r}.titleText'),
141            color=app.ui_v1.title_color,
142            h_align='center',
143            v_align='top',
144        )
145
146        if self._back_button is not None:
147            bui.buttonwidget(
148                edit=self._back_button,
149                button_type='backSmall',
150                size=(60, 60),
151                label=bui.charstr(bui.SpecialChar.BACK),
152            )
153
154        self._scrollwidget = bui.scrollwidget(
155            parent=self._root_widget,
156            position=(50 + x_inset, 50),
157            simple_culling_v=20.0,
158            highlight=False,
159            size=(self._scroll_width, self._scroll_height),
160            selection_loops_to_parent=True,
161        )
162        bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
163        self._subcontainer = bui.containerwidget(
164            parent=self._scrollwidget,
165            size=(self._sub_width, self._sub_height),
166            background=False,
167            selection_loops_to_parent=True,
168        )
169
170        self._rebuild()
171
172        # Rebuild periodically to pick up language changes/additions/etc.
173        self._rebuild_timer = bui.AppTimer(
174            1.0, bui.WeakCall(self._rebuild), repeat=True
175        )
176
177        # Fetch the list of completed languages.
178        bui.app.classic.master_server_v1_get(
179            'bsLangGetCompleted',
180            {'b': app.env.engine_build_number},
181            callback=bui.WeakCall(self._completed_langs_cb),
182        )
183
184    # noinspection PyUnresolvedReferences
185    @staticmethod
186    def _preload_modules() -> None:
187        """Preload modules we use; avoids hitches (called in bg thread)."""
188        from babase import modutils as _unused2
189        from bauiv1lib import config as _unused1
190        from bauiv1lib.settings import vrtesting as _unused3
191        from bauiv1lib.settings import nettesting as _unused4
192        from bauiv1lib import appinvite as _unused5
193        from bauiv1lib import account as _unused6
194        from bauiv1lib import sendinfo as _unused7
195        from bauiv1lib import debug as _unused8
196        from bauiv1lib.settings import plugins as _unused9
197        from bauiv1lib.settings import devtools as _unused10
198
199    def _update_lang_status(self) -> None:
200        if self._complete_langs_list is not None:
201            up_to_date = bui.app.lang.language in self._complete_langs_list
202            bui.textwidget(
203                edit=self._lang_status_text,
204                text=(
205                    ''
206                    if bui.app.lang.language == 'Test'
207                    else (
208                        bui.Lstr(
209                            resource=f'{self._r}.translationNoUpdateNeededText'
210                        )
211                        if up_to_date
212                        else bui.Lstr(
213                            resource=f'{self._r}.translationUpdateNeededText'
214                        )
215                    )
216                ),
217                color=(
218                    (0.2, 1.0, 0.2, 0.8) if up_to_date else (1.0, 0.2, 0.2, 0.8)
219                ),
220            )
221        else:
222            bui.textwidget(
223                edit=self._lang_status_text,
224                text=(
225                    bui.Lstr(resource=f'{self._r}.translationFetchErrorText')
226                    if self._complete_langs_error
227                    else bui.Lstr(
228                        resource=f'{self._r}.translationFetchingStatusText'
229                    )
230                ),
231                color=(
232                    (1.0, 0.5, 0.2)
233                    if self._complete_langs_error
234                    else (0.7, 0.7, 0.7)
235                ),
236            )
237
238    def _rebuild(self) -> None:
239        # pylint: disable=too-many-statements
240        # pylint: disable=too-many-branches
241        # pylint: disable=too-many-locals
242
243        from bauiv1lib.config import ConfigCheckBox
244        from babase.modutils import show_user_scripts
245
246        plus = bui.app.plus
247        assert plus is not None
248
249        available_languages = bui.app.lang.available_languages
250
251        # Don't rebuild if the menu is open or if our language and
252        # language-list hasn't changed.
253
254        # NOTE - although we now support widgets updating their own
255        # translations, we still change the label formatting on the language
256        # menu based on the language so still need this. ...however we could
257        # make this more limited to it only rebuilds that one menu instead
258        # of everything.
259        if self._menu_open or (
260            self._prev_lang == bui.app.config.get('Lang', None)
261            and self._prev_lang_list == available_languages
262        ):
263            return
264        self._prev_lang = bui.app.config.get('Lang', None)
265        self._prev_lang_list = available_languages
266
267        # Clear out our sub-container.
268        children = self._subcontainer.get_children()
269        for child in children:
270            child.delete()
271
272        v = self._sub_height - 35
273
274        v -= self._spacing * 1.2
275
276        # Update our existing back button and title.
277        if self._back_button is not None:
278            bui.buttonwidget(
279                edit=self._back_button, label=bui.Lstr(resource='backText')
280            )
281            bui.buttonwidget(
282                edit=self._back_button, label=bui.charstr(bui.SpecialChar.BACK)
283            )
284
285        bui.textwidget(
286            edit=self._title_text,
287            text=bui.Lstr(resource=f'{self._r}.titleText'),
288        )
289
290        this_button_width = 410
291
292        assert bui.app.classic is not None
293        bui.textwidget(
294            parent=self._subcontainer,
295            position=(70, v + 10),
296            size=(0, 0),
297            text=bui.Lstr(resource=f'{self._r}.languageText'),
298            maxwidth=150,
299            scale=1.2,
300            color=bui.app.ui_v1.title_color,
301            h_align='left',
302            v_align='center',
303        )
304
305        languages = bui.app.lang.available_languages
306        cur_lang = bui.app.config.get('Lang', None)
307        if cur_lang is None:
308            cur_lang = 'Auto'
309
310        # We have a special dict of language names in that language
311        # so we don't have to go digging through each full language.
312        try:
313            import json
314
315            with open(
316                os.path.join(
317                    bui.app.env.data_directory,
318                    'ba_data',
319                    'data',
320                    'langdata.json',
321                ),
322                encoding='utf-8',
323            ) as infile:
324                lang_names_translated = json.loads(infile.read())[
325                    'lang_names_translated'
326                ]
327        except Exception:
328            logging.exception('Error reading lang data.')
329            lang_names_translated = {}
330
331        langs_translated = {}
332        for lang in languages:
333            langs_translated[lang] = lang_names_translated.get(lang, lang)
334
335        langs_full = {}
336        for lang in languages:
337            lang_translated = bui.Lstr(translate=('languages', lang)).evaluate()
338            if langs_translated[lang] == lang_translated:
339                langs_full[lang] = lang_translated
340            else:
341                langs_full[lang] = (
342                    langs_translated[lang] + ' (' + lang_translated + ')'
343                )
344
345        self._language_popup = PopupMenu(
346            parent=self._subcontainer,
347            position=(210, v - 19),
348            width=150,
349            opening_call=bui.WeakCall(self._on_menu_open),
350            closing_call=bui.WeakCall(self._on_menu_close),
351            autoselect=False,
352            on_value_change_call=bui.WeakCall(self._on_menu_choice),
353            choices=['Auto'] + languages,
354            button_size=(250, 60),
355            choices_display=(
356                [
357                    bui.Lstr(
358                        value=(
359                            bui.Lstr(resource='autoText').evaluate()
360                            + ' ('
361                            + bui.Lstr(
362                                translate=(
363                                    'languages',
364                                    bui.app.lang.default_language,
365                                )
366                            ).evaluate()
367                            + ')'
368                        )
369                    )
370                ]
371                + [bui.Lstr(value=langs_full[l]) for l in languages]
372            ),
373            current_choice=cur_lang,
374        )
375
376        v -= self._spacing * 1.8
377
378        bui.textwidget(
379            parent=self._subcontainer,
380            position=(90, v + 10),
381            size=(0, 0),
382            text=bui.Lstr(
383                resource=f'{self._r}.helpTranslateText',
384                subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
385            ),
386            maxwidth=self._sub_width * 0.9,
387            max_height=55,
388            flatness=1.0,
389            scale=0.65,
390            color=(0.4, 0.9, 0.4, 0.8),
391            h_align='left',
392            v_align='center',
393        )
394        v -= self._spacing * 1.9
395        this_button_width = 410
396        self._translation_editor_button = bui.buttonwidget(
397            parent=self._subcontainer,
398            position=(self._sub_width / 2 - this_button_width / 2, v - 24),
399            size=(this_button_width, 60),
400            label=bui.Lstr(
401                resource=f'{self._r}.translationEditorButtonText',
402                subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
403            ),
404            autoselect=True,
405            on_activate_call=bui.Call(
406                bui.open_url, 'https://legacy.ballistica.net/translate'
407            ),
408        )
409
410        self._lang_status_text = bui.textwidget(
411            parent=self._subcontainer,
412            position=(self._sub_width * 0.5, v - 40),
413            size=(0, 0),
414            text='',
415            flatness=1.0,
416            scale=0.63,
417            h_align='center',
418            v_align='center',
419            maxwidth=400.0,
420        )
421        self._update_lang_status()
422        v -= 50
423
424        lang_inform = plus.get_v1_account_misc_val('langInform', False)
425
426        self._language_inform_checkbox = cbw = bui.checkboxwidget(
427            parent=self._subcontainer,
428            position=(50, v - 50),
429            size=(self._sub_width - 100, 30),
430            autoselect=True,
431            maxwidth=430,
432            textcolor=(0.8, 0.8, 0.8),
433            value=lang_inform,
434            text=bui.Lstr(resource=f'{self._r}.translationInformMe'),
435            on_value_change_call=bui.WeakCall(
436                self._on_lang_inform_value_change
437            ),
438        )
439
440        bui.widget(
441            edit=self._translation_editor_button,
442            down_widget=cbw,
443            up_widget=self._language_popup.get_button(),
444        )
445
446        v -= self._spacing * 3.0
447
448        self._kick_idle_players_check_box = ConfigCheckBox(
449            parent=self._subcontainer,
450            position=(50, v),
451            size=(self._sub_width - 100, 30),
452            configkey='Kick Idle Players',
453            displayname=bui.Lstr(resource=f'{self._r}.kickIdlePlayersText'),
454            scale=1.0,
455            maxwidth=430,
456        )
457
458        v -= 42
459        self._show_game_ping_check_box = ConfigCheckBox(
460            parent=self._subcontainer,
461            position=(50, v),
462            size=(self._sub_width - 100, 30),
463            configkey='Show Ping',
464            displayname=bui.Lstr(resource=f'{self._r}.showInGamePingText'),
465            scale=1.0,
466            maxwidth=430,
467        )
468
469        v -= 42
470        self._show_demos_when_idle_check_box = ConfigCheckBox(
471            parent=self._subcontainer,
472            position=(50, v),
473            size=(self._sub_width - 100, 30),
474            configkey='Show Demos When Idle',
475            displayname=bui.Lstr(resource=f'{self._r}.showDemosWhenIdleText'),
476            scale=1.0,
477            maxwidth=430,
478        )
479
480        v -= 42
481        self._show_deprecated_login_types_check_box = ConfigCheckBox(
482            parent=self._subcontainer,
483            position=(50, v),
484            size=(self._sub_width - 100, 30),
485            configkey='Show Deprecated Login Types',
486            displayname=bui.Lstr(
487                resource=f'{self._r}.showDeprecatedLoginTypesText'
488            ),
489            scale=1.0,
490            maxwidth=430,
491        )
492
493        v -= 42
494        self._disable_camera_shake_check_box = ConfigCheckBox(
495            parent=self._subcontainer,
496            position=(50, v),
497            size=(self._sub_width - 100, 30),
498            configkey='Disable Camera Shake',
499            displayname=bui.Lstr(resource=f'{self._r}.disableCameraShakeText'),
500            scale=1.0,
501            maxwidth=430,
502        )
503
504        self._disable_gyro_check_box: ConfigCheckBox | None = None
505        if self._show_disable_gyro:
506            v -= 42
507            self._disable_gyro_check_box = ConfigCheckBox(
508                parent=self._subcontainer,
509                position=(50, v),
510                size=(self._sub_width - 100, 30),
511                configkey='Disable Camera Gyro',
512                displayname=bui.Lstr(
513                    resource=f'{self._r}.disableCameraGyroscopeMotionText'
514                ),
515                scale=1.0,
516                maxwidth=430,
517            )
518
519        self._always_use_internal_keyboard_check_box: ConfigCheckBox | None
520        if self._show_always_use_internal_keyboard:
521            v -= 42
522            self._always_use_internal_keyboard_check_box = ConfigCheckBox(
523                parent=self._subcontainer,
524                position=(50, v),
525                size=(self._sub_width - 100, 30),
526                configkey='Always Use Internal Keyboard',
527                autoselect=True,
528                displayname=bui.Lstr(
529                    resource=f'{self._r}.alwaysUseInternalKeyboardText'
530                ),
531                scale=1.0,
532                maxwidth=430,
533            )
534            bui.textwidget(
535                parent=self._subcontainer,
536                position=(90, v - 10),
537                size=(0, 0),
538                text=bui.Lstr(
539                    resource=(
540                        f'{self._r}.alwaysUseInternalKeyboardDescriptionText'
541                    )
542                ),
543                maxwidth=400,
544                flatness=1.0,
545                scale=0.65,
546                color=(0.4, 0.9, 0.4, 0.8),
547                h_align='left',
548                v_align='center',
549            )
550            v -= 20
551        else:
552            self._always_use_internal_keyboard_check_box = None
553
554        v -= self._spacing * 2.1
555
556        this_button_width = 410
557        self._modding_guide_button = bui.buttonwidget(
558            parent=self._subcontainer,
559            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
560            size=(this_button_width, 60),
561            autoselect=True,
562            label=bui.Lstr(resource=f'{self._r}.moddingGuideText'),
563            text_scale=1.0,
564            on_activate_call=bui.Call(
565                bui.open_url, 'https://ballistica.net/wiki/modding-guide'
566            ),
567        )
568
569        v -= self._spacing * 2.0
570
571        self._dev_tools_button = bui.buttonwidget(
572            parent=self._subcontainer,
573            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
574            size=(this_button_width, 60),
575            autoselect=True,
576            label=bui.Lstr(resource=f'{self._r}.devToolsText'),
577            text_scale=1.0,
578            on_activate_call=self._on_dev_tools_button_press,
579        )
580
581        if self._show_always_use_internal_keyboard:
582            assert self._always_use_internal_keyboard_check_box is not None
583            bui.widget(
584                edit=self._always_use_internal_keyboard_check_box.widget,
585                down_widget=self._modding_guide_button,
586            )
587            bui.widget(
588                edit=self._modding_guide_button,
589                up_widget=self._always_use_internal_keyboard_check_box.widget,
590            )
591        else:
592            # ew.
593            next_widget_up = (
594                self._disable_gyro_check_box.widget
595                if self._disable_gyro_check_box is not None
596                else self._disable_camera_shake_check_box.widget
597            )
598            bui.widget(
599                edit=self._modding_guide_button,
600                up_widget=next_widget_up,
601            )
602            bui.widget(
603                edit=next_widget_up,
604                down_widget=self._modding_guide_button,
605            )
606
607        v -= self._spacing * 2.0
608
609        self._show_user_mods_button = bui.buttonwidget(
610            parent=self._subcontainer,
611            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
612            size=(this_button_width, 60),
613            autoselect=True,
614            label=bui.Lstr(resource=f'{self._r}.showUserModsText'),
615            text_scale=1.0,
616            on_activate_call=show_user_scripts,
617        )
618
619        v -= self._spacing * 2.0
620
621        self._plugins_button = bui.buttonwidget(
622            parent=self._subcontainer,
623            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
624            size=(this_button_width, 60),
625            autoselect=True,
626            label=bui.Lstr(resource='pluginsText'),
627            text_scale=1.0,
628            on_activate_call=self._on_plugins_button_press,
629        )
630
631        v -= self._spacing * 0.6
632
633        self._vr_test_button: bui.Widget | None
634        if self._do_vr_test_button:
635            v -= self._extra_button_spacing
636            self._vr_test_button = bui.buttonwidget(
637                parent=self._subcontainer,
638                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
639                size=(this_button_width, 60),
640                autoselect=True,
641                label=bui.Lstr(resource=f'{self._r}.vrTestingText'),
642                text_scale=1.0,
643                on_activate_call=self._on_vr_test_press,
644            )
645        else:
646            self._vr_test_button = None
647
648        self._net_test_button: bui.Widget | None
649        if self._do_net_test_button:
650            v -= self._extra_button_spacing
651            self._net_test_button = bui.buttonwidget(
652                parent=self._subcontainer,
653                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
654                size=(this_button_width, 60),
655                autoselect=True,
656                label=bui.Lstr(resource=f'{self._r}.netTestingText'),
657                text_scale=1.0,
658                on_activate_call=self._on_net_test_press,
659            )
660        else:
661            self._net_test_button = None
662
663        v -= 70
664        self._benchmarks_button = bui.buttonwidget(
665            parent=self._subcontainer,
666            position=(self._sub_width / 2 - this_button_width / 2, v - 14),
667            size=(this_button_width, 60),
668            autoselect=True,
669            label=bui.Lstr(resource=f'{self._r}.benchmarksText'),
670            text_scale=1.0,
671            on_activate_call=self._on_benchmark_press,
672        )
673
674        v -= 100
675        self._send_info_button = bui.buttonwidget(
676            parent=self._subcontainer,
677            position=(self._sub_width / 2 - this_button_width / 2, v - 14),
678            size=(this_button_width, 60),
679            autoselect=True,
680            label=bui.Lstr(resource=f'{self._r}.sendInfoText'),
681            text_scale=1.0,
682            on_activate_call=self._on_send_info_press,
683        )
684
685        for child in self._subcontainer.get_children():
686            bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20)
687
688        if bui.app.ui_v1.use_toolbars:
689            pbtn = bui.get_special_widget('party_button')
690            bui.widget(edit=self._scrollwidget, right_widget=pbtn)
691            if self._back_button is None:
692                bui.widget(
693                    edit=self._scrollwidget,
694                    left_widget=bui.get_special_widget('back_button'),
695                )
696
697        self._restore_state()
698
699    def _show_restart_needed(self, value: Any) -> None:
700        del value  # Unused.
701        bui.screenmessage(
702            bui.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0)
703        )
704
705    def _on_lang_inform_value_change(self, val: bool) -> None:
706        plus = bui.app.plus
707        assert plus is not None
708        plus.add_v1_account_transaction(
709            {'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val}
710        )
711        plus.run_v1_account_transactions()
712
713    def _on_vr_test_press(self) -> None:
714        from bauiv1lib.settings.vrtesting import VRTestingWindow
715
716        # no-op if our underlying widget is dead or on its way out.
717        if not self._root_widget or self._root_widget.transitioning_out:
718            return
719
720        self._save_state()
721        bui.containerwidget(edit=self._root_widget, transition='out_left')
722        assert bui.app.classic is not None
723        bui.app.ui_v1.set_main_menu_window(
724            VRTestingWindow(transition='in_right').get_root_widget(),
725            from_window=self._root_widget,
726        )
727
728    def _on_net_test_press(self) -> None:
729        plus = bui.app.plus
730        assert plus is not None
731        from bauiv1lib.settings.nettesting import NetTestingWindow
732
733        # no-op if our underlying widget is dead or on its way out.
734        if not self._root_widget or self._root_widget.transitioning_out:
735            return
736
737        self._save_state()
738        bui.containerwidget(edit=self._root_widget, transition='out_left')
739        assert bui.app.classic is not None
740        bui.app.ui_v1.set_main_menu_window(
741            NetTestingWindow(transition='in_right').get_root_widget(),
742            from_window=self._root_widget,
743        )
744
745    def _on_friend_promo_code_press(self) -> None:
746        from bauiv1lib import appinvite
747        from bauiv1lib import account
748
749        plus = bui.app.plus
750        assert plus is not None
751
752        if plus.get_v1_account_state() != 'signed_in':
753            account.show_sign_in_prompt()
754            return
755        appinvite.handle_app_invites_press()
756
757    def _on_plugins_button_press(self) -> None:
758        from bauiv1lib.settings.plugins import PluginWindow
759
760        # no-op if our underlying widget is dead or on its way out.
761        if not self._root_widget or self._root_widget.transitioning_out:
762            return
763
764        self._save_state()
765        bui.containerwidget(edit=self._root_widget, transition='out_left')
766        assert bui.app.classic is not None
767        bui.app.ui_v1.set_main_menu_window(
768            PluginWindow(origin_widget=self._plugins_button).get_root_widget(),
769            from_window=self._root_widget,
770        )
771
772    def _on_dev_tools_button_press(self) -> None:
773        # pylint: disable=cyclic-import
774        from bauiv1lib.settings.devtools import DevToolsWindow
775
776        # no-op if our underlying widget is dead or on its way out.
777        if not self._root_widget or self._root_widget.transitioning_out:
778            return
779
780        self._save_state()
781        bui.containerwidget(edit=self._root_widget, transition='out_left')
782        assert bui.app.classic is not None
783        bui.app.ui_v1.set_main_menu_window(
784            DevToolsWindow(
785                origin_widget=self._dev_tools_button
786            ).get_root_widget(),
787            from_window=self._root_widget,
788        )
789
790    def _on_send_info_press(self) -> None:
791        from bauiv1lib.sendinfo import SendInfoWindow
792
793        # no-op if our underlying widget is dead or on its way out.
794        if not self._root_widget or self._root_widget.transitioning_out:
795            return
796
797        plus = bui.app.plus
798        assert plus is not None
799
800        self._save_state()
801        bui.containerwidget(edit=self._root_widget, transition='out_left')
802        assert bui.app.classic is not None
803        bui.app.ui_v1.set_main_menu_window(
804            SendInfoWindow(
805                origin_widget=self._send_info_button
806            ).get_root_widget(),
807            from_window=self._root_widget,
808        )
809
810    def _on_benchmark_press(self) -> None:
811        from bauiv1lib.debug import DebugWindow
812
813        # no-op if our underlying widget is dead or on its way out.
814        if not self._root_widget or self._root_widget.transitioning_out:
815            return
816
817        self._save_state()
818        bui.containerwidget(edit=self._root_widget, transition='out_left')
819        assert bui.app.classic is not None
820        bui.app.ui_v1.set_main_menu_window(
821            DebugWindow(transition='in_right').get_root_widget(),
822            from_window=self._root_widget,
823        )
824
825    def _save_state(self) -> None:
826        # pylint: disable=too-many-branches
827        # pylint: disable=too-many-statements
828        try:
829            sel = self._root_widget.get_selected_child()
830            if sel == self._scrollwidget:
831                sel = self._subcontainer.get_selected_child()
832                if sel == self._vr_test_button:
833                    sel_name = 'VRTest'
834                elif sel == self._net_test_button:
835                    sel_name = 'NetTest'
836                elif sel == self._send_info_button:
837                    sel_name = 'SendInfo'
838                elif sel == self._benchmarks_button:
839                    sel_name = 'Benchmarks'
840                elif sel == self._kick_idle_players_check_box.widget:
841                    sel_name = 'KickIdlePlayers'
842                elif sel == self._show_demos_when_idle_check_box.widget:
843                    sel_name = 'ShowDemosWhenIdle'
844                elif sel == self._show_deprecated_login_types_check_box.widget:
845                    sel_name = 'ShowDeprecatedLoginTypes'
846                elif sel == self._show_game_ping_check_box.widget:
847                    sel_name = 'ShowPing'
848                elif sel == self._disable_camera_shake_check_box.widget:
849                    sel_name = 'DisableCameraShake'
850                elif (
851                    self._always_use_internal_keyboard_check_box is not None
852                    and sel
853                    == self._always_use_internal_keyboard_check_box.widget
854                ):
855                    sel_name = 'AlwaysUseInternalKeyboard'
856                elif (
857                    self._disable_gyro_check_box is not None
858                    and sel == self._disable_gyro_check_box.widget
859                ):
860                    sel_name = 'DisableGyro'
861                elif (
862                    self._language_popup is not None
863                    and sel == self._language_popup.get_button()
864                ):
865                    sel_name = 'Languages'
866                elif sel == self._translation_editor_button:
867                    sel_name = 'TranslationEditor'
868                elif sel == self._show_user_mods_button:
869                    sel_name = 'ShowUserMods'
870                elif sel == self._plugins_button:
871                    sel_name = 'Plugins'
872                elif sel == self._dev_tools_button:
873                    sel_name = 'DevTools'
874                elif sel == self._modding_guide_button:
875                    sel_name = 'ModdingGuide'
876                elif sel == self._language_inform_checkbox:
877                    sel_name = 'LangInform'
878                else:
879                    raise ValueError(f'unrecognized selection \'{sel}\'')
880            elif sel == self._back_button:
881                sel_name = 'Back'
882            else:
883                raise ValueError(f'unrecognized selection \'{sel}\'')
884            assert bui.app.classic is not None
885            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
886
887        except Exception:
888            logging.exception('Error saving state for %s.', self)
889
890    def _restore_state(self) -> None:
891        # pylint: disable=too-many-branches
892        try:
893            assert bui.app.classic is not None
894            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
895                'sel_name'
896            )
897            if sel_name == 'Back':
898                sel = self._back_button
899            else:
900                bui.containerwidget(
901                    edit=self._root_widget, selected_child=self._scrollwidget
902                )
903                if sel_name == 'VRTest':
904                    sel = self._vr_test_button
905                elif sel_name == 'NetTest':
906                    sel = self._net_test_button
907                elif sel_name == 'SendInfo':
908                    sel = self._send_info_button
909                elif sel_name == 'Benchmarks':
910                    sel = self._benchmarks_button
911                elif sel_name == 'KickIdlePlayers':
912                    sel = self._kick_idle_players_check_box.widget
913                elif sel_name == 'ShowDemosWhenIdle':
914                    sel = self._show_demos_when_idle_check_box.widget
915                elif sel_name == 'ShowDeprecatedLoginTypes':
916                    sel = self._show_deprecated_login_types_check_box.widget
917                elif sel_name == 'ShowPing':
918                    sel = self._show_game_ping_check_box.widget
919                elif sel_name == 'DisableCameraShake':
920                    sel = self._disable_camera_shake_check_box.widget
921                elif (
922                    sel_name == 'AlwaysUseInternalKeyboard'
923                    and self._always_use_internal_keyboard_check_box is not None
924                ):
925                    sel = self._always_use_internal_keyboard_check_box.widget
926                elif (
927                    sel_name == 'DisableGyro'
928                    and self._disable_gyro_check_box is not None
929                ):
930                    sel = self._disable_gyro_check_box.widget
931                elif (
932                    sel_name == 'Languages' and self._language_popup is not None
933                ):
934                    sel = self._language_popup.get_button()
935                elif sel_name == 'TranslationEditor':
936                    sel = self._translation_editor_button
937                elif sel_name == 'ShowUserMods':
938                    sel = self._show_user_mods_button
939                elif sel_name == 'Plugins':
940                    sel = self._plugins_button
941                elif sel_name == 'DevTools':
942                    sel = self._dev_tools_button
943                elif sel_name == 'ModdingGuide':
944                    sel = self._modding_guide_button
945                elif sel_name == 'LangInform':
946                    sel = self._language_inform_checkbox
947                else:
948                    sel = None
949                if sel is not None:
950                    bui.containerwidget(
951                        edit=self._subcontainer,
952                        selected_child=sel,
953                        visible_child=sel,
954                    )
955        except Exception:
956            logging.exception('Error restoring state for %s.', self)
957
958    def _on_menu_open(self) -> None:
959        self._menu_open = True
960
961    def _on_menu_close(self) -> None:
962        self._menu_open = False
963
964    def _on_menu_choice(self, choice: str) -> None:
965        bui.app.lang.setlanguage(None if choice == 'Auto' else choice)
966        self._save_state()
967        bui.apptimer(0.1, bui.WeakCall(self._rebuild))
968
969    def _completed_langs_cb(self, results: dict[str, Any] | None) -> None:
970        if results is not None and results['langs'] is not None:
971            self._complete_langs_list = results['langs']
972            self._complete_langs_error = False
973        else:
974            self._complete_langs_list = None
975            self._complete_langs_error = True
976        bui.apptimer(0.001, bui.WeakCall(self._update_lang_status))
977
978    def _do_back(self) -> None:
979        from bauiv1lib.settings.allsettings import AllSettingsWindow
980
981        # no-op if our underlying widget is dead or on its way out.
982        if not self._root_widget or self._root_widget.transitioning_out:
983            return
984
985        self._save_state()
986        bui.containerwidget(
987            edit=self._root_widget, transition=self._transition_out
988        )
989        assert bui.app.classic is not None
990        bui.app.ui_v1.set_main_menu_window(
991            AllSettingsWindow(transition='in_left').get_root_widget(),
992            from_window=self._root_widget,
993        )

Window for editing advanced app settings.

AdvancedSettingsWindow( transition: str = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 22    def __init__(
 23        self,
 24        transition: str = 'in_right',
 25        origin_widget: bui.Widget | None = None,
 26    ):
 27        # pylint: disable=too-many-statements
 28        import threading
 29
 30        if bui.app.classic is None:
 31            raise RuntimeError('This requires classic support.')
 32
 33        # Preload some modules we use in a background thread so we won't
 34        # have a visual hitch when the user taps them.
 35        threading.Thread(target=self._preload_modules).start()
 36
 37        app = bui.app
 38        assert app.classic is not None
 39
 40        # If they provided an origin-widget, scale up from that.
 41        scale_origin: tuple[float, float] | None
 42        if origin_widget is not None:
 43            self._transition_out = 'out_scale'
 44            scale_origin = origin_widget.get_screen_space_center()
 45            transition = 'in_scale'
 46        else:
 47            self._transition_out = 'out_right'
 48            scale_origin = None
 49
 50        uiscale = bui.app.ui_v1.uiscale
 51        self._width = 970.0 if uiscale is bui.UIScale.SMALL else 670.0
 52        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 53        self._height = (
 54            390.0
 55            if uiscale is bui.UIScale.SMALL
 56            else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0
 57        )
 58        self._lang_status_text: bui.Widget | None = None
 59
 60        self._spacing = 32
 61        self._menu_open = False
 62        top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
 63
 64        super().__init__(
 65            root_widget=bui.containerwidget(
 66                size=(self._width, self._height + top_extra),
 67                transition=transition,
 68                toolbar_visibility='menu_minimal',
 69                scale_origin_stack_offset=scale_origin,
 70                scale=(
 71                    2.06
 72                    if uiscale is bui.UIScale.SMALL
 73                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 74                ),
 75                stack_offset=(
 76                    (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0)
 77                ),
 78            )
 79        )
 80
 81        self._prev_lang = ''
 82        self._prev_lang_list: list[str] = []
 83        self._complete_langs_list: list | None = None
 84        self._complete_langs_error = False
 85        self._language_popup: PopupMenu | None = None
 86
 87        # In vr-mode, the internal keyboard is currently the *only* option,
 88        # so no need to show this.
 89        self._show_always_use_internal_keyboard = not app.env.vr
 90
 91        self._scroll_width = self._width - (100 + 2 * x_inset)
 92        self._scroll_height = self._height - 115.0
 93        self._sub_width = self._scroll_width * 0.95
 94        self._sub_height = 870.0
 95
 96        if self._show_always_use_internal_keyboard:
 97            self._sub_height += 62
 98
 99        self._show_disable_gyro = app.classic.platform in {'ios', 'android'}
100        if self._show_disable_gyro:
101            self._sub_height += 42
102
103        self._do_vr_test_button = app.env.vr
104        self._do_net_test_button = True
105        self._extra_button_spacing = self._spacing * 2.5
106
107        if self._do_vr_test_button:
108            self._sub_height += self._extra_button_spacing
109        if self._do_net_test_button:
110            self._sub_height += self._extra_button_spacing
111        self._sub_height += self._spacing * 2.0  # plugins
112        self._sub_height += self._spacing * 2.0  # dev tools
113
114        self._r = 'settingsWindowAdvanced'
115
116        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
117            bui.containerwidget(
118                edit=self._root_widget, on_cancel_call=self._do_back
119            )
120            self._back_button = None
121        else:
122            self._back_button = bui.buttonwidget(
123                parent=self._root_widget,
124                position=(53 + x_inset, self._height - 60),
125                size=(140, 60),
126                scale=0.8,
127                autoselect=True,
128                label=bui.Lstr(resource='backText'),
129                button_type='back',
130                on_activate_call=self._do_back,
131            )
132            bui.containerwidget(
133                edit=self._root_widget, cancel_button=self._back_button
134            )
135
136        self._title_text = bui.textwidget(
137            parent=self._root_widget,
138            position=(0, self._height - 52),
139            size=(self._width, 25),
140            text=bui.Lstr(resource=f'{self._r}.titleText'),
141            color=app.ui_v1.title_color,
142            h_align='center',
143            v_align='top',
144        )
145
146        if self._back_button is not None:
147            bui.buttonwidget(
148                edit=self._back_button,
149                button_type='backSmall',
150                size=(60, 60),
151                label=bui.charstr(bui.SpecialChar.BACK),
152            )
153
154        self._scrollwidget = bui.scrollwidget(
155            parent=self._root_widget,
156            position=(50 + x_inset, 50),
157            simple_culling_v=20.0,
158            highlight=False,
159            size=(self._scroll_width, self._scroll_height),
160            selection_loops_to_parent=True,
161        )
162        bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
163        self._subcontainer = bui.containerwidget(
164            parent=self._scrollwidget,
165            size=(self._sub_width, self._sub_height),
166            background=False,
167            selection_loops_to_parent=True,
168        )
169
170        self._rebuild()
171
172        # Rebuild periodically to pick up language changes/additions/etc.
173        self._rebuild_timer = bui.AppTimer(
174            1.0, bui.WeakCall(self._rebuild), repeat=True
175        )
176
177        # Fetch the list of completed languages.
178        bui.app.classic.master_server_v1_get(
179            'bsLangGetCompleted',
180            {'b': app.env.engine_build_number},
181            callback=bui.WeakCall(self._completed_langs_cb),
182        )
Inherited Members
bauiv1._uitypes.Window
get_root_widget