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

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:
183    @override
184    def get_main_window_state(self) -> bui.MainWindowState:
185        # Support recreating our window for back/refresh purposes.
186        cls = type(self)
187        return bui.BasicMainWindowState(
188            create_call=lambda transition, origin_widget: cls(
189                transition=transition, origin_widget=origin_widget
190            )
191        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
193    @override
194    def on_main_window_close(self) -> None:
195        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_is_auxiliary
main_window_close
main_window_has_control
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget