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

Window for editing advanced game settings.

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