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

Window for editing advanced app settings.

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

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
181    @override
182    def get_main_window_state(self) -> bui.MainWindowState:
183        # Support recreating our window for back/refresh purposes.
184        cls = type(self)
185        return bui.BasicMainWindowState(
186            create_call=lambda transition, origin_widget: cls(
187                transition=transition, origin_widget=origin_widget
188            )
189        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
191    @override
192    def on_main_window_close(self) -> None:
193        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_is_top_level
main_window_close
main_window_has_control
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget