bauiv1lib.settings.advanced

UI functionality for advanced settings.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""UI functionality for advanced settings."""
  4
  5from __future__ import annotations
  6
  7import os
  8import logging
  9from typing import TYPE_CHECKING
 10
 11from bauiv1lib.popup import PopupMenu
 12import bauiv1 as bui
 13
 14if TYPE_CHECKING:
 15    from typing import Any
 16
 17
 18class AdvancedSettingsWindow(bui.Window):
 19    """Window for editing advanced app settings."""
 20
 21    def __init__(
 22        self,
 23        transition: str = 'in_right',
 24        origin_widget: bui.Widget | None = None,
 25    ):
 26        # pylint: disable=too-many-statements
 27        import threading
 28
 29        if bui.app.classic is None:
 30            raise RuntimeError('This requires classic support.')
 31
 32        # Preload some modules we use in a background thread so we won't
 33        # have a visual hitch when the user taps them.
 34        threading.Thread(target=self._preload_modules).start()
 35
 36        app = bui.app
 37        assert app.classic is not None
 38
 39        # If they provided an origin-widget, scale up from that.
 40        scale_origin: tuple[float, float] | None
 41        if origin_widget is not None:
 42            self._transition_out = 'out_scale'
 43            scale_origin = origin_widget.get_screen_space_center()
 44            transition = 'in_scale'
 45        else:
 46            self._transition_out = 'out_right'
 47            scale_origin = None
 48
 49        uiscale = bui.app.ui_v1.uiscale
 50        self._width = 970.0 if uiscale is bui.UIScale.SMALL else 670.0
 51        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 52        self._height = (
 53            390.0
 54            if uiscale is bui.UIScale.SMALL
 55            else 450.0
 56            if uiscale is bui.UIScale.MEDIUM
 57            else 520.0
 58        )
 59        self._lang_status_text: bui.Widget | None = None
 60
 61        self._spacing = 32
 62        self._menu_open = False
 63        top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
 64
 65        super().__init__(
 66            root_widget=bui.containerwidget(
 67                size=(self._width, self._height + top_extra),
 68                transition=transition,
 69                toolbar_visibility='menu_minimal',
 70                scale_origin_stack_offset=scale_origin,
 71                scale=(
 72                    2.06
 73                    if uiscale is bui.UIScale.SMALL
 74                    else 1.4
 75                    if uiscale is bui.UIScale.MEDIUM
 76                    else 1.0
 77                ),
 78                stack_offset=(0, -25)
 79                if uiscale is bui.UIScale.SMALL
 80                else (0, 0),
 81            )
 82        )
 83
 84        self._prev_lang = ''
 85        self._prev_lang_list: list[str] = []
 86        self._complete_langs_list: list | None = None
 87        self._complete_langs_error = False
 88        self._language_popup: PopupMenu | None = None
 89
 90        # In vr-mode, the internal keyboard is currently the *only* option,
 91        # so no need to show this.
 92        self._show_always_use_internal_keyboard = not app.env.vr
 93
 94        self._scroll_width = self._width - (100 + 2 * x_inset)
 95        self._scroll_height = self._height - 115.0
 96        self._sub_width = self._scroll_width * 0.95
 97        self._sub_height = 808.0
 98
 99        if self._show_always_use_internal_keyboard:
100            self._sub_height += 62
101
102        self._show_disable_gyro = app.classic.platform in {'ios', 'android'}
103        if self._show_disable_gyro:
104            self._sub_height += 42
105
106        self._do_vr_test_button = app.env.vr
107        self._do_net_test_button = True
108        self._extra_button_spacing = self._spacing * 2.5
109
110        if self._do_vr_test_button:
111            self._sub_height += self._extra_button_spacing
112        if self._do_net_test_button:
113            self._sub_height += self._extra_button_spacing
114        self._sub_height += self._spacing * 2.0  # plugins
115
116        self._r = 'settingsWindowAdvanced'
117
118        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
119            bui.containerwidget(
120                edit=self._root_widget, on_cancel_call=self._do_back
121            )
122            self._back_button = None
123        else:
124            self._back_button = bui.buttonwidget(
125                parent=self._root_widget,
126                position=(53 + x_inset, self._height - 60),
127                size=(140, 60),
128                scale=0.8,
129                autoselect=True,
130                label=bui.Lstr(resource='backText'),
131                button_type='back',
132                on_activate_call=self._do_back,
133            )
134            bui.containerwidget(
135                edit=self._root_widget, cancel_button=self._back_button
136            )
137
138        self._title_text = bui.textwidget(
139            parent=self._root_widget,
140            position=(0, self._height - 52),
141            size=(self._width, 25),
142            text=bui.Lstr(resource=f'{self._r}.titleText'),
143            color=app.ui_v1.title_color,
144            h_align='center',
145            v_align='top',
146        )
147
148        if self._back_button is not None:
149            bui.buttonwidget(
150                edit=self._back_button,
151                button_type='backSmall',
152                size=(60, 60),
153                label=bui.charstr(bui.SpecialChar.BACK),
154            )
155
156        self._scrollwidget = bui.scrollwidget(
157            parent=self._root_widget,
158            position=(50 + x_inset, 50),
159            simple_culling_v=20.0,
160            highlight=False,
161            size=(self._scroll_width, self._scroll_height),
162            selection_loops_to_parent=True,
163        )
164        bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
165        self._subcontainer = bui.containerwidget(
166            parent=self._scrollwidget,
167            size=(self._sub_width, self._sub_height),
168            background=False,
169            selection_loops_to_parent=True,
170        )
171
172        self._rebuild()
173
174        # Rebuild periodically to pick up language changes/additions/etc.
175        self._rebuild_timer = bui.AppTimer(
176            1.0, bui.WeakCall(self._rebuild), repeat=True
177        )
178
179        # Fetch the list of completed languages.
180        bui.app.classic.master_server_v1_get(
181            'bsLangGetCompleted',
182            {'b': app.env.build_number},
183            callback=bui.WeakCall(self._completed_langs_cb),
184        )
185
186    # noinspection PyUnresolvedReferences
187    @staticmethod
188    def _preload_modules() -> None:
189        """Preload modules we use; avoids hitches (called in bg thread)."""
190        from babase import modutils as _unused2
191        from bauiv1lib import config as _unused1
192        from bauiv1lib.settings import vrtesting as _unused3
193        from bauiv1lib.settings import nettesting as _unused4
194        from bauiv1lib import appinvite as _unused5
195        from bauiv1lib import account as _unused6
196        from bauiv1lib import promocode as _unused7
197        from bauiv1lib import debug as _unused8
198        from bauiv1lib.settings import plugins as _unused9
199
200    def _update_lang_status(self) -> None:
201        if self._complete_langs_list is not None:
202            up_to_date = bui.app.lang.language in self._complete_langs_list
203            bui.textwidget(
204                edit=self._lang_status_text,
205                text=''
206                if bui.app.lang.language == 'Test'
207                else bui.Lstr(
208                    resource=f'{self._r}.translationNoUpdateNeededText'
209                )
210                if up_to_date
211                else bui.Lstr(
212                    resource=f'{self._r}.translationUpdateNeededText'
213                ),
214                color=(0.2, 1.0, 0.2, 0.8)
215                if up_to_date
216                else (1.0, 0.2, 0.2, 0.8),
217            )
218        else:
219            bui.textwidget(
220                edit=self._lang_status_text,
221                text=bui.Lstr(resource=f'{self._r}.translationFetchErrorText')
222                if self._complete_langs_error
223                else bui.Lstr(
224                    resource=f'{self._r}.translationFetchingStatusText'
225                ),
226                color=(1.0, 0.5, 0.2)
227                if self._complete_langs_error
228                else (0.7, 0.7, 0.7),
229            )
230
231    def _rebuild(self) -> None:
232        # pylint: disable=too-many-statements
233        # pylint: disable=too-many-branches
234        # pylint: disable=too-many-locals
235
236        from bauiv1lib.config import ConfigCheckBox
237        from babase.modutils import show_user_scripts
238
239        plus = bui.app.plus
240        assert plus is not None
241
242        available_languages = bui.app.lang.available_languages
243
244        # Don't rebuild if the menu is open or if our language and
245        # language-list hasn't changed.
246
247        # NOTE - although we now support widgets updating their own
248        # translations, we still change the label formatting on the language
249        # menu based on the language so still need this. ...however we could
250        # make this more limited to it only rebuilds that one menu instead
251        # of everything.
252        if self._menu_open or (
253            self._prev_lang == bui.app.config.get('Lang', None)
254            and self._prev_lang_list == available_languages
255        ):
256            return
257        self._prev_lang = bui.app.config.get('Lang', None)
258        self._prev_lang_list = available_languages
259
260        # Clear out our sub-container.
261        children = self._subcontainer.get_children()
262        for child in children:
263            child.delete()
264
265        v = self._sub_height - 35
266
267        v -= self._spacing * 1.2
268
269        # Update our existing back button and title.
270        if self._back_button is not None:
271            bui.buttonwidget(
272                edit=self._back_button, label=bui.Lstr(resource='backText')
273            )
274            bui.buttonwidget(
275                edit=self._back_button, label=bui.charstr(bui.SpecialChar.BACK)
276            )
277
278        bui.textwidget(
279            edit=self._title_text,
280            text=bui.Lstr(resource=f'{self._r}.titleText'),
281        )
282
283        this_button_width = 410
284
285        self._promo_code_button = bui.buttonwidget(
286            parent=self._subcontainer,
287            position=(self._sub_width / 2 - this_button_width / 2, v - 14),
288            size=(this_button_width, 60),
289            autoselect=True,
290            label=bui.Lstr(resource=f'{self._r}.enterPromoCodeText'),
291            text_scale=1.0,
292            on_activate_call=self._on_promo_code_press,
293        )
294        if self._back_button is not None:
295            bui.widget(
296                edit=self._promo_code_button,
297                up_widget=self._back_button,
298                left_widget=self._back_button,
299            )
300        v -= self._extra_button_spacing * 0.8
301
302        assert bui.app.classic is not None
303        bui.textwidget(
304            parent=self._subcontainer,
305            position=(200, v + 10),
306            size=(0, 0),
307            text=bui.Lstr(resource=f'{self._r}.languageText'),
308            maxwidth=150,
309            scale=0.95,
310            color=bui.app.ui_v1.title_color,
311            h_align='right',
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=(self._sub_width * 0.5, 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='center',
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 -= 40
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_dev_console_button_check_box = ConfigCheckBox(
481            parent=self._subcontainer,
482            position=(50, v),
483            size=(self._sub_width - 100, 30),
484            configkey='Show Dev Console Button',
485            displayname=bui.Lstr(
486                resource=f'{self._r}.showDevConsoleButtonText'
487            ),
488            scale=1.0,
489            maxwidth=430,
490        )
491
492        v -= 42
493        self._show_demos_when_idle_check_box = ConfigCheckBox(
494            parent=self._subcontainer,
495            position=(50, v),
496            size=(self._sub_width - 100, 30),
497            configkey='Show Demos When Idle',
498            displayname=bui.Lstr(resource=f'{self._r}.showDemosWhenIdleText'),
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        if self._show_always_use_internal_keyboard:
579            assert self._always_use_internal_keyboard_check_box is not None
580            bui.widget(
581                edit=self._always_use_internal_keyboard_check_box.widget,
582                down_widget=self._modding_guide_button,
583            )
584            bui.widget(
585                edit=self._modding_guide_button,
586                up_widget=self._always_use_internal_keyboard_check_box.widget,
587            )
588        else:
589            # ew.
590            next_widget_up = (
591                self._disable_gyro_check_box.widget
592                if self._disable_gyro_check_box is not None
593                else self._disable_camera_shake_check_box.widget
594            )
595            bui.widget(
596                edit=self._modding_guide_button,
597                up_widget=next_widget_up,
598            )
599            bui.widget(
600                edit=next_widget_up,
601                down_widget=self._modding_guide_button,
602            )
603
604        v -= self._spacing * 2.0
605
606        self._show_user_mods_button = bui.buttonwidget(
607            parent=self._subcontainer,
608            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
609            size=(this_button_width, 60),
610            autoselect=True,
611            label=bui.Lstr(resource=f'{self._r}.showUserModsText'),
612            text_scale=1.0,
613            on_activate_call=show_user_scripts,
614        )
615
616        v -= self._spacing * 2.0
617
618        self._plugins_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='pluginsText'),
624            text_scale=1.0,
625            on_activate_call=self._on_plugins_button_press,
626        )
627
628        v -= self._spacing * 0.6
629
630        self._vr_test_button: bui.Widget | None
631        if self._do_vr_test_button:
632            v -= self._extra_button_spacing
633            self._vr_test_button = bui.buttonwidget(
634                parent=self._subcontainer,
635                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
636                size=(this_button_width, 60),
637                autoselect=True,
638                label=bui.Lstr(resource=f'{self._r}.vrTestingText'),
639                text_scale=1.0,
640                on_activate_call=self._on_vr_test_press,
641            )
642        else:
643            self._vr_test_button = None
644
645        self._net_test_button: bui.Widget | None
646        if self._do_net_test_button:
647            v -= self._extra_button_spacing
648            self._net_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}.netTestingText'),
654                text_scale=1.0,
655                on_activate_call=self._on_net_test_press,
656            )
657        else:
658            self._net_test_button = None
659
660        v -= 70
661        self._benchmarks_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}.benchmarksText'),
667            text_scale=1.0,
668            on_activate_call=self._on_benchmark_press,
669        )
670
671        for child in self._subcontainer.get_children():
672            bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20)
673
674        if bui.app.ui_v1.use_toolbars:
675            pbtn = bui.get_special_widget('party_button')
676            bui.widget(edit=self._scrollwidget, right_widget=pbtn)
677            if self._back_button is None:
678                bui.widget(
679                    edit=self._scrollwidget,
680                    left_widget=bui.get_special_widget('back_button'),
681                )
682
683        self._restore_state()
684
685    def _show_restart_needed(self, value: Any) -> None:
686        del value  # Unused.
687        bui.screenmessage(
688            bui.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0)
689        )
690
691    def _on_lang_inform_value_change(self, val: bool) -> None:
692        plus = bui.app.plus
693        assert plus is not None
694        plus.add_v1_account_transaction(
695            {'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val}
696        )
697        plus.run_v1_account_transactions()
698
699    def _on_vr_test_press(self) -> None:
700        from bauiv1lib.settings.vrtesting import VRTestingWindow
701
702        # no-op if our underlying widget is dead or on its way out.
703        if not self._root_widget or self._root_widget.transitioning_out:
704            return
705
706        self._save_state()
707        bui.containerwidget(edit=self._root_widget, transition='out_left')
708        assert bui.app.classic is not None
709        bui.app.ui_v1.set_main_menu_window(
710            VRTestingWindow(transition='in_right').get_root_widget(),
711            from_window=self._root_widget,
712        )
713
714    def _on_net_test_press(self) -> None:
715        plus = bui.app.plus
716        assert plus is not None
717        from bauiv1lib.settings.nettesting import NetTestingWindow
718
719        # no-op if our underlying widget is dead or on its way out.
720        if not self._root_widget or self._root_widget.transitioning_out:
721            return
722
723        # Net-testing requires a signed in v1 account.
724        if plus.get_v1_account_state() != 'signed_in':
725            bui.screenmessage(
726                bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
727            )
728            bui.getsound('error').play()
729            return
730
731        self._save_state()
732        bui.containerwidget(edit=self._root_widget, transition='out_left')
733        assert bui.app.classic is not None
734        bui.app.ui_v1.set_main_menu_window(
735            NetTestingWindow(transition='in_right').get_root_widget(),
736            from_window=self._root_widget,
737        )
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 our underlying widget is dead or on its way out.
755        if not self._root_widget or self._root_widget.transitioning_out:
756            return
757
758        self._save_state()
759        bui.containerwidget(edit=self._root_widget, transition='out_left')
760        assert bui.app.classic is not None
761        bui.app.ui_v1.set_main_menu_window(
762            PluginWindow(origin_widget=self._plugins_button).get_root_widget(),
763            from_window=self._root_widget,
764        )
765
766    def _on_promo_code_press(self) -> None:
767        from bauiv1lib.promocode import PromoCodeWindow
768        from bauiv1lib.account import show_sign_in_prompt
769
770        # no-op if our underlying widget is dead or on its way out.
771        if not self._root_widget or self._root_widget.transitioning_out:
772            return
773
774        plus = bui.app.plus
775        assert plus is not None
776
777        # We have to be logged in for promo-codes to work.
778        if plus.get_v1_account_state() != 'signed_in':
779            show_sign_in_prompt()
780            return
781
782        self._save_state()
783        bui.containerwidget(edit=self._root_widget, transition='out_left')
784        assert bui.app.classic is not None
785        bui.app.ui_v1.set_main_menu_window(
786            PromoCodeWindow(
787                origin_widget=self._promo_code_button
788            ).get_root_widget(),
789            from_window=self._root_widget,
790        )
791
792    def _on_benchmark_press(self) -> None:
793        from bauiv1lib.debug import DebugWindow
794
795        # no-op if our underlying widget is dead or on its way out.
796        if not self._root_widget or self._root_widget.transitioning_out:
797            return
798
799        self._save_state()
800        bui.containerwidget(edit=self._root_widget, transition='out_left')
801        assert bui.app.classic is not None
802        bui.app.ui_v1.set_main_menu_window(
803            DebugWindow(transition='in_right').get_root_widget(),
804            from_window=self._root_widget,
805        )
806
807    def _save_state(self) -> None:
808        # pylint: disable=too-many-branches
809        try:
810            sel = self._root_widget.get_selected_child()
811            if sel == self._scrollwidget:
812                sel = self._subcontainer.get_selected_child()
813                if sel == self._vr_test_button:
814                    sel_name = 'VRTest'
815                elif sel == self._net_test_button:
816                    sel_name = 'NetTest'
817                elif sel == self._promo_code_button:
818                    sel_name = 'PromoCode'
819                elif sel == self._benchmarks_button:
820                    sel_name = 'Benchmarks'
821                elif sel == self._kick_idle_players_check_box.widget:
822                    sel_name = 'KickIdlePlayers'
823                elif sel == self._show_demos_when_idle_check_box.widget:
824                    sel_name = 'ShowDemosWhenIdle'
825                elif sel == self._show_game_ping_check_box.widget:
826                    sel_name = 'ShowPing'
827                elif sel == self._disable_camera_shake_check_box.widget:
828                    sel_name = 'DisableCameraShake'
829                elif (
830                    self._always_use_internal_keyboard_check_box is not None
831                    and sel
832                    == self._always_use_internal_keyboard_check_box.widget
833                ):
834                    sel_name = 'AlwaysUseInternalKeyboard'
835                elif (
836                    self._disable_gyro_check_box is not None
837                    and sel == self._disable_gyro_check_box.widget
838                ):
839                    sel_name = 'DisableGyro'
840                elif (
841                    self._language_popup is not None
842                    and sel == self._language_popup.get_button()
843                ):
844                    sel_name = 'Languages'
845                elif sel == self._translation_editor_button:
846                    sel_name = 'TranslationEditor'
847                elif sel == self._show_user_mods_button:
848                    sel_name = 'ShowUserMods'
849                elif sel == self._plugins_button:
850                    sel_name = 'Plugins'
851                elif sel == self._modding_guide_button:
852                    sel_name = 'ModdingGuide'
853                elif sel == self._language_inform_checkbox:
854                    sel_name = 'LangInform'
855                elif sel == self._show_dev_console_button_check_box.widget:
856                    sel_name = 'ShowDevConsole'
857                else:
858                    raise ValueError(f'unrecognized selection \'{sel}\'')
859            elif sel == self._back_button:
860                sel_name = 'Back'
861            else:
862                raise ValueError(f'unrecognized selection \'{sel}\'')
863            assert bui.app.classic is not None
864            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
865
866        except Exception:
867            logging.exception('Error saving state for %s.', self)
868
869    def _restore_state(self) -> None:
870        # pylint: disable=too-many-branches
871        try:
872            assert bui.app.classic is not None
873            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
874                'sel_name'
875            )
876            if sel_name == 'Back':
877                sel = self._back_button
878            else:
879                bui.containerwidget(
880                    edit=self._root_widget, selected_child=self._scrollwidget
881                )
882                if sel_name == 'VRTest':
883                    sel = self._vr_test_button
884                elif sel_name == 'NetTest':
885                    sel = self._net_test_button
886                elif sel_name == 'PromoCode':
887                    sel = self._promo_code_button
888                elif sel_name == 'Benchmarks':
889                    sel = self._benchmarks_button
890                elif sel_name == 'KickIdlePlayers':
891                    sel = self._kick_idle_players_check_box.widget
892                elif sel_name == 'ShowDemosWhenIdle':
893                    sel = self._show_demos_when_idle_check_box.widget
894                elif sel_name == 'ShowPing':
895                    sel = self._show_game_ping_check_box.widget
896                elif sel_name == 'DisableCameraShake':
897                    sel = self._disable_camera_shake_check_box.widget
898                elif (
899                    sel_name == 'AlwaysUseInternalKeyboard'
900                    and self._always_use_internal_keyboard_check_box is not None
901                ):
902                    sel = self._always_use_internal_keyboard_check_box.widget
903                elif (
904                    sel_name == 'DisableGyro'
905                    and self._disable_gyro_check_box is not None
906                ):
907                    sel = self._disable_gyro_check_box.widget
908                elif (
909                    sel_name == 'Languages' and self._language_popup is not None
910                ):
911                    sel = self._language_popup.get_button()
912                elif sel_name == 'TranslationEditor':
913                    sel = self._translation_editor_button
914                elif sel_name == 'ShowUserMods':
915                    sel = self._show_user_mods_button
916                elif sel_name == 'Plugins':
917                    sel = self._plugins_button
918                elif sel_name == 'ModdingGuide':
919                    sel = self._modding_guide_button
920                elif sel_name == 'LangInform':
921                    sel = self._language_inform_checkbox
922                elif sel_name == 'ShowDevConsole':
923                    sel = self._show_dev_console_button_check_box.widget
924                else:
925                    sel = None
926                if sel is not None:
927                    bui.containerwidget(
928                        edit=self._subcontainer,
929                        selected_child=sel,
930                        visible_child=sel,
931                    )
932        except Exception:
933            logging.exception('Error restoring state for %s.', self)
934
935    def _on_menu_open(self) -> None:
936        self._menu_open = True
937
938    def _on_menu_close(self) -> None:
939        self._menu_open = False
940
941    def _on_menu_choice(self, choice: str) -> None:
942        bui.app.lang.setlanguage(None if choice == 'Auto' else choice)
943        self._save_state()
944        bui.apptimer(0.1, bui.WeakCall(self._rebuild))
945
946    def _completed_langs_cb(self, results: dict[str, Any] | None) -> None:
947        if results is not None and results['langs'] is not None:
948            self._complete_langs_list = results['langs']
949            self._complete_langs_error = False
950        else:
951            self._complete_langs_list = None
952            self._complete_langs_error = True
953        bui.apptimer(0.001, bui.WeakCall(self._update_lang_status))
954
955    def _do_back(self) -> None:
956        from bauiv1lib.settings.allsettings import AllSettingsWindow
957
958        # no-op if our underlying widget is dead or on its way out.
959        if not self._root_widget or self._root_widget.transitioning_out:
960            return
961
962        self._save_state()
963        bui.containerwidget(
964            edit=self._root_widget, transition=self._transition_out
965        )
966        assert bui.app.classic is not None
967        bui.app.ui_v1.set_main_menu_window(
968            AllSettingsWindow(transition='in_left').get_root_widget(),
969            from_window=self._root_widget,
970        )
class AdvancedSettingsWindow(bauiv1._uitypes.Window):
 19class AdvancedSettingsWindow(bui.Window):
 20    """Window for editing advanced app settings."""
 21
 22    def __init__(
 23        self,
 24        transition: str = 'in_right',
 25        origin_widget: bui.Widget | None = None,
 26    ):
 27        # pylint: disable=too-many-statements
 28        import threading
 29
 30        if bui.app.classic is None:
 31            raise RuntimeError('This requires classic support.')
 32
 33        # Preload some modules we use in a background thread so we won't
 34        # have a visual hitch when the user taps them.
 35        threading.Thread(target=self._preload_modules).start()
 36
 37        app = bui.app
 38        assert app.classic is not None
 39
 40        # If they provided an origin-widget, scale up from that.
 41        scale_origin: tuple[float, float] | None
 42        if origin_widget is not None:
 43            self._transition_out = 'out_scale'
 44            scale_origin = origin_widget.get_screen_space_center()
 45            transition = 'in_scale'
 46        else:
 47            self._transition_out = 'out_right'
 48            scale_origin = None
 49
 50        uiscale = bui.app.ui_v1.uiscale
 51        self._width = 970.0 if uiscale is bui.UIScale.SMALL else 670.0
 52        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 53        self._height = (
 54            390.0
 55            if uiscale is bui.UIScale.SMALL
 56            else 450.0
 57            if uiscale is bui.UIScale.MEDIUM
 58            else 520.0
 59        )
 60        self._lang_status_text: bui.Widget | None = None
 61
 62        self._spacing = 32
 63        self._menu_open = False
 64        top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
 65
 66        super().__init__(
 67            root_widget=bui.containerwidget(
 68                size=(self._width, self._height + top_extra),
 69                transition=transition,
 70                toolbar_visibility='menu_minimal',
 71                scale_origin_stack_offset=scale_origin,
 72                scale=(
 73                    2.06
 74                    if uiscale is bui.UIScale.SMALL
 75                    else 1.4
 76                    if uiscale is bui.UIScale.MEDIUM
 77                    else 1.0
 78                ),
 79                stack_offset=(0, -25)
 80                if uiscale is bui.UIScale.SMALL
 81                else (0, 0),
 82            )
 83        )
 84
 85        self._prev_lang = ''
 86        self._prev_lang_list: list[str] = []
 87        self._complete_langs_list: list | None = None
 88        self._complete_langs_error = False
 89        self._language_popup: PopupMenu | None = None
 90
 91        # In vr-mode, the internal keyboard is currently the *only* option,
 92        # so no need to show this.
 93        self._show_always_use_internal_keyboard = not app.env.vr
 94
 95        self._scroll_width = self._width - (100 + 2 * x_inset)
 96        self._scroll_height = self._height - 115.0
 97        self._sub_width = self._scroll_width * 0.95
 98        self._sub_height = 808.0
 99
100        if self._show_always_use_internal_keyboard:
101            self._sub_height += 62
102
103        self._show_disable_gyro = app.classic.platform in {'ios', 'android'}
104        if self._show_disable_gyro:
105            self._sub_height += 42
106
107        self._do_vr_test_button = app.env.vr
108        self._do_net_test_button = True
109        self._extra_button_spacing = self._spacing * 2.5
110
111        if self._do_vr_test_button:
112            self._sub_height += self._extra_button_spacing
113        if self._do_net_test_button:
114            self._sub_height += self._extra_button_spacing
115        self._sub_height += self._spacing * 2.0  # plugins
116
117        self._r = 'settingsWindowAdvanced'
118
119        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
120            bui.containerwidget(
121                edit=self._root_widget, on_cancel_call=self._do_back
122            )
123            self._back_button = None
124        else:
125            self._back_button = bui.buttonwidget(
126                parent=self._root_widget,
127                position=(53 + x_inset, self._height - 60),
128                size=(140, 60),
129                scale=0.8,
130                autoselect=True,
131                label=bui.Lstr(resource='backText'),
132                button_type='back',
133                on_activate_call=self._do_back,
134            )
135            bui.containerwidget(
136                edit=self._root_widget, cancel_button=self._back_button
137            )
138
139        self._title_text = bui.textwidget(
140            parent=self._root_widget,
141            position=(0, self._height - 52),
142            size=(self._width, 25),
143            text=bui.Lstr(resource=f'{self._r}.titleText'),
144            color=app.ui_v1.title_color,
145            h_align='center',
146            v_align='top',
147        )
148
149        if self._back_button is not None:
150            bui.buttonwidget(
151                edit=self._back_button,
152                button_type='backSmall',
153                size=(60, 60),
154                label=bui.charstr(bui.SpecialChar.BACK),
155            )
156
157        self._scrollwidget = bui.scrollwidget(
158            parent=self._root_widget,
159            position=(50 + x_inset, 50),
160            simple_culling_v=20.0,
161            highlight=False,
162            size=(self._scroll_width, self._scroll_height),
163            selection_loops_to_parent=True,
164        )
165        bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
166        self._subcontainer = bui.containerwidget(
167            parent=self._scrollwidget,
168            size=(self._sub_width, self._sub_height),
169            background=False,
170            selection_loops_to_parent=True,
171        )
172
173        self._rebuild()
174
175        # Rebuild periodically to pick up language changes/additions/etc.
176        self._rebuild_timer = bui.AppTimer(
177            1.0, bui.WeakCall(self._rebuild), repeat=True
178        )
179
180        # Fetch the list of completed languages.
181        bui.app.classic.master_server_v1_get(
182            'bsLangGetCompleted',
183            {'b': app.env.build_number},
184            callback=bui.WeakCall(self._completed_langs_cb),
185        )
186
187    # noinspection PyUnresolvedReferences
188    @staticmethod
189    def _preload_modules() -> None:
190        """Preload modules we use; avoids hitches (called in bg thread)."""
191        from babase import modutils as _unused2
192        from bauiv1lib import config as _unused1
193        from bauiv1lib.settings import vrtesting as _unused3
194        from bauiv1lib.settings import nettesting as _unused4
195        from bauiv1lib import appinvite as _unused5
196        from bauiv1lib import account as _unused6
197        from bauiv1lib import promocode as _unused7
198        from bauiv1lib import debug as _unused8
199        from bauiv1lib.settings import plugins as _unused9
200
201    def _update_lang_status(self) -> None:
202        if self._complete_langs_list is not None:
203            up_to_date = bui.app.lang.language in self._complete_langs_list
204            bui.textwidget(
205                edit=self._lang_status_text,
206                text=''
207                if bui.app.lang.language == 'Test'
208                else bui.Lstr(
209                    resource=f'{self._r}.translationNoUpdateNeededText'
210                )
211                if up_to_date
212                else bui.Lstr(
213                    resource=f'{self._r}.translationUpdateNeededText'
214                ),
215                color=(0.2, 1.0, 0.2, 0.8)
216                if up_to_date
217                else (1.0, 0.2, 0.2, 0.8),
218            )
219        else:
220            bui.textwidget(
221                edit=self._lang_status_text,
222                text=bui.Lstr(resource=f'{self._r}.translationFetchErrorText')
223                if self._complete_langs_error
224                else bui.Lstr(
225                    resource=f'{self._r}.translationFetchingStatusText'
226                ),
227                color=(1.0, 0.5, 0.2)
228                if self._complete_langs_error
229                else (0.7, 0.7, 0.7),
230            )
231
232    def _rebuild(self) -> None:
233        # pylint: disable=too-many-statements
234        # pylint: disable=too-many-branches
235        # pylint: disable=too-many-locals
236
237        from bauiv1lib.config import ConfigCheckBox
238        from babase.modutils import show_user_scripts
239
240        plus = bui.app.plus
241        assert plus is not None
242
243        available_languages = bui.app.lang.available_languages
244
245        # Don't rebuild if the menu is open or if our language and
246        # language-list hasn't changed.
247
248        # NOTE - although we now support widgets updating their own
249        # translations, we still change the label formatting on the language
250        # menu based on the language so still need this. ...however we could
251        # make this more limited to it only rebuilds that one menu instead
252        # of everything.
253        if self._menu_open or (
254            self._prev_lang == bui.app.config.get('Lang', None)
255            and self._prev_lang_list == available_languages
256        ):
257            return
258        self._prev_lang = bui.app.config.get('Lang', None)
259        self._prev_lang_list = available_languages
260
261        # Clear out our sub-container.
262        children = self._subcontainer.get_children()
263        for child in children:
264            child.delete()
265
266        v = self._sub_height - 35
267
268        v -= self._spacing * 1.2
269
270        # Update our existing back button and title.
271        if self._back_button is not None:
272            bui.buttonwidget(
273                edit=self._back_button, label=bui.Lstr(resource='backText')
274            )
275            bui.buttonwidget(
276                edit=self._back_button, label=bui.charstr(bui.SpecialChar.BACK)
277            )
278
279        bui.textwidget(
280            edit=self._title_text,
281            text=bui.Lstr(resource=f'{self._r}.titleText'),
282        )
283
284        this_button_width = 410
285
286        self._promo_code_button = bui.buttonwidget(
287            parent=self._subcontainer,
288            position=(self._sub_width / 2 - this_button_width / 2, v - 14),
289            size=(this_button_width, 60),
290            autoselect=True,
291            label=bui.Lstr(resource=f'{self._r}.enterPromoCodeText'),
292            text_scale=1.0,
293            on_activate_call=self._on_promo_code_press,
294        )
295        if self._back_button is not None:
296            bui.widget(
297                edit=self._promo_code_button,
298                up_widget=self._back_button,
299                left_widget=self._back_button,
300            )
301        v -= self._extra_button_spacing * 0.8
302
303        assert bui.app.classic is not None
304        bui.textwidget(
305            parent=self._subcontainer,
306            position=(200, v + 10),
307            size=(0, 0),
308            text=bui.Lstr(resource=f'{self._r}.languageText'),
309            maxwidth=150,
310            scale=0.95,
311            color=bui.app.ui_v1.title_color,
312            h_align='right',
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=(self._sub_width * 0.5, 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='center',
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 -= 40
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_dev_console_button_check_box = ConfigCheckBox(
482            parent=self._subcontainer,
483            position=(50, v),
484            size=(self._sub_width - 100, 30),
485            configkey='Show Dev Console Button',
486            displayname=bui.Lstr(
487                resource=f'{self._r}.showDevConsoleButtonText'
488            ),
489            scale=1.0,
490            maxwidth=430,
491        )
492
493        v -= 42
494        self._show_demos_when_idle_check_box = ConfigCheckBox(
495            parent=self._subcontainer,
496            position=(50, v),
497            size=(self._sub_width - 100, 30),
498            configkey='Show Demos When Idle',
499            displayname=bui.Lstr(resource=f'{self._r}.showDemosWhenIdleText'),
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        if self._show_always_use_internal_keyboard:
580            assert self._always_use_internal_keyboard_check_box is not None
581            bui.widget(
582                edit=self._always_use_internal_keyboard_check_box.widget,
583                down_widget=self._modding_guide_button,
584            )
585            bui.widget(
586                edit=self._modding_guide_button,
587                up_widget=self._always_use_internal_keyboard_check_box.widget,
588            )
589        else:
590            # ew.
591            next_widget_up = (
592                self._disable_gyro_check_box.widget
593                if self._disable_gyro_check_box is not None
594                else self._disable_camera_shake_check_box.widget
595            )
596            bui.widget(
597                edit=self._modding_guide_button,
598                up_widget=next_widget_up,
599            )
600            bui.widget(
601                edit=next_widget_up,
602                down_widget=self._modding_guide_button,
603            )
604
605        v -= self._spacing * 2.0
606
607        self._show_user_mods_button = bui.buttonwidget(
608            parent=self._subcontainer,
609            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
610            size=(this_button_width, 60),
611            autoselect=True,
612            label=bui.Lstr(resource=f'{self._r}.showUserModsText'),
613            text_scale=1.0,
614            on_activate_call=show_user_scripts,
615        )
616
617        v -= self._spacing * 2.0
618
619        self._plugins_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='pluginsText'),
625            text_scale=1.0,
626            on_activate_call=self._on_plugins_button_press,
627        )
628
629        v -= self._spacing * 0.6
630
631        self._vr_test_button: bui.Widget | None
632        if self._do_vr_test_button:
633            v -= self._extra_button_spacing
634            self._vr_test_button = bui.buttonwidget(
635                parent=self._subcontainer,
636                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
637                size=(this_button_width, 60),
638                autoselect=True,
639                label=bui.Lstr(resource=f'{self._r}.vrTestingText'),
640                text_scale=1.0,
641                on_activate_call=self._on_vr_test_press,
642            )
643        else:
644            self._vr_test_button = None
645
646        self._net_test_button: bui.Widget | None
647        if self._do_net_test_button:
648            v -= self._extra_button_spacing
649            self._net_test_button = bui.buttonwidget(
650                parent=self._subcontainer,
651                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
652                size=(this_button_width, 60),
653                autoselect=True,
654                label=bui.Lstr(resource=f'{self._r}.netTestingText'),
655                text_scale=1.0,
656                on_activate_call=self._on_net_test_press,
657            )
658        else:
659            self._net_test_button = None
660
661        v -= 70
662        self._benchmarks_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}.benchmarksText'),
668            text_scale=1.0,
669            on_activate_call=self._on_benchmark_press,
670        )
671
672        for child in self._subcontainer.get_children():
673            bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20)
674
675        if bui.app.ui_v1.use_toolbars:
676            pbtn = bui.get_special_widget('party_button')
677            bui.widget(edit=self._scrollwidget, right_widget=pbtn)
678            if self._back_button is None:
679                bui.widget(
680                    edit=self._scrollwidget,
681                    left_widget=bui.get_special_widget('back_button'),
682                )
683
684        self._restore_state()
685
686    def _show_restart_needed(self, value: Any) -> None:
687        del value  # Unused.
688        bui.screenmessage(
689            bui.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0)
690        )
691
692    def _on_lang_inform_value_change(self, val: bool) -> None:
693        plus = bui.app.plus
694        assert plus is not None
695        plus.add_v1_account_transaction(
696            {'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val}
697        )
698        plus.run_v1_account_transactions()
699
700    def _on_vr_test_press(self) -> None:
701        from bauiv1lib.settings.vrtesting import VRTestingWindow
702
703        # no-op if our underlying widget is dead or on its way out.
704        if not self._root_widget or self._root_widget.transitioning_out:
705            return
706
707        self._save_state()
708        bui.containerwidget(edit=self._root_widget, transition='out_left')
709        assert bui.app.classic is not None
710        bui.app.ui_v1.set_main_menu_window(
711            VRTestingWindow(transition='in_right').get_root_widget(),
712            from_window=self._root_widget,
713        )
714
715    def _on_net_test_press(self) -> None:
716        plus = bui.app.plus
717        assert plus is not None
718        from bauiv1lib.settings.nettesting import NetTestingWindow
719
720        # no-op if our underlying widget is dead or on its way out.
721        if not self._root_widget or self._root_widget.transitioning_out:
722            return
723
724        # Net-testing requires a signed in v1 account.
725        if plus.get_v1_account_state() != 'signed_in':
726            bui.screenmessage(
727                bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
728            )
729            bui.getsound('error').play()
730            return
731
732        self._save_state()
733        bui.containerwidget(edit=self._root_widget, transition='out_left')
734        assert bui.app.classic is not None
735        bui.app.ui_v1.set_main_menu_window(
736            NetTestingWindow(transition='in_right').get_root_widget(),
737            from_window=self._root_widget,
738        )
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 our underlying widget is dead or on its way out.
756        if not self._root_widget or self._root_widget.transitioning_out:
757            return
758
759        self._save_state()
760        bui.containerwidget(edit=self._root_widget, transition='out_left')
761        assert bui.app.classic is not None
762        bui.app.ui_v1.set_main_menu_window(
763            PluginWindow(origin_widget=self._plugins_button).get_root_widget(),
764            from_window=self._root_widget,
765        )
766
767    def _on_promo_code_press(self) -> None:
768        from bauiv1lib.promocode import PromoCodeWindow
769        from bauiv1lib.account import show_sign_in_prompt
770
771        # no-op if our underlying widget is dead or on its way out.
772        if not self._root_widget or self._root_widget.transitioning_out:
773            return
774
775        plus = bui.app.plus
776        assert plus is not None
777
778        # We have to be logged in for promo-codes to work.
779        if plus.get_v1_account_state() != 'signed_in':
780            show_sign_in_prompt()
781            return
782
783        self._save_state()
784        bui.containerwidget(edit=self._root_widget, transition='out_left')
785        assert bui.app.classic is not None
786        bui.app.ui_v1.set_main_menu_window(
787            PromoCodeWindow(
788                origin_widget=self._promo_code_button
789            ).get_root_widget(),
790            from_window=self._root_widget,
791        )
792
793    def _on_benchmark_press(self) -> None:
794        from bauiv1lib.debug import DebugWindow
795
796        # no-op if our underlying widget is dead or on its way out.
797        if not self._root_widget or self._root_widget.transitioning_out:
798            return
799
800        self._save_state()
801        bui.containerwidget(edit=self._root_widget, transition='out_left')
802        assert bui.app.classic is not None
803        bui.app.ui_v1.set_main_menu_window(
804            DebugWindow(transition='in_right').get_root_widget(),
805            from_window=self._root_widget,
806        )
807
808    def _save_state(self) -> None:
809        # pylint: disable=too-many-branches
810        try:
811            sel = self._root_widget.get_selected_child()
812            if sel == self._scrollwidget:
813                sel = self._subcontainer.get_selected_child()
814                if sel == self._vr_test_button:
815                    sel_name = 'VRTest'
816                elif sel == self._net_test_button:
817                    sel_name = 'NetTest'
818                elif sel == self._promo_code_button:
819                    sel_name = 'PromoCode'
820                elif sel == self._benchmarks_button:
821                    sel_name = 'Benchmarks'
822                elif sel == self._kick_idle_players_check_box.widget:
823                    sel_name = 'KickIdlePlayers'
824                elif sel == self._show_demos_when_idle_check_box.widget:
825                    sel_name = 'ShowDemosWhenIdle'
826                elif sel == self._show_game_ping_check_box.widget:
827                    sel_name = 'ShowPing'
828                elif sel == self._disable_camera_shake_check_box.widget:
829                    sel_name = 'DisableCameraShake'
830                elif (
831                    self._always_use_internal_keyboard_check_box is not None
832                    and sel
833                    == self._always_use_internal_keyboard_check_box.widget
834                ):
835                    sel_name = 'AlwaysUseInternalKeyboard'
836                elif (
837                    self._disable_gyro_check_box is not None
838                    and sel == self._disable_gyro_check_box.widget
839                ):
840                    sel_name = 'DisableGyro'
841                elif (
842                    self._language_popup is not None
843                    and sel == self._language_popup.get_button()
844                ):
845                    sel_name = 'Languages'
846                elif sel == self._translation_editor_button:
847                    sel_name = 'TranslationEditor'
848                elif sel == self._show_user_mods_button:
849                    sel_name = 'ShowUserMods'
850                elif sel == self._plugins_button:
851                    sel_name = 'Plugins'
852                elif sel == self._modding_guide_button:
853                    sel_name = 'ModdingGuide'
854                elif sel == self._language_inform_checkbox:
855                    sel_name = 'LangInform'
856                elif sel == self._show_dev_console_button_check_box.widget:
857                    sel_name = 'ShowDevConsole'
858                else:
859                    raise ValueError(f'unrecognized selection \'{sel}\'')
860            elif sel == self._back_button:
861                sel_name = 'Back'
862            else:
863                raise ValueError(f'unrecognized selection \'{sel}\'')
864            assert bui.app.classic is not None
865            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
866
867        except Exception:
868            logging.exception('Error saving state for %s.', self)
869
870    def _restore_state(self) -> None:
871        # pylint: disable=too-many-branches
872        try:
873            assert bui.app.classic is not None
874            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
875                'sel_name'
876            )
877            if sel_name == 'Back':
878                sel = self._back_button
879            else:
880                bui.containerwidget(
881                    edit=self._root_widget, selected_child=self._scrollwidget
882                )
883                if sel_name == 'VRTest':
884                    sel = self._vr_test_button
885                elif sel_name == 'NetTest':
886                    sel = self._net_test_button
887                elif sel_name == 'PromoCode':
888                    sel = self._promo_code_button
889                elif sel_name == 'Benchmarks':
890                    sel = self._benchmarks_button
891                elif sel_name == 'KickIdlePlayers':
892                    sel = self._kick_idle_players_check_box.widget
893                elif sel_name == 'ShowDemosWhenIdle':
894                    sel = self._show_demos_when_idle_check_box.widget
895                elif sel_name == 'ShowPing':
896                    sel = self._show_game_ping_check_box.widget
897                elif sel_name == 'DisableCameraShake':
898                    sel = self._disable_camera_shake_check_box.widget
899                elif (
900                    sel_name == 'AlwaysUseInternalKeyboard'
901                    and self._always_use_internal_keyboard_check_box is not None
902                ):
903                    sel = self._always_use_internal_keyboard_check_box.widget
904                elif (
905                    sel_name == 'DisableGyro'
906                    and self._disable_gyro_check_box is not None
907                ):
908                    sel = self._disable_gyro_check_box.widget
909                elif (
910                    sel_name == 'Languages' and self._language_popup is not None
911                ):
912                    sel = self._language_popup.get_button()
913                elif sel_name == 'TranslationEditor':
914                    sel = self._translation_editor_button
915                elif sel_name == 'ShowUserMods':
916                    sel = self._show_user_mods_button
917                elif sel_name == 'Plugins':
918                    sel = self._plugins_button
919                elif sel_name == 'ModdingGuide':
920                    sel = self._modding_guide_button
921                elif sel_name == 'LangInform':
922                    sel = self._language_inform_checkbox
923                elif sel_name == 'ShowDevConsole':
924                    sel = self._show_dev_console_button_check_box.widget
925                else:
926                    sel = None
927                if sel is not None:
928                    bui.containerwidget(
929                        edit=self._subcontainer,
930                        selected_child=sel,
931                        visible_child=sel,
932                    )
933        except Exception:
934            logging.exception('Error restoring state for %s.', self)
935
936    def _on_menu_open(self) -> None:
937        self._menu_open = True
938
939    def _on_menu_close(self) -> None:
940        self._menu_open = False
941
942    def _on_menu_choice(self, choice: str) -> None:
943        bui.app.lang.setlanguage(None if choice == 'Auto' else choice)
944        self._save_state()
945        bui.apptimer(0.1, bui.WeakCall(self._rebuild))
946
947    def _completed_langs_cb(self, results: dict[str, Any] | None) -> None:
948        if results is not None and results['langs'] is not None:
949            self._complete_langs_list = results['langs']
950            self._complete_langs_error = False
951        else:
952            self._complete_langs_list = None
953            self._complete_langs_error = True
954        bui.apptimer(0.001, bui.WeakCall(self._update_lang_status))
955
956    def _do_back(self) -> None:
957        from bauiv1lib.settings.allsettings import AllSettingsWindow
958
959        # no-op if our underlying widget is dead or on its way out.
960        if not self._root_widget or self._root_widget.transitioning_out:
961            return
962
963        self._save_state()
964        bui.containerwidget(
965            edit=self._root_widget, transition=self._transition_out
966        )
967        assert bui.app.classic is not None
968        bui.app.ui_v1.set_main_menu_window(
969            AllSettingsWindow(transition='in_left').get_root_widget(),
970            from_window=self._root_widget,
971        )

Window for editing advanced app settings.

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