bauiv1lib.settings.advanced

UI functionality for advanced settings.

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

Window for editing advanced app settings.

AdvancedSettingsWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 24    def __init__(
 25        self,
 26        transition: str | None = 'in_right',
 27        origin_widget: bui.Widget | None = None,
 28    ):
 29        # pylint: disable=too-many-statements
 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        bui.app.threadpool.submit_no_wait(self._preload_modules)
 37
 38        app = bui.app
 39        assert app.classic is not None
 40
 41        uiscale = bui.app.ui_v1.uiscale
 42        self._width = 1030.0 if uiscale is bui.UIScale.SMALL else 670.0
 43        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 44        self._height = (
 45            390.0
 46            if uiscale is bui.UIScale.SMALL
 47            else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0
 48        )
 49        self._lang_status_text: bui.Widget | None = None
 50
 51        self._spacing = 32
 52        self._menu_open = False
 53        top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
 54
 55        super().__init__(
 56            root_widget=bui.containerwidget(
 57                size=(self._width, self._height + top_extra),
 58                toolbar_visibility=(
 59                    'menu_minimal'
 60                    if uiscale is bui.UIScale.SMALL
 61                    else 'menu_full'
 62                ),
 63                scale=(
 64                    2.04
 65                    if uiscale is bui.UIScale.SMALL
 66                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 67                ),
 68                stack_offset=(
 69                    (0, 10) if uiscale is bui.UIScale.SMALL else (0, 0)
 70                ),
 71            ),
 72            transition=transition,
 73            origin_widget=origin_widget,
 74        )
 75
 76        self._prev_lang = ''
 77        self._prev_lang_list: list[str] = []
 78        self._complete_langs_list: list | None = None
 79        self._complete_langs_error = False
 80        self._language_popup: PopupMenu | None = None
 81
 82        # In vr-mode, the internal keyboard is currently the *only* option,
 83        # so no need to show this.
 84        self._show_always_use_internal_keyboard = not app.env.vr
 85
 86        self._scroll_width = self._width - (100 + 2 * x_inset)
 87        self._scroll_height = self._height - (
 88            125.0 if uiscale is bui.UIScale.SMALL else 115.0
 89        )
 90        self._sub_width = self._scroll_width * 0.95
 91        self._sub_height = 870.0
 92
 93        if self._show_always_use_internal_keyboard:
 94            self._sub_height += 62
 95
 96        self._show_disable_gyro = app.classic.platform in {'ios', 'android'}
 97        if self._show_disable_gyro:
 98            self._sub_height += 42
 99
100        self._show_use_insecure_connections = True
101        if self._show_use_insecure_connections:
102            self._sub_height += 82
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  # dev tools
114
115        self._r = 'settingsWindowAdvanced'
116
117        if uiscale is bui.UIScale.SMALL:
118            bui.containerwidget(
119                edit=self._root_widget, on_cancel_call=self.main_window_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.main_window_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=(
140                self._width * 0.5,
141                self._height - (61 if uiscale is bui.UIScale.SMALL else 40),
142            ),
143            size=(0, 0),
144            scale=0.85 if uiscale is bui.UIScale.SMALL else 1.0,
145            text=bui.Lstr(resource=f'{self._r}.titleText'),
146            color=app.ui_v1.title_color,
147            h_align='center',
148            v_align='center',
149        )
150
151        if self._back_button is not None:
152            bui.buttonwidget(
153                edit=self._back_button,
154                button_type='backSmall',
155                size=(60, 60),
156                label=bui.charstr(bui.SpecialChar.BACK),
157            )
158
159        self._scrollwidget = bui.scrollwidget(
160            parent=self._root_widget,
161            position=(50 + x_inset, 50),
162            simple_culling_v=20.0,
163            highlight=False,
164            size=(self._scroll_width, self._scroll_height),
165            selection_loops_to_parent=True,
166            border_opacity=0.4,
167        )
168        bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
169        self._subcontainer = bui.containerwidget(
170            parent=self._scrollwidget,
171            size=(self._sub_width, self._sub_height),
172            background=False,
173            selection_loops_to_parent=True,
174        )
175
176        self._rebuild()
177
178        # Rebuild periodically to pick up language changes/additions/etc.
179        self._rebuild_timer = bui.AppTimer(
180            1.0, bui.WeakCall(self._rebuild), repeat=True
181        )
182
183        # Fetch the list of completed languages.
184        bui.app.classic.master_server_v1_get(
185            'bsLangGetCompleted',
186            {'b': app.env.engine_build_number},
187            callback=bui.WeakCall(self._completed_langs_cb),
188        )

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
190    @override
191    def get_main_window_state(self) -> bui.MainWindowState:
192        # Support recreating our window for back/refresh purposes.
193        cls = type(self)
194        return bui.BasicMainWindowState(
195            create_call=lambda transition, origin_widget: cls(
196                transition=transition, origin_widget=origin_widget
197            )
198        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
200    @override
201    def on_main_window_close(self) -> None:
202        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.