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

Window for editing advanced game settings.

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