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

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:
198    @override
199    def get_main_window_state(self) -> bui.MainWindowState:
200        # Support recreating our window for back/refresh purposes.
201        cls = type(self)
202        return bui.BasicMainWindowState(
203            create_call=lambda transition, origin_widget: cls(
204                transition=transition, origin_widget=origin_widget
205            )
206        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
208    @override
209    def on_main_window_close(self) -> None:
210        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.