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        )
 166        bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
 167        self._subcontainer = bui.containerwidget(
 168            parent=self._scrollwidget,
 169            size=(self._sub_width, self._sub_height),
 170            background=False,
 171            selection_loops_to_parent=True,
 172        )
 173
 174        self._rebuild()
 175
 176        # Rebuild periodically to pick up language changes/additions/etc.
 177        self._rebuild_timer = bui.AppTimer(
 178            1.0, bui.WeakCall(self._rebuild), repeat=True
 179        )
 180
 181        # Fetch the list of completed languages.
 182        bui.app.classic.master_server_v1_get(
 183            'bsLangGetCompleted',
 184            {'b': app.env.engine_build_number},
 185            callback=bui.WeakCall(self._completed_langs_cb),
 186        )
 187
 188    @override
 189    def get_main_window_state(self) -> bui.MainWindowState:
 190        # Support recreating our window for back/refresh purposes.
 191        cls = type(self)
 192        return bui.BasicMainWindowState(
 193            create_call=lambda transition, origin_widget: cls(
 194                transition=transition, origin_widget=origin_widget
 195            )
 196        )
 197
 198    @override
 199    def on_main_window_close(self) -> None:
 200        self._save_state()
 201
 202    @staticmethod
 203    def _preload_modules() -> None:
 204        """Preload stuff in bg thread to avoid hitches in logic thread"""
 205        from babase import modutils as _unused2
 206        from bauiv1lib import config as _unused1
 207        from bauiv1lib.settings import vrtesting as _unused3
 208        from bauiv1lib.settings import nettesting as _unused4
 209        from bauiv1lib import appinvite as _unused5
 210        from bauiv1lib import account as _unused6
 211        from bauiv1lib import sendinfo as _unused7
 212        from bauiv1lib.settings import benchmarks as _unused8
 213        from bauiv1lib.settings import plugins as _unused9
 214        from bauiv1lib.settings import devtools as _unused10
 215
 216    def _update_lang_status(self) -> None:
 217        if self._complete_langs_list is not None:
 218            up_to_date = bui.app.lang.language in self._complete_langs_list
 219            bui.textwidget(
 220                edit=self._lang_status_text,
 221                text=(
 222                    ''
 223                    if bui.app.lang.language == 'Test'
 224                    else (
 225                        bui.Lstr(
 226                            resource=f'{self._r}.translationNoUpdateNeededText'
 227                        )
 228                        if up_to_date
 229                        else bui.Lstr(
 230                            resource=f'{self._r}.translationUpdateNeededText'
 231                        )
 232                    )
 233                ),
 234                color=(
 235                    (0.2, 1.0, 0.2, 0.8) if up_to_date else (1.0, 0.2, 0.2, 0.8)
 236                ),
 237            )
 238        else:
 239            bui.textwidget(
 240                edit=self._lang_status_text,
 241                text=(
 242                    bui.Lstr(resource=f'{self._r}.translationFetchErrorText')
 243                    if self._complete_langs_error
 244                    else bui.Lstr(
 245                        resource=f'{self._r}.translationFetchingStatusText'
 246                    )
 247                ),
 248                color=(
 249                    (1.0, 0.5, 0.2)
 250                    if self._complete_langs_error
 251                    else (0.7, 0.7, 0.7)
 252                ),
 253            )
 254
 255    def _rebuild(self) -> None:
 256        # pylint: disable=too-many-statements
 257        # pylint: disable=too-many-branches
 258        # pylint: disable=too-many-locals
 259
 260        from bauiv1lib.config import ConfigCheckBox
 261        from babase.modutils import show_user_scripts
 262
 263        plus = bui.app.plus
 264        assert plus is not None
 265
 266        available_languages = bui.app.lang.available_languages
 267
 268        # Don't rebuild if the menu is open or if our language and
 269        # language-list hasn't changed.
 270
 271        # NOTE - although we now support widgets updating their own
 272        # translations, we still change the label formatting on the language
 273        # menu based on the language so still need this. ...however we could
 274        # make this more limited to it only rebuilds that one menu instead
 275        # of everything.
 276        if self._menu_open or (
 277            self._prev_lang == bui.app.config.get('Lang', None)
 278            and self._prev_lang_list == available_languages
 279        ):
 280            return
 281        self._prev_lang = bui.app.config.get('Lang', None)
 282        self._prev_lang_list = available_languages
 283
 284        # Clear out our sub-container.
 285        children = self._subcontainer.get_children()
 286        for child in children:
 287            child.delete()
 288
 289        v = self._sub_height - 35
 290
 291        v -= self._spacing * 1.2
 292
 293        # Update our existing back button and title.
 294        if self._back_button is not None:
 295            bui.buttonwidget(
 296                edit=self._back_button, label=bui.Lstr(resource='backText')
 297            )
 298            bui.buttonwidget(
 299                edit=self._back_button, label=bui.charstr(bui.SpecialChar.BACK)
 300            )
 301
 302        bui.textwidget(
 303            edit=self._title_text,
 304            text=bui.Lstr(resource=f'{self._r}.titleText'),
 305        )
 306
 307        this_button_width = 410
 308
 309        assert bui.app.classic is not None
 310        bui.textwidget(
 311            parent=self._subcontainer,
 312            position=(70, v + 10),
 313            size=(0, 0),
 314            text=bui.Lstr(resource=f'{self._r}.languageText'),
 315            maxwidth=150,
 316            scale=1.2,
 317            color=bui.app.ui_v1.title_color,
 318            h_align='left',
 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=(90, 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='left',
 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 -= 50
 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_demos_when_idle_check_box = ConfigCheckBox(
 488            parent=self._subcontainer,
 489            position=(50, v),
 490            size=(self._sub_width - 100, 30),
 491            configkey='Show Demos When Idle',
 492            displayname=bui.Lstr(resource=f'{self._r}.showDemosWhenIdleText'),
 493            scale=1.0,
 494            maxwidth=430,
 495        )
 496
 497        v -= 42
 498        self._show_deprecated_login_types_check_box = ConfigCheckBox(
 499            parent=self._subcontainer,
 500            position=(50, v),
 501            size=(self._sub_width - 100, 30),
 502            configkey='Show Deprecated Login Types',
 503            displayname=bui.Lstr(
 504                resource=f'{self._r}.showDeprecatedLoginTypesText'
 505            ),
 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._use_insecure_connections_check_box: ConfigCheckBox | None
 537        if self._show_use_insecure_connections:
 538            v -= 42
 539            self._use_insecure_connections_check_box = ConfigCheckBox(
 540                parent=self._subcontainer,
 541                position=(50, v),
 542                size=(self._sub_width - 100, 30),
 543                configkey='Use Insecure Connections',
 544                autoselect=True,
 545                # displayname='USE INSECURE CONNECTIONS',
 546                displayname=bui.Lstr(
 547                    resource=(f'{self._r}.insecureConnectionsText')
 548                ),
 549                # displayname=bui.Lstr(
 550                #     resource=f'{self._r}.alwaysUseInternalKeyboardText'
 551                # ),
 552                scale=1.0,
 553                maxwidth=430,
 554            )
 555            bui.textwidget(
 556                parent=self._subcontainer,
 557                position=(90, v - 20),
 558                size=(0, 0),
 559                # text=(
 560                #     'not recommended, but may allow online play\n'
 561                #     'from restricted countries or networks'
 562                # ),
 563                text=bui.Lstr(
 564                    resource=(f'{self._r}.insecureConnectionsDescriptionText')
 565                ),
 566                maxwidth=400,
 567                flatness=1.0,
 568                scale=0.65,
 569                color=(0.4, 0.9, 0.4, 0.8),
 570                h_align='left',
 571                v_align='center',
 572            )
 573            v -= 40
 574        else:
 575            self._use_insecure_connections_check_box = None
 576
 577        self._always_use_internal_keyboard_check_box: ConfigCheckBox | None
 578        if self._show_always_use_internal_keyboard:
 579            v -= 42
 580            self._always_use_internal_keyboard_check_box = ConfigCheckBox(
 581                parent=self._subcontainer,
 582                position=(50, v),
 583                size=(self._sub_width - 100, 30),
 584                configkey='Always Use Internal Keyboard',
 585                autoselect=True,
 586                displayname=bui.Lstr(
 587                    resource=f'{self._r}.alwaysUseInternalKeyboardText'
 588                ),
 589                scale=1.0,
 590                maxwidth=430,
 591            )
 592            bui.textwidget(
 593                parent=self._subcontainer,
 594                position=(90, v - 10),
 595                size=(0, 0),
 596                text=bui.Lstr(
 597                    resource=(
 598                        f'{self._r}.alwaysUseInternalKeyboardDescriptionText'
 599                    )
 600                ),
 601                maxwidth=400,
 602                flatness=1.0,
 603                scale=0.65,
 604                color=(0.4, 0.9, 0.4, 0.8),
 605                h_align='left',
 606                v_align='center',
 607            )
 608            v -= 20
 609        else:
 610            self._always_use_internal_keyboard_check_box = None
 611
 612        v -= self._spacing * 2.1
 613
 614        this_button_width = 410
 615        self._modding_guide_button = bui.buttonwidget(
 616            parent=self._subcontainer,
 617            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
 618            size=(this_button_width, 60),
 619            autoselect=True,
 620            label=bui.Lstr(resource=f'{self._r}.moddingGuideText'),
 621            text_scale=1.0,
 622            on_activate_call=bui.Call(
 623                bui.open_url, 'https://ballistica.net/wiki/modding-guide'
 624            ),
 625        )
 626
 627        v -= self._spacing * 2.0
 628
 629        self._dev_tools_button = bui.buttonwidget(
 630            parent=self._subcontainer,
 631            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
 632            size=(this_button_width, 60),
 633            autoselect=True,
 634            label=bui.Lstr(resource=f'{self._r}.devToolsText'),
 635            text_scale=1.0,
 636            on_activate_call=self._on_dev_tools_button_press,
 637        )
 638
 639        if self._show_always_use_internal_keyboard:
 640            assert self._always_use_internal_keyboard_check_box is not None
 641            bui.widget(
 642                edit=self._always_use_internal_keyboard_check_box.widget,
 643                down_widget=self._modding_guide_button,
 644            )
 645            bui.widget(
 646                edit=self._modding_guide_button,
 647                up_widget=self._always_use_internal_keyboard_check_box.widget,
 648            )
 649        else:
 650            # ew.
 651            next_widget_up = (
 652                self._disable_gyro_check_box.widget
 653                if self._disable_gyro_check_box is not None
 654                else self._disable_camera_shake_check_box.widget
 655            )
 656            bui.widget(
 657                edit=self._modding_guide_button,
 658                up_widget=next_widget_up,
 659            )
 660            bui.widget(
 661                edit=next_widget_up,
 662                down_widget=self._modding_guide_button,
 663            )
 664
 665        v -= self._spacing * 2.0
 666
 667        self._show_user_mods_button = bui.buttonwidget(
 668            parent=self._subcontainer,
 669            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
 670            size=(this_button_width, 60),
 671            autoselect=True,
 672            label=bui.Lstr(resource=f'{self._r}.showUserModsText'),
 673            text_scale=1.0,
 674            on_activate_call=show_user_scripts,
 675        )
 676
 677        v -= self._spacing * 2.0
 678
 679        self._plugins_button = bui.buttonwidget(
 680            parent=self._subcontainer,
 681            position=(self._sub_width / 2 - this_button_width / 2, v - 10),
 682            size=(this_button_width, 60),
 683            autoselect=True,
 684            label=bui.Lstr(resource='pluginsText'),
 685            text_scale=1.0,
 686            on_activate_call=self._on_plugins_button_press,
 687        )
 688
 689        v -= self._spacing * 0.6
 690
 691        self._vr_test_button: bui.Widget | None
 692        if self._do_vr_test_button:
 693            v -= self._extra_button_spacing
 694            self._vr_test_button = bui.buttonwidget(
 695                parent=self._subcontainer,
 696                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
 697                size=(this_button_width, 60),
 698                autoselect=True,
 699                label=bui.Lstr(resource=f'{self._r}.vrTestingText'),
 700                text_scale=1.0,
 701                on_activate_call=self._on_vr_test_press,
 702            )
 703        else:
 704            self._vr_test_button = None
 705
 706        self._net_test_button: bui.Widget | None
 707        if self._do_net_test_button:
 708            v -= self._extra_button_spacing
 709            self._net_test_button = bui.buttonwidget(
 710                parent=self._subcontainer,
 711                position=(self._sub_width / 2 - this_button_width / 2, v - 14),
 712                size=(this_button_width, 60),
 713                autoselect=True,
 714                label=bui.Lstr(resource=f'{self._r}.netTestingText'),
 715                text_scale=1.0,
 716                on_activate_call=self._on_net_test_press,
 717            )
 718        else:
 719            self._net_test_button = None
 720
 721        v -= 70
 722        self._benchmarks_button = bui.buttonwidget(
 723            parent=self._subcontainer,
 724            position=(self._sub_width / 2 - this_button_width / 2, v - 14),
 725            size=(this_button_width, 60),
 726            autoselect=True,
 727            label=bui.Lstr(resource=f'{self._r}.benchmarksText'),
 728            text_scale=1.0,
 729            on_activate_call=self._on_benchmark_press,
 730        )
 731
 732        v -= 100
 733        self._send_info_button = bui.buttonwidget(
 734            parent=self._subcontainer,
 735            position=(self._sub_width / 2 - this_button_width / 2, v - 14),
 736            size=(this_button_width, 60),
 737            autoselect=True,
 738            label=bui.Lstr(resource=f'{self._r}.sendInfoText'),
 739            text_scale=1.0,
 740            on_activate_call=self._on_send_info_press,
 741        )
 742
 743        for child in self._subcontainer.get_children():
 744            bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20)
 745
 746        pbtn = bui.get_special_widget('squad_button')
 747        bui.widget(edit=self._scrollwidget, right_widget=pbtn)
 748        if self._back_button is None:
 749            bui.widget(
 750                edit=self._scrollwidget,
 751                left_widget=bui.get_special_widget('back_button'),
 752            )
 753
 754        self._restore_state()
 755
 756    def _show_restart_needed(self, value: Any) -> None:
 757        del value  # Unused.
 758        bui.screenmessage(
 759            bui.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0)
 760        )
 761
 762    def _on_lang_inform_value_change(self, val: bool) -> None:
 763        plus = bui.app.plus
 764        assert plus is not None
 765        plus.add_v1_account_transaction(
 766            {'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val}
 767        )
 768        plus.run_v1_account_transactions()
 769
 770    def _on_vr_test_press(self) -> None:
 771        from bauiv1lib.settings.vrtesting import VRTestingWindow
 772
 773        # no-op if we're not in control.
 774        if not self.main_window_has_control():
 775            return
 776
 777        self.main_window_replace(VRTestingWindow(transition='in_right'))
 778
 779    def _on_net_test_press(self) -> None:
 780        from bauiv1lib.settings.nettesting import NetTestingWindow
 781
 782        # no-op if we're not in control.
 783        if not self.main_window_has_control():
 784            return
 785
 786        self.main_window_replace(NetTestingWindow(transition='in_right'))
 787
 788    def _on_friend_promo_code_press(self) -> None:
 789        from bauiv1lib import appinvite
 790        from bauiv1lib import account
 791
 792        plus = bui.app.plus
 793        assert plus is not None
 794
 795        if plus.get_v1_account_state() != 'signed_in':
 796            account.show_sign_in_prompt()
 797            return
 798        appinvite.handle_app_invites_press()
 799
 800    def _on_plugins_button_press(self) -> None:
 801        from bauiv1lib.settings.plugins import PluginWindow
 802
 803        # no-op if we're not in control.
 804        if not self.main_window_has_control():
 805            return
 806
 807        self.main_window_replace(
 808            PluginWindow(origin_widget=self._plugins_button)
 809        )
 810
 811    def _on_dev_tools_button_press(self) -> None:
 812        # pylint: disable=cyclic-import
 813        from bauiv1lib.settings.devtools import DevToolsWindow
 814
 815        # no-op if we're not in control.
 816        if not self.main_window_has_control():
 817            return
 818
 819        self.main_window_replace(
 820            DevToolsWindow(origin_widget=self._dev_tools_button)
 821        )
 822
 823    def _on_send_info_press(self) -> None:
 824        from bauiv1lib.sendinfo import SendInfoWindow
 825
 826        # no-op if we're not in control.
 827        if not self.main_window_has_control():
 828            return
 829
 830        self.main_window_replace(
 831            SendInfoWindow(origin_widget=self._send_info_button)
 832        )
 833
 834    def _on_benchmark_press(self) -> None:
 835        from bauiv1lib.settings.benchmarks import BenchmarksAndStressTestsWindow
 836
 837        # no-op if we're not in control.
 838        if not self.main_window_has_control():
 839            return
 840
 841        self.main_window_replace(
 842            BenchmarksAndStressTestsWindow(transition='in_right')
 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._send_info_button:
 857                    sel_name = 'SendInfo'
 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_deprecated_login_types_check_box.widget:
 865                    sel_name = 'ShowDeprecatedLoginTypes'
 866                elif sel == self._show_game_ping_check_box.widget:
 867                    sel_name = 'ShowPing'
 868                elif sel == self._disable_camera_shake_check_box.widget:
 869                    sel_name = 'DisableCameraShake'
 870                elif (
 871                    self._always_use_internal_keyboard_check_box is not None
 872                    and sel
 873                    == self._always_use_internal_keyboard_check_box.widget
 874                ):
 875                    sel_name = 'AlwaysUseInternalKeyboard'
 876                elif (
 877                    self._use_insecure_connections_check_box is not None
 878                    and sel == self._use_insecure_connections_check_box.widget
 879                ):
 880                    sel_name = 'UseInsecureConnections'
 881                elif (
 882                    self._disable_gyro_check_box is not None
 883                    and sel == self._disable_gyro_check_box.widget
 884                ):
 885                    sel_name = 'DisableGyro'
 886                elif (
 887                    self._language_popup is not None
 888                    and sel == self._language_popup.get_button()
 889                ):
 890                    sel_name = 'Languages'
 891                elif sel == self._translation_editor_button:
 892                    sel_name = 'TranslationEditor'
 893                elif sel == self._show_user_mods_button:
 894                    sel_name = 'ShowUserMods'
 895                elif sel == self._plugins_button:
 896                    sel_name = 'Plugins'
 897                elif sel == self._dev_tools_button:
 898                    sel_name = 'DevTools'
 899                elif sel == self._modding_guide_button:
 900                    sel_name = 'ModdingGuide'
 901                elif sel == self._language_inform_checkbox:
 902                    sel_name = 'LangInform'
 903                else:
 904                    raise ValueError(f'unrecognized selection \'{sel}\'')
 905            elif sel == self._back_button:
 906                sel_name = 'Back'
 907            else:
 908                raise ValueError(f'unrecognized selection \'{sel}\'')
 909            assert bui.app.classic is not None
 910            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
 911
 912        except Exception:
 913            logging.exception('Error saving state for %s.', self)
 914
 915    def _restore_state(self) -> None:
 916        # pylint: disable=too-many-branches
 917        # pylint: disable=too-many-statements
 918        try:
 919            assert bui.app.classic is not None
 920            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
 921                'sel_name'
 922            )
 923            if sel_name == 'Back':
 924                sel = self._back_button
 925            else:
 926                bui.containerwidget(
 927                    edit=self._root_widget, selected_child=self._scrollwidget
 928                )
 929                if sel_name == 'VRTest':
 930                    sel = self._vr_test_button
 931                elif sel_name == 'NetTest':
 932                    sel = self._net_test_button
 933                elif sel_name == 'SendInfo':
 934                    sel = self._send_info_button
 935                elif sel_name == 'Benchmarks':
 936                    sel = self._benchmarks_button
 937                elif sel_name == 'KickIdlePlayers':
 938                    sel = self._kick_idle_players_check_box.widget
 939                elif sel_name == 'ShowDemosWhenIdle':
 940                    sel = self._show_demos_when_idle_check_box.widget
 941                elif sel_name == 'ShowDeprecatedLoginTypes':
 942                    sel = self._show_deprecated_login_types_check_box.widget
 943                elif sel_name == 'ShowPing':
 944                    sel = self._show_game_ping_check_box.widget
 945                elif sel_name == 'DisableCameraShake':
 946                    sel = self._disable_camera_shake_check_box.widget
 947                elif (
 948                    sel_name == 'AlwaysUseInternalKeyboard'
 949                    and self._always_use_internal_keyboard_check_box is not None
 950                ):
 951                    sel = self._always_use_internal_keyboard_check_box.widget
 952                elif (
 953                    sel_name == 'UseInsecureConnections'
 954                    and self._use_insecure_connections_check_box is not None
 955                ):
 956                    sel = self._use_insecure_connections_check_box.widget
 957                elif (
 958                    sel_name == 'DisableGyro'
 959                    and self._disable_gyro_check_box is not None
 960                ):
 961                    sel = self._disable_gyro_check_box.widget
 962                elif (
 963                    sel_name == 'Languages' and self._language_popup is not None
 964                ):
 965                    sel = self._language_popup.get_button()
 966                elif sel_name == 'TranslationEditor':
 967                    sel = self._translation_editor_button
 968                elif sel_name == 'ShowUserMods':
 969                    sel = self._show_user_mods_button
 970                elif sel_name == 'Plugins':
 971                    sel = self._plugins_button
 972                elif sel_name == 'DevTools':
 973                    sel = self._dev_tools_button
 974                elif sel_name == 'ModdingGuide':
 975                    sel = self._modding_guide_button
 976                elif sel_name == 'LangInform':
 977                    sel = self._language_inform_checkbox
 978                else:
 979                    sel = None
 980                if sel is not None:
 981                    bui.containerwidget(
 982                        edit=self._subcontainer,
 983                        selected_child=sel,
 984                        visible_child=sel,
 985                    )
 986        except Exception:
 987            logging.exception('Error restoring state for %s.', self)
 988
 989    def _on_menu_open(self) -> None:
 990        self._menu_open = True
 991
 992    def _on_menu_close(self) -> None:
 993        self._menu_open = False
 994
 995    def _on_menu_choice(self, choice: str) -> None:
 996        bui.app.lang.setlanguage(None if choice == 'Auto' else choice)
 997        self._save_state()
 998        bui.apptimer(0.1, bui.WeakCall(self._rebuild))
 999
1000    def _completed_langs_cb(self, results: dict[str, Any] | None) -> None:
1001        if results is not None and results['langs'] is not None:
1002            self._complete_langs_list = results['langs']
1003            self._complete_langs_error = False
1004        else:
1005            self._complete_langs_list = None
1006            self._complete_langs_error = True
1007        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        )
 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 import account
 792
 793        plus = bui.app.plus
 794        assert plus is not None
 795
 796        if plus.get_v1_account_state() != 'signed_in':
 797            account.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))

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        )
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        )

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:
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        )

Return a WindowState to recreate this window, if supported.

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

Called before transitioning out a main window.

A good opportunity to save window state/etc.