bauiv1lib.settings.advanced

UI functionality for advanced settings.

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

Window for editing advanced app settings.

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