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 = 870.0 if uiscale is bui.UIScale.SMALL else 670.0
 51        x_inset = 100 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 = 766.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._disable_camera_shake_check_box = ConfigCheckBox(
494            parent=self._subcontainer,
495            position=(50, v),
496            size=(self._sub_width - 100, 30),
497            configkey='Disable Camera Shake',
498            displayname=bui.Lstr(resource=f'{self._r}.disableCameraShakeText'),
499            scale=1.0,
500            maxwidth=430,
501        )
502
503        self._disable_gyro_check_box: ConfigCheckBox | None = None
504        if self._show_disable_gyro:
505            v -= 42
506            self._disable_gyro_check_box = ConfigCheckBox(
507                parent=self._subcontainer,
508                position=(50, v),
509                size=(self._sub_width - 100, 30),
510                configkey='Disable Camera Gyro',
511                displayname=bui.Lstr(
512                    resource=f'{self._r}.disableCameraGyroscopeMotionText'
513                ),
514                scale=1.0,
515                maxwidth=430,
516            )
517
518        self._always_use_internal_keyboard_check_box: ConfigCheckBox | None
519        if self._show_always_use_internal_keyboard:
520            v -= 42
521            self._always_use_internal_keyboard_check_box = ConfigCheckBox(
522                parent=self._subcontainer,
523                position=(50, v),
524                size=(self._sub_width - 100, 30),
525                configkey='Always Use Internal Keyboard',
526                autoselect=True,
527                displayname=bui.Lstr(
528                    resource=f'{self._r}.alwaysUseInternalKeyboardText'
529                ),
530                scale=1.0,
531                maxwidth=430,
532            )
533            bui.textwidget(
534                parent=self._subcontainer,
535                position=(90, v - 10),
536                size=(0, 0),
537                text=bui.Lstr(
538                    resource=(
539                        f'{self._r}.alwaysUseInternalKeyboardDescriptionText'
540                    )
541                ),
542                maxwidth=400,
543                flatness=1.0,
544                scale=0.65,
545                color=(0.4, 0.9, 0.4, 0.8),
546                h_align='left',
547                v_align='center',
548            )
549            v -= 20
550        else:
551            self._always_use_internal_keyboard_check_box = None
552
553        v -= self._spacing * 2.1
554
555        this_button_width = 410
556        self._modding_guide_button = bui.buttonwidget(
557            parent=self._subcontainer,
558            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
559            size=(this_button_width, 60),
560            autoselect=True,
561            label=bui.Lstr(resource=f'{self._r}.moddingGuideText'),
562            text_scale=1.0,
563            on_activate_call=bui.Call(
564                bui.open_url, 'https://ballistica.net/wiki/modding-guide'
565            ),
566        )
567        if self._show_always_use_internal_keyboard:
568            assert self._always_use_internal_keyboard_check_box is not None
569            bui.widget(
570                edit=self._always_use_internal_keyboard_check_box.widget,
571                down_widget=self._modding_guide_button,
572            )
573            bui.widget(
574                edit=self._modding_guide_button,
575                up_widget=self._always_use_internal_keyboard_check_box.widget,
576            )
577        else:
578            bui.widget(
579                edit=self._modding_guide_button,
580                up_widget=self._kick_idle_players_check_box.widget,
581            )
582            bui.widget(
583                edit=self._kick_idle_players_check_box.widget,
584                down_widget=self._modding_guide_button,
585            )
586
587        v -= self._spacing * 2.0
588
589        self._show_user_mods_button = bui.buttonwidget(
590            parent=self._subcontainer,
591            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
592            size=(this_button_width, 60),
593            autoselect=True,
594            label=bui.Lstr(resource=f'{self._r}.showUserModsText'),
595            text_scale=1.0,
596            on_activate_call=show_user_scripts,
597        )
598
599        v -= self._spacing * 2.0
600
601        self._plugins_button = bui.buttonwidget(
602            parent=self._subcontainer,
603            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
604            size=(this_button_width, 60),
605            autoselect=True,
606            label=bui.Lstr(resource='pluginsText'),
607            text_scale=1.0,
608            on_activate_call=self._on_plugins_button_press,
609        )
610
611        v -= self._spacing * 0.6
612
613        self._vr_test_button: bui.Widget | None
614        if self._do_vr_test_button:
615            v -= self._extra_button_spacing
616            self._vr_test_button = bui.buttonwidget(
617                parent=self._subcontainer,
618                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
619                size=(this_button_width, 60),
620                autoselect=True,
621                label=bui.Lstr(resource=f'{self._r}.vrTestingText'),
622                text_scale=1.0,
623                on_activate_call=self._on_vr_test_press,
624            )
625        else:
626            self._vr_test_button = None
627
628        self._net_test_button: bui.Widget | None
629        if self._do_net_test_button:
630            v -= self._extra_button_spacing
631            self._net_test_button = bui.buttonwidget(
632                parent=self._subcontainer,
633                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
634                size=(this_button_width, 60),
635                autoselect=True,
636                label=bui.Lstr(resource=f'{self._r}.netTestingText'),
637                text_scale=1.0,
638                on_activate_call=self._on_net_test_press,
639            )
640        else:
641            self._net_test_button = None
642
643        v -= 70
644        self._benchmarks_button = bui.buttonwidget(
645            parent=self._subcontainer,
646            position=(self._sub_width / 2 - this_button_width / 2, v - 14),
647            size=(this_button_width, 60),
648            autoselect=True,
649            label=bui.Lstr(resource=f'{self._r}.benchmarksText'),
650            text_scale=1.0,
651            on_activate_call=self._on_benchmark_press,
652        )
653
654        for child in self._subcontainer.get_children():
655            bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20)
656
657        if bui.app.ui_v1.use_toolbars:
658            pbtn = bui.get_special_widget('party_button')
659            bui.widget(edit=self._scrollwidget, right_widget=pbtn)
660            if self._back_button is None:
661                bui.widget(
662                    edit=self._scrollwidget,
663                    left_widget=bui.get_special_widget('back_button'),
664                )
665
666        self._restore_state()
667
668    def _show_restart_needed(self, value: Any) -> None:
669        del value  # Unused.
670        bui.screenmessage(
671            bui.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0)
672        )
673
674    def _on_lang_inform_value_change(self, val: bool) -> None:
675        plus = bui.app.plus
676        assert plus is not None
677        plus.add_v1_account_transaction(
678            {'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val}
679        )
680        plus.run_v1_account_transactions()
681
682    def _on_vr_test_press(self) -> None:
683        from bauiv1lib.settings.vrtesting import VRTestingWindow
684
685        self._save_state()
686        bui.containerwidget(edit=self._root_widget, transition='out_left')
687        assert bui.app.classic is not None
688        bui.app.ui_v1.set_main_menu_window(
689            VRTestingWindow(transition='in_right').get_root_widget()
690        )
691
692    def _on_net_test_press(self) -> None:
693        plus = bui.app.plus
694        assert plus is not None
695        from bauiv1lib.settings.nettesting import NetTestingWindow
696
697        # Net-testing requires a signed in v1 account.
698        if plus.get_v1_account_state() != 'signed_in':
699            bui.screenmessage(
700                bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
701            )
702            bui.getsound('error').play()
703            return
704
705        self._save_state()
706        bui.containerwidget(edit=self._root_widget, transition='out_left')
707        assert bui.app.classic is not None
708        bui.app.ui_v1.set_main_menu_window(
709            NetTestingWindow(transition='in_right').get_root_widget()
710        )
711
712    def _on_friend_promo_code_press(self) -> None:
713        from bauiv1lib import appinvite
714        from bauiv1lib import account
715
716        plus = bui.app.plus
717        assert plus is not None
718
719        if plus.get_v1_account_state() != 'signed_in':
720            account.show_sign_in_prompt()
721            return
722        appinvite.handle_app_invites_press()
723
724    def _on_plugins_button_press(self) -> None:
725        from bauiv1lib.settings.plugins import PluginWindow
726
727        self._save_state()
728        bui.containerwidget(edit=self._root_widget, transition='out_left')
729        assert bui.app.classic is not None
730        bui.app.ui_v1.set_main_menu_window(
731            PluginWindow(origin_widget=self._plugins_button).get_root_widget()
732        )
733
734    def _on_promo_code_press(self) -> None:
735        from bauiv1lib.promocode import PromoCodeWindow
736        from bauiv1lib.account import show_sign_in_prompt
737
738        plus = bui.app.plus
739        assert plus is not None
740
741        # We have to be logged in for promo-codes to work.
742        if plus.get_v1_account_state() != 'signed_in':
743            show_sign_in_prompt()
744            return
745        self._save_state()
746        bui.containerwidget(edit=self._root_widget, transition='out_left')
747        assert bui.app.classic is not None
748        bui.app.ui_v1.set_main_menu_window(
749            PromoCodeWindow(
750                origin_widget=self._promo_code_button
751            ).get_root_widget()
752        )
753
754    def _on_benchmark_press(self) -> None:
755        from bauiv1lib.debug import DebugWindow
756
757        self._save_state()
758        bui.containerwidget(edit=self._root_widget, transition='out_left')
759        assert bui.app.classic is not None
760        bui.app.ui_v1.set_main_menu_window(
761            DebugWindow(transition='in_right').get_root_widget()
762        )
763
764    def _save_state(self) -> None:
765        # pylint: disable=too-many-branches
766        try:
767            sel = self._root_widget.get_selected_child()
768            if sel == self._scrollwidget:
769                sel = self._subcontainer.get_selected_child()
770                if sel == self._vr_test_button:
771                    sel_name = 'VRTest'
772                elif sel == self._net_test_button:
773                    sel_name = 'NetTest'
774                elif sel == self._promo_code_button:
775                    sel_name = 'PromoCode'
776                elif sel == self._benchmarks_button:
777                    sel_name = 'Benchmarks'
778                elif sel == self._kick_idle_players_check_box.widget:
779                    sel_name = 'KickIdlePlayers'
780                elif sel == self._show_game_ping_check_box.widget:
781                    sel_name = 'ShowPing'
782                elif sel == self._disable_camera_shake_check_box.widget:
783                    sel_name = 'DisableCameraShake'
784                elif (
785                    self._always_use_internal_keyboard_check_box is not None
786                    and sel
787                    == self._always_use_internal_keyboard_check_box.widget
788                ):
789                    sel_name = 'AlwaysUseInternalKeyboard'
790                elif (
791                    self._disable_gyro_check_box is not None
792                    and sel == self._disable_gyro_check_box.widget
793                ):
794                    sel_name = 'DisableGyro'
795                elif (
796                    self._language_popup is not None
797                    and sel == self._language_popup.get_button()
798                ):
799                    sel_name = 'Languages'
800                elif sel == self._translation_editor_button:
801                    sel_name = 'TranslationEditor'
802                elif sel == self._show_user_mods_button:
803                    sel_name = 'ShowUserMods'
804                elif sel == self._plugins_button:
805                    sel_name = 'Plugins'
806                elif sel == self._modding_guide_button:
807                    sel_name = 'ModdingGuide'
808                elif sel == self._language_inform_checkbox:
809                    sel_name = 'LangInform'
810                else:
811                    raise ValueError(f'unrecognized selection \'{sel}\'')
812            elif sel == self._back_button:
813                sel_name = 'Back'
814            else:
815                raise ValueError(f'unrecognized selection \'{sel}\'')
816            assert bui.app.classic is not None
817            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
818
819        except Exception:
820            logging.exception('Error saving state for %s.', self)
821
822    def _restore_state(self) -> None:
823        # pylint: disable=too-many-branches
824        try:
825            assert bui.app.classic is not None
826            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
827                'sel_name'
828            )
829            if sel_name == 'Back':
830                sel = self._back_button
831            else:
832                bui.containerwidget(
833                    edit=self._root_widget, selected_child=self._scrollwidget
834                )
835                if sel_name == 'VRTest':
836                    sel = self._vr_test_button
837                elif sel_name == 'NetTest':
838                    sel = self._net_test_button
839                elif sel_name == 'PromoCode':
840                    sel = self._promo_code_button
841                elif sel_name == 'Benchmarks':
842                    sel = self._benchmarks_button
843                elif sel_name == 'KickIdlePlayers':
844                    sel = self._kick_idle_players_check_box.widget
845                elif sel_name == 'ShowPing':
846                    sel = self._show_game_ping_check_box.widget
847                elif sel_name == 'DisableCameraShake':
848                    sel = self._disable_camera_shake_check_box.widget
849                elif (
850                    sel_name == 'AlwaysUseInternalKeyboard'
851                    and self._always_use_internal_keyboard_check_box is not None
852                ):
853                    sel = self._always_use_internal_keyboard_check_box.widget
854                elif (
855                    sel_name == 'DisableGyro'
856                    and self._disable_gyro_check_box is not None
857                ):
858                    sel = self._disable_gyro_check_box.widget
859                elif (
860                    sel_name == 'Languages' and self._language_popup is not None
861                ):
862                    sel = self._language_popup.get_button()
863                elif sel_name == 'TranslationEditor':
864                    sel = self._translation_editor_button
865                elif sel_name == 'ShowUserMods':
866                    sel = self._show_user_mods_button
867                elif sel_name == 'Plugins':
868                    sel = self._plugins_button
869                elif sel_name == 'ModdingGuide':
870                    sel = self._modding_guide_button
871                elif sel_name == 'LangInform':
872                    sel = self._language_inform_checkbox
873                else:
874                    sel = None
875                if sel is not None:
876                    bui.containerwidget(
877                        edit=self._subcontainer,
878                        selected_child=sel,
879                        visible_child=sel,
880                    )
881        except Exception:
882            logging.exception('Error restoring state for %s.', self)
883
884    def _on_menu_open(self) -> None:
885        self._menu_open = True
886
887    def _on_menu_close(self) -> None:
888        self._menu_open = False
889
890    def _on_menu_choice(self, choice: str) -> None:
891        bui.app.lang.setlanguage(None if choice == 'Auto' else choice)
892        self._save_state()
893        bui.apptimer(0.1, bui.WeakCall(self._rebuild))
894
895    def _completed_langs_cb(self, results: dict[str, Any] | None) -> None:
896        if results is not None and results['langs'] is not None:
897            self._complete_langs_list = results['langs']
898            self._complete_langs_error = False
899        else:
900            self._complete_langs_list = None
901            self._complete_langs_error = True
902        bui.apptimer(0.001, bui.WeakCall(self._update_lang_status))
903
904    def _do_back(self) -> None:
905        from bauiv1lib.settings.allsettings import AllSettingsWindow
906
907        self._save_state()
908        bui.containerwidget(
909            edit=self._root_widget, transition=self._transition_out
910        )
911        assert bui.app.classic is not None
912        bui.app.ui_v1.set_main_menu_window(
913            AllSettingsWindow(transition='in_left').get_root_widget()
914        )
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 = 870.0 if uiscale is bui.UIScale.SMALL else 670.0
 52        x_inset = 100 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 = 766.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._disable_camera_shake_check_box = ConfigCheckBox(
495            parent=self._subcontainer,
496            position=(50, v),
497            size=(self._sub_width - 100, 30),
498            configkey='Disable Camera Shake',
499            displayname=bui.Lstr(resource=f'{self._r}.disableCameraShakeText'),
500            scale=1.0,
501            maxwidth=430,
502        )
503
504        self._disable_gyro_check_box: ConfigCheckBox | None = None
505        if self._show_disable_gyro:
506            v -= 42
507            self._disable_gyro_check_box = ConfigCheckBox(
508                parent=self._subcontainer,
509                position=(50, v),
510                size=(self._sub_width - 100, 30),
511                configkey='Disable Camera Gyro',
512                displayname=bui.Lstr(
513                    resource=f'{self._r}.disableCameraGyroscopeMotionText'
514                ),
515                scale=1.0,
516                maxwidth=430,
517            )
518
519        self._always_use_internal_keyboard_check_box: ConfigCheckBox | None
520        if self._show_always_use_internal_keyboard:
521            v -= 42
522            self._always_use_internal_keyboard_check_box = ConfigCheckBox(
523                parent=self._subcontainer,
524                position=(50, v),
525                size=(self._sub_width - 100, 30),
526                configkey='Always Use Internal Keyboard',
527                autoselect=True,
528                displayname=bui.Lstr(
529                    resource=f'{self._r}.alwaysUseInternalKeyboardText'
530                ),
531                scale=1.0,
532                maxwidth=430,
533            )
534            bui.textwidget(
535                parent=self._subcontainer,
536                position=(90, v - 10),
537                size=(0, 0),
538                text=bui.Lstr(
539                    resource=(
540                        f'{self._r}.alwaysUseInternalKeyboardDescriptionText'
541                    )
542                ),
543                maxwidth=400,
544                flatness=1.0,
545                scale=0.65,
546                color=(0.4, 0.9, 0.4, 0.8),
547                h_align='left',
548                v_align='center',
549            )
550            v -= 20
551        else:
552            self._always_use_internal_keyboard_check_box = None
553
554        v -= self._spacing * 2.1
555
556        this_button_width = 410
557        self._modding_guide_button = bui.buttonwidget(
558            parent=self._subcontainer,
559            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
560            size=(this_button_width, 60),
561            autoselect=True,
562            label=bui.Lstr(resource=f'{self._r}.moddingGuideText'),
563            text_scale=1.0,
564            on_activate_call=bui.Call(
565                bui.open_url, 'https://ballistica.net/wiki/modding-guide'
566            ),
567        )
568        if self._show_always_use_internal_keyboard:
569            assert self._always_use_internal_keyboard_check_box is not None
570            bui.widget(
571                edit=self._always_use_internal_keyboard_check_box.widget,
572                down_widget=self._modding_guide_button,
573            )
574            bui.widget(
575                edit=self._modding_guide_button,
576                up_widget=self._always_use_internal_keyboard_check_box.widget,
577            )
578        else:
579            bui.widget(
580                edit=self._modding_guide_button,
581                up_widget=self._kick_idle_players_check_box.widget,
582            )
583            bui.widget(
584                edit=self._kick_idle_players_check_box.widget,
585                down_widget=self._modding_guide_button,
586            )
587
588        v -= self._spacing * 2.0
589
590        self._show_user_mods_button = bui.buttonwidget(
591            parent=self._subcontainer,
592            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
593            size=(this_button_width, 60),
594            autoselect=True,
595            label=bui.Lstr(resource=f'{self._r}.showUserModsText'),
596            text_scale=1.0,
597            on_activate_call=show_user_scripts,
598        )
599
600        v -= self._spacing * 2.0
601
602        self._plugins_button = bui.buttonwidget(
603            parent=self._subcontainer,
604            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
605            size=(this_button_width, 60),
606            autoselect=True,
607            label=bui.Lstr(resource='pluginsText'),
608            text_scale=1.0,
609            on_activate_call=self._on_plugins_button_press,
610        )
611
612        v -= self._spacing * 0.6
613
614        self._vr_test_button: bui.Widget | None
615        if self._do_vr_test_button:
616            v -= self._extra_button_spacing
617            self._vr_test_button = bui.buttonwidget(
618                parent=self._subcontainer,
619                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
620                size=(this_button_width, 60),
621                autoselect=True,
622                label=bui.Lstr(resource=f'{self._r}.vrTestingText'),
623                text_scale=1.0,
624                on_activate_call=self._on_vr_test_press,
625            )
626        else:
627            self._vr_test_button = None
628
629        self._net_test_button: bui.Widget | None
630        if self._do_net_test_button:
631            v -= self._extra_button_spacing
632            self._net_test_button = bui.buttonwidget(
633                parent=self._subcontainer,
634                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
635                size=(this_button_width, 60),
636                autoselect=True,
637                label=bui.Lstr(resource=f'{self._r}.netTestingText'),
638                text_scale=1.0,
639                on_activate_call=self._on_net_test_press,
640            )
641        else:
642            self._net_test_button = None
643
644        v -= 70
645        self._benchmarks_button = bui.buttonwidget(
646            parent=self._subcontainer,
647            position=(self._sub_width / 2 - this_button_width / 2, v - 14),
648            size=(this_button_width, 60),
649            autoselect=True,
650            label=bui.Lstr(resource=f'{self._r}.benchmarksText'),
651            text_scale=1.0,
652            on_activate_call=self._on_benchmark_press,
653        )
654
655        for child in self._subcontainer.get_children():
656            bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20)
657
658        if bui.app.ui_v1.use_toolbars:
659            pbtn = bui.get_special_widget('party_button')
660            bui.widget(edit=self._scrollwidget, right_widget=pbtn)
661            if self._back_button is None:
662                bui.widget(
663                    edit=self._scrollwidget,
664                    left_widget=bui.get_special_widget('back_button'),
665                )
666
667        self._restore_state()
668
669    def _show_restart_needed(self, value: Any) -> None:
670        del value  # Unused.
671        bui.screenmessage(
672            bui.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0)
673        )
674
675    def _on_lang_inform_value_change(self, val: bool) -> None:
676        plus = bui.app.plus
677        assert plus is not None
678        plus.add_v1_account_transaction(
679            {'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val}
680        )
681        plus.run_v1_account_transactions()
682
683    def _on_vr_test_press(self) -> None:
684        from bauiv1lib.settings.vrtesting import VRTestingWindow
685
686        self._save_state()
687        bui.containerwidget(edit=self._root_widget, transition='out_left')
688        assert bui.app.classic is not None
689        bui.app.ui_v1.set_main_menu_window(
690            VRTestingWindow(transition='in_right').get_root_widget()
691        )
692
693    def _on_net_test_press(self) -> None:
694        plus = bui.app.plus
695        assert plus is not None
696        from bauiv1lib.settings.nettesting import NetTestingWindow
697
698        # Net-testing requires a signed in v1 account.
699        if plus.get_v1_account_state() != 'signed_in':
700            bui.screenmessage(
701                bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
702            )
703            bui.getsound('error').play()
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            NetTestingWindow(transition='in_right').get_root_widget()
711        )
712
713    def _on_friend_promo_code_press(self) -> None:
714        from bauiv1lib import appinvite
715        from bauiv1lib import account
716
717        plus = bui.app.plus
718        assert plus is not None
719
720        if plus.get_v1_account_state() != 'signed_in':
721            account.show_sign_in_prompt()
722            return
723        appinvite.handle_app_invites_press()
724
725    def _on_plugins_button_press(self) -> None:
726        from bauiv1lib.settings.plugins import PluginWindow
727
728        self._save_state()
729        bui.containerwidget(edit=self._root_widget, transition='out_left')
730        assert bui.app.classic is not None
731        bui.app.ui_v1.set_main_menu_window(
732            PluginWindow(origin_widget=self._plugins_button).get_root_widget()
733        )
734
735    def _on_promo_code_press(self) -> None:
736        from bauiv1lib.promocode import PromoCodeWindow
737        from bauiv1lib.account import show_sign_in_prompt
738
739        plus = bui.app.plus
740        assert plus is not None
741
742        # We have to be logged in for promo-codes to work.
743        if plus.get_v1_account_state() != 'signed_in':
744            show_sign_in_prompt()
745            return
746        self._save_state()
747        bui.containerwidget(edit=self._root_widget, transition='out_left')
748        assert bui.app.classic is not None
749        bui.app.ui_v1.set_main_menu_window(
750            PromoCodeWindow(
751                origin_widget=self._promo_code_button
752            ).get_root_widget()
753        )
754
755    def _on_benchmark_press(self) -> None:
756        from bauiv1lib.debug import DebugWindow
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            DebugWindow(transition='in_right').get_root_widget()
763        )
764
765    def _save_state(self) -> None:
766        # pylint: disable=too-many-branches
767        try:
768            sel = self._root_widget.get_selected_child()
769            if sel == self._scrollwidget:
770                sel = self._subcontainer.get_selected_child()
771                if sel == self._vr_test_button:
772                    sel_name = 'VRTest'
773                elif sel == self._net_test_button:
774                    sel_name = 'NetTest'
775                elif sel == self._promo_code_button:
776                    sel_name = 'PromoCode'
777                elif sel == self._benchmarks_button:
778                    sel_name = 'Benchmarks'
779                elif sel == self._kick_idle_players_check_box.widget:
780                    sel_name = 'KickIdlePlayers'
781                elif sel == self._show_game_ping_check_box.widget:
782                    sel_name = 'ShowPing'
783                elif sel == self._disable_camera_shake_check_box.widget:
784                    sel_name = 'DisableCameraShake'
785                elif (
786                    self._always_use_internal_keyboard_check_box is not None
787                    and sel
788                    == self._always_use_internal_keyboard_check_box.widget
789                ):
790                    sel_name = 'AlwaysUseInternalKeyboard'
791                elif (
792                    self._disable_gyro_check_box is not None
793                    and sel == self._disable_gyro_check_box.widget
794                ):
795                    sel_name = 'DisableGyro'
796                elif (
797                    self._language_popup is not None
798                    and sel == self._language_popup.get_button()
799                ):
800                    sel_name = 'Languages'
801                elif sel == self._translation_editor_button:
802                    sel_name = 'TranslationEditor'
803                elif sel == self._show_user_mods_button:
804                    sel_name = 'ShowUserMods'
805                elif sel == self._plugins_button:
806                    sel_name = 'Plugins'
807                elif sel == self._modding_guide_button:
808                    sel_name = 'ModdingGuide'
809                elif sel == self._language_inform_checkbox:
810                    sel_name = 'LangInform'
811                else:
812                    raise ValueError(f'unrecognized selection \'{sel}\'')
813            elif sel == self._back_button:
814                sel_name = 'Back'
815            else:
816                raise ValueError(f'unrecognized selection \'{sel}\'')
817            assert bui.app.classic is not None
818            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
819
820        except Exception:
821            logging.exception('Error saving state for %s.', self)
822
823    def _restore_state(self) -> None:
824        # pylint: disable=too-many-branches
825        try:
826            assert bui.app.classic is not None
827            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
828                'sel_name'
829            )
830            if sel_name == 'Back':
831                sel = self._back_button
832            else:
833                bui.containerwidget(
834                    edit=self._root_widget, selected_child=self._scrollwidget
835                )
836                if sel_name == 'VRTest':
837                    sel = self._vr_test_button
838                elif sel_name == 'NetTest':
839                    sel = self._net_test_button
840                elif sel_name == 'PromoCode':
841                    sel = self._promo_code_button
842                elif sel_name == 'Benchmarks':
843                    sel = self._benchmarks_button
844                elif sel_name == 'KickIdlePlayers':
845                    sel = self._kick_idle_players_check_box.widget
846                elif sel_name == 'ShowPing':
847                    sel = self._show_game_ping_check_box.widget
848                elif sel_name == 'DisableCameraShake':
849                    sel = self._disable_camera_shake_check_box.widget
850                elif (
851                    sel_name == 'AlwaysUseInternalKeyboard'
852                    and self._always_use_internal_keyboard_check_box is not None
853                ):
854                    sel = self._always_use_internal_keyboard_check_box.widget
855                elif (
856                    sel_name == 'DisableGyro'
857                    and self._disable_gyro_check_box is not None
858                ):
859                    sel = self._disable_gyro_check_box.widget
860                elif (
861                    sel_name == 'Languages' and self._language_popup is not None
862                ):
863                    sel = self._language_popup.get_button()
864                elif sel_name == 'TranslationEditor':
865                    sel = self._translation_editor_button
866                elif sel_name == 'ShowUserMods':
867                    sel = self._show_user_mods_button
868                elif sel_name == 'Plugins':
869                    sel = self._plugins_button
870                elif sel_name == 'ModdingGuide':
871                    sel = self._modding_guide_button
872                elif sel_name == 'LangInform':
873                    sel = self._language_inform_checkbox
874                else:
875                    sel = None
876                if sel is not None:
877                    bui.containerwidget(
878                        edit=self._subcontainer,
879                        selected_child=sel,
880                        visible_child=sel,
881                    )
882        except Exception:
883            logging.exception('Error restoring state for %s.', self)
884
885    def _on_menu_open(self) -> None:
886        self._menu_open = True
887
888    def _on_menu_close(self) -> None:
889        self._menu_open = False
890
891    def _on_menu_choice(self, choice: str) -> None:
892        bui.app.lang.setlanguage(None if choice == 'Auto' else choice)
893        self._save_state()
894        bui.apptimer(0.1, bui.WeakCall(self._rebuild))
895
896    def _completed_langs_cb(self, results: dict[str, Any] | None) -> None:
897        if results is not None and results['langs'] is not None:
898            self._complete_langs_list = results['langs']
899            self._complete_langs_error = False
900        else:
901            self._complete_langs_list = None
902            self._complete_langs_error = True
903        bui.apptimer(0.001, bui.WeakCall(self._update_lang_status))
904
905    def _do_back(self) -> None:
906        from bauiv1lib.settings.allsettings import AllSettingsWindow
907
908        self._save_state()
909        bui.containerwidget(
910            edit=self._root_widget, transition=self._transition_out
911        )
912        assert bui.app.classic is not None
913        bui.app.ui_v1.set_main_menu_window(
914            AllSettingsWindow(transition='in_left').get_root_widget()
915        )

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 = 870.0 if uiscale is bui.UIScale.SMALL else 670.0
 52        x_inset = 100 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 = 766.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