Source code for bauiv1lib.settings.advanced

# Released under the MIT License. See LICENSE for details.
#
"""UI functionality for advanced settings."""

from __future__ import annotations

import os
import logging
from typing import TYPE_CHECKING, override

from bauiv1lib.popup import PopupMenu
import bauiv1 as bui

if TYPE_CHECKING:
    from typing import Any


[docs] class AdvancedSettingsWindow(bui.MainWindow): """Window for editing advanced app settings.""" def __init__( self, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements if bui.app.classic is None: raise RuntimeError('This requires classic support.') # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. bui.app.threadpool_submit_no_wait(self._preload_modules) app = bui.app assert app.classic is not None uiscale = bui.app.ui_v1.uiscale self._width = 1030.0 if uiscale is bui.UIScale.SMALL else 670.0 x_inset = 150 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 390.0 if uiscale is bui.UIScale.SMALL else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0 ) self._lang_status_text: bui.Widget | None = None self._spacing = 32 self._menu_open = False top_extra = 10 if uiscale is bui.UIScale.SMALL else 0 super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), toolbar_visibility=( 'menu_minimal' if uiscale is bui.UIScale.SMALL else 'menu_full' ), scale=( 2.04 if uiscale is bui.UIScale.SMALL else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, 10) if uiscale is bui.UIScale.SMALL else (0, 0) ), ), transition=transition, origin_widget=origin_widget, ) self._prev_lang = '' self._prev_lang_list: list[str] = [] self._complete_langs_list: list | None = None self._complete_langs_error = False self._language_popup: PopupMenu | None = None # In vr-mode, the internal keyboard is currently the *only* option, # so no need to show this. self._show_always_use_internal_keyboard = not app.env.vr self._scroll_width = self._width - (100 + 2 * x_inset) self._scroll_height = self._height - ( 125.0 if uiscale is bui.UIScale.SMALL else 115.0 ) self._sub_width = self._scroll_width * 0.95 self._sub_height = 870.0 if self._show_always_use_internal_keyboard: self._sub_height += 62 self._show_disable_gyro = app.classic.platform in {'ios', 'android'} if self._show_disable_gyro: self._sub_height += 42 self._do_vr_test_button = app.env.vr self._do_net_test_button = True self._extra_button_spacing = self._spacing * 2.5 if self._do_vr_test_button: self._sub_height += self._extra_button_spacing if self._do_net_test_button: self._sub_height += self._extra_button_spacing self._sub_height += self._spacing * 2.0 # plugins self._sub_height += self._spacing * 2.0 # dev tools self._r = 'settingsWindowAdvanced' if uiscale is bui.UIScale.SMALL: bui.containerwidget( edit=self._root_widget, on_cancel_call=self.main_window_back ) self._back_button = None else: self._back_button = bui.buttonwidget( parent=self._root_widget, position=(53 + x_inset, self._height - 60), size=(140, 60), scale=0.8, autoselect=True, label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self.main_window_back, ) bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) self._title_text = bui.textwidget( parent=self._root_widget, position=( self._width * 0.5, self._height - (61 if uiscale is bui.UIScale.SMALL else 40), ), size=(0, 0), scale=0.85 if uiscale is bui.UIScale.SMALL else 1.0, text=bui.Lstr(resource=f'{self._r}.titleText'), color=app.ui_v1.title_color, h_align='center', v_align='center', ) if self._back_button is not None: bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 60), label=bui.charstr(bui.SpecialChar.BACK), ) self._scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(50 + x_inset, 50), simple_culling_v=20.0, highlight=False, size=(self._scroll_width, self._scroll_height), selection_loops_to_parent=True, ) bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, selection_loops_to_parent=True, ) self._rebuild() # Rebuild periodically to pick up language changes/additions/etc. self._rebuild_timer = bui.AppTimer( 1.0, bui.WeakCall(self._rebuild), repeat=True ) # Fetch the list of completed languages. bui.app.classic.master_server_v1_get( 'bsLangGetCompleted', {'b': app.env.engine_build_number}, callback=bui.WeakCall(self._completed_langs_cb), )
[docs] @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. cls = type(self) return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget ) )
[docs] @override def on_main_window_close(self) -> None: self._save_state()
@staticmethod def _preload_modules() -> None: """Preload stuff in bg thread to avoid hitches in logic thread""" from babase import modutils as _unused2 from bauiv1lib import config as _unused1 from bauiv1lib.settings import vrtesting as _unused3 from bauiv1lib.settings import nettesting as _unused4 from bauiv1lib import appinvite as _unused5 from bauiv1lib import account as _unused6 from bauiv1lib import sendinfo as _unused7 from bauiv1lib.settings import benchmarks as _unused8 from bauiv1lib.settings import plugins as _unused9 from bauiv1lib.settings import devtools as _unused10 def _update_lang_status(self) -> None: if self._complete_langs_list is not None: up_to_date = bui.app.lang.language in self._complete_langs_list bui.textwidget( edit=self._lang_status_text, text=( '' if bui.app.lang.language == 'Test' else ( bui.Lstr( resource=f'{self._r}.translationNoUpdateNeededText' ) if up_to_date else bui.Lstr( resource=f'{self._r}.translationUpdateNeededText' ) ) ), color=( (0.2, 1.0, 0.2, 0.8) if up_to_date else (1.0, 0.2, 0.2, 0.8) ), ) else: bui.textwidget( edit=self._lang_status_text, text=( bui.Lstr(resource=f'{self._r}.translationFetchErrorText') if self._complete_langs_error else bui.Lstr( resource=f'{self._r}.translationFetchingStatusText' ) ), color=( (1.0, 0.5, 0.2) if self._complete_langs_error else (0.7, 0.7, 0.7) ), ) def _rebuild(self) -> None: # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals from bauiv1lib.config import ConfigCheckBox from babase.modutils import show_user_scripts plus = bui.app.plus assert plus is not None available_languages = bui.app.lang.available_languages # Don't rebuild if the menu is open or if our language and # language-list hasn't changed. # NOTE - although we now support widgets updating their own # translations, we still change the label formatting on the language # menu based on the language so still need this. ...however we could # make this more limited to it only rebuilds that one menu instead # of everything. if self._menu_open or ( self._prev_lang == bui.app.config.get('Lang', None) and self._prev_lang_list == available_languages ): return self._prev_lang = bui.app.config.get('Lang', None) self._prev_lang_list = available_languages # Clear out our sub-container. children = self._subcontainer.get_children() for child in children: child.delete() v = self._sub_height - 35 v -= self._spacing * 1.2 # Update our existing back button and title. if self._back_button is not None: bui.buttonwidget( edit=self._back_button, label=bui.Lstr(resource='backText') ) bui.buttonwidget( edit=self._back_button, label=bui.charstr(bui.SpecialChar.BACK) ) bui.textwidget( edit=self._title_text, text=bui.Lstr(resource=f'{self._r}.titleText'), ) this_button_width = 410 assert bui.app.classic is not None bui.textwidget( parent=self._subcontainer, position=(70, v + 10), size=(0, 0), text=bui.Lstr(resource=f'{self._r}.languageText'), maxwidth=150, scale=1.2, color=bui.app.ui_v1.title_color, h_align='left', v_align='center', ) languages = bui.app.lang.available_languages cur_lang = bui.app.config.get('Lang', None) if cur_lang is None: cur_lang = 'Auto' # We have a special dict of language names in that language # so we don't have to go digging through each full language. try: import json with open( os.path.join( bui.app.env.data_directory, 'ba_data', 'data', 'langdata.json', ), encoding='utf-8', ) as infile: lang_names_translated = json.loads(infile.read())[ 'lang_names_translated' ] except Exception: logging.exception('Error reading lang data.') lang_names_translated = {} langs_translated = {} for lang in languages: langs_translated[lang] = lang_names_translated.get(lang, lang) langs_full = {} for lang in languages: lang_translated = bui.Lstr(translate=('languages', lang)).evaluate() if langs_translated[lang] == lang_translated: langs_full[lang] = lang_translated else: langs_full[lang] = ( langs_translated[lang] + ' (' + lang_translated + ')' ) self._language_popup = PopupMenu( parent=self._subcontainer, position=(210, v - 19), width=150, opening_call=bui.WeakCall(self._on_menu_open), closing_call=bui.WeakCall(self._on_menu_close), autoselect=False, on_value_change_call=bui.WeakCall(self._on_menu_choice), choices=['Auto'] + languages, button_size=(250, 60), choices_display=( [ bui.Lstr( value=( bui.Lstr(resource='autoText').evaluate() + ' (' + bui.Lstr( translate=( 'languages', bui.app.lang.default_language, ) ).evaluate() + ')' ) ) ] + [bui.Lstr(value=langs_full[l]) for l in languages] ), current_choice=cur_lang, ) v -= self._spacing * 1.8 bui.textwidget( parent=self._subcontainer, position=(90, v + 10), size=(0, 0), text=bui.Lstr( resource=f'{self._r}.helpTranslateText', subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), maxwidth=self._sub_width * 0.9, max_height=55, flatness=1.0, scale=0.65, color=(0.4, 0.9, 0.4, 0.8), h_align='left', v_align='center', ) v -= self._spacing * 1.9 this_button_width = 410 self._translation_editor_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 24), size=(this_button_width, 60), label=bui.Lstr( resource=f'{self._r}.translationEditorButtonText', subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), autoselect=True, on_activate_call=bui.Call( bui.open_url, 'https://legacy.ballistica.net/translate' ), ) self._lang_status_text = bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v - 40), size=(0, 0), text='', flatness=1.0, scale=0.63, h_align='center', v_align='center', maxwidth=400.0, ) self._update_lang_status() v -= 50 lang_inform = plus.get_v1_account_misc_val('langInform', False) self._language_inform_checkbox = cbw = bui.checkboxwidget( parent=self._subcontainer, position=(50, v - 50), size=(self._sub_width - 100, 30), autoselect=True, maxwidth=430, textcolor=(0.8, 0.8, 0.8), value=lang_inform, text=bui.Lstr(resource=f'{self._r}.translationInformMe'), on_value_change_call=bui.WeakCall( self._on_lang_inform_value_change ), ) bui.widget( edit=self._translation_editor_button, down_widget=cbw, up_widget=self._language_popup.get_button(), ) v -= self._spacing * 3.0 self._kick_idle_players_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Kick Idle Players', displayname=bui.Lstr(resource=f'{self._r}.kickIdlePlayersText'), scale=1.0, maxwidth=430, ) v -= 42 self._show_game_ping_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Show Ping', displayname=bui.Lstr(resource=f'{self._r}.showInGamePingText'), scale=1.0, maxwidth=430, ) v -= 42 self._show_demos_when_idle_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Show Demos When Idle', displayname=bui.Lstr(resource=f'{self._r}.showDemosWhenIdleText'), scale=1.0, maxwidth=430, ) v -= 42 self._show_deprecated_login_types_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Show Deprecated Login Types', displayname=bui.Lstr( resource=f'{self._r}.showDeprecatedLoginTypesText' ), scale=1.0, maxwidth=430, ) v -= 42 self._disable_camera_shake_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Disable Camera Shake', displayname=bui.Lstr(resource=f'{self._r}.disableCameraShakeText'), scale=1.0, maxwidth=430, ) self._disable_gyro_check_box: ConfigCheckBox | None = None if self._show_disable_gyro: v -= 42 self._disable_gyro_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Disable Camera Gyro', displayname=bui.Lstr( resource=f'{self._r}.disableCameraGyroscopeMotionText' ), scale=1.0, maxwidth=430, ) self._always_use_internal_keyboard_check_box: ConfigCheckBox | None if self._show_always_use_internal_keyboard: v -= 42 self._always_use_internal_keyboard_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Always Use Internal Keyboard', autoselect=True, displayname=bui.Lstr( resource=f'{self._r}.alwaysUseInternalKeyboardText' ), scale=1.0, maxwidth=430, ) bui.textwidget( parent=self._subcontainer, position=(90, v - 10), size=(0, 0), text=bui.Lstr( resource=( f'{self._r}.alwaysUseInternalKeyboardDescriptionText' ) ), maxwidth=400, flatness=1.0, scale=0.65, color=(0.4, 0.9, 0.4, 0.8), h_align='left', v_align='center', ) v -= 20 else: self._always_use_internal_keyboard_check_box = None v -= self._spacing * 2.1 this_button_width = 410 self._modding_guide_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), size=(this_button_width, 60), autoselect=True, label=bui.Lstr(resource=f'{self._r}.moddingGuideText'), text_scale=1.0, on_activate_call=bui.Call( bui.open_url, 'https://ballistica.net/wiki/modding-guide' ), ) v -= self._spacing * 2.0 self._dev_tools_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), size=(this_button_width, 60), autoselect=True, label=bui.Lstr(resource=f'{self._r}.devToolsText'), text_scale=1.0, on_activate_call=self._on_dev_tools_button_press, ) if self._show_always_use_internal_keyboard: assert self._always_use_internal_keyboard_check_box is not None bui.widget( edit=self._always_use_internal_keyboard_check_box.widget, down_widget=self._modding_guide_button, ) bui.widget( edit=self._modding_guide_button, up_widget=self._always_use_internal_keyboard_check_box.widget, ) else: # ew. next_widget_up = ( self._disable_gyro_check_box.widget if self._disable_gyro_check_box is not None else self._disable_camera_shake_check_box.widget ) bui.widget( edit=self._modding_guide_button, up_widget=next_widget_up, ) bui.widget( edit=next_widget_up, down_widget=self._modding_guide_button, ) v -= self._spacing * 2.0 self._show_user_mods_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), size=(this_button_width, 60), autoselect=True, label=bui.Lstr(resource=f'{self._r}.showUserModsText'), text_scale=1.0, on_activate_call=show_user_scripts, ) v -= self._spacing * 2.0 self._plugins_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), size=(this_button_width, 60), autoselect=True, label=bui.Lstr(resource='pluginsText'), text_scale=1.0, on_activate_call=self._on_plugins_button_press, ) v -= self._spacing * 0.6 self._vr_test_button: bui.Widget | None if self._do_vr_test_button: v -= self._extra_button_spacing self._vr_test_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, label=bui.Lstr(resource=f'{self._r}.vrTestingText'), text_scale=1.0, on_activate_call=self._on_vr_test_press, ) else: self._vr_test_button = None self._net_test_button: bui.Widget | None if self._do_net_test_button: v -= self._extra_button_spacing self._net_test_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, label=bui.Lstr(resource=f'{self._r}.netTestingText'), text_scale=1.0, on_activate_call=self._on_net_test_press, ) else: self._net_test_button = None v -= 70 self._benchmarks_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, label=bui.Lstr(resource=f'{self._r}.benchmarksText'), text_scale=1.0, on_activate_call=self._on_benchmark_press, ) v -= 100 self._send_info_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, label=bui.Lstr(resource=f'{self._r}.sendInfoText'), text_scale=1.0, on_activate_call=self._on_send_info_press, ) for child in self._subcontainer.get_children(): bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20) pbtn = bui.get_special_widget('squad_button') bui.widget(edit=self._scrollwidget, right_widget=pbtn) if self._back_button is None: bui.widget( edit=self._scrollwidget, left_widget=bui.get_special_widget('back_button'), ) self._restore_state() def _show_restart_needed(self, value: Any) -> None: del value # Unused. bui.screenmessage( bui.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0) ) def _on_lang_inform_value_change(self, val: bool) -> None: plus = bui.app.plus assert plus is not None plus.add_v1_account_transaction( {'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val} ) plus.run_v1_account_transactions() def _on_vr_test_press(self) -> None: from bauiv1lib.settings.vrtesting import VRTestingWindow # no-op if we're not in control. if not self.main_window_has_control(): return self.main_window_replace(VRTestingWindow(transition='in_right')) def _on_net_test_press(self) -> None: from bauiv1lib.settings.nettesting import NetTestingWindow # no-op if we're not in control. if not self.main_window_has_control(): return self.main_window_replace(NetTestingWindow(transition='in_right')) def _on_friend_promo_code_press(self) -> None: from bauiv1lib import appinvite from bauiv1lib import account plus = bui.app.plus assert plus is not None if plus.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return appinvite.handle_app_invites_press() def _on_plugins_button_press(self) -> None: from bauiv1lib.settings.plugins import PluginWindow # no-op if we're not in control. if not self.main_window_has_control(): return self.main_window_replace( PluginWindow(origin_widget=self._plugins_button) ) def _on_dev_tools_button_press(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.devtools import DevToolsWindow # no-op if we're not in control. if not self.main_window_has_control(): return self.main_window_replace( DevToolsWindow(origin_widget=self._dev_tools_button) ) def _on_send_info_press(self) -> None: from bauiv1lib.sendinfo import SendInfoWindow # no-op if we're not in control. if not self.main_window_has_control(): return self.main_window_replace( SendInfoWindow(origin_widget=self._send_info_button) ) def _on_benchmark_press(self) -> None: from bauiv1lib.settings.benchmarks import BenchmarksAndStressTestsWindow # no-op if we're not in control. if not self.main_window_has_control(): return self.main_window_replace( BenchmarksAndStressTestsWindow(transition='in_right') ) def _save_state(self) -> None: # pylint: disable=too-many-branches # pylint: disable=too-many-statements try: sel = self._root_widget.get_selected_child() if sel == self._scrollwidget: sel = self._subcontainer.get_selected_child() if sel == self._vr_test_button: sel_name = 'VRTest' elif sel == self._net_test_button: sel_name = 'NetTest' elif sel == self._send_info_button: sel_name = 'SendInfo' elif sel == self._benchmarks_button: sel_name = 'Benchmarks' elif sel == self._kick_idle_players_check_box.widget: sel_name = 'KickIdlePlayers' elif sel == self._show_demos_when_idle_check_box.widget: sel_name = 'ShowDemosWhenIdle' elif sel == self._show_deprecated_login_types_check_box.widget: sel_name = 'ShowDeprecatedLoginTypes' elif sel == self._show_game_ping_check_box.widget: sel_name = 'ShowPing' elif sel == self._disable_camera_shake_check_box.widget: sel_name = 'DisableCameraShake' elif ( self._always_use_internal_keyboard_check_box is not None and sel == self._always_use_internal_keyboard_check_box.widget ): sel_name = 'AlwaysUseInternalKeyboard' elif ( self._disable_gyro_check_box is not None and sel == self._disable_gyro_check_box.widget ): sel_name = 'DisableGyro' elif ( self._language_popup is not None and sel == self._language_popup.get_button() ): sel_name = 'Languages' elif sel == self._translation_editor_button: sel_name = 'TranslationEditor' elif sel == self._show_user_mods_button: sel_name = 'ShowUserMods' elif sel == self._plugins_button: sel_name = 'Plugins' elif sel == self._dev_tools_button: sel_name = 'DevTools' elif sel == self._modding_guide_button: sel_name = 'ModdingGuide' elif sel == self._language_inform_checkbox: sel_name = 'LangInform' else: raise ValueError(f'unrecognized selection \'{sel}\'') elif sel == self._back_button: sel_name = 'Back' else: raise ValueError(f'unrecognized selection \'{sel}\'') assert bui.app.classic is not None bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} except Exception: logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: # pylint: disable=too-many-branches try: assert bui.app.classic is not None sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 'sel_name' ) if sel_name == 'Back': sel = self._back_button else: bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) if sel_name == 'VRTest': sel = self._vr_test_button elif sel_name == 'NetTest': sel = self._net_test_button elif sel_name == 'SendInfo': sel = self._send_info_button elif sel_name == 'Benchmarks': sel = self._benchmarks_button elif sel_name == 'KickIdlePlayers': sel = self._kick_idle_players_check_box.widget elif sel_name == 'ShowDemosWhenIdle': sel = self._show_demos_when_idle_check_box.widget elif sel_name == 'ShowDeprecatedLoginTypes': sel = self._show_deprecated_login_types_check_box.widget elif sel_name == 'ShowPing': sel = self._show_game_ping_check_box.widget elif sel_name == 'DisableCameraShake': sel = self._disable_camera_shake_check_box.widget elif ( sel_name == 'AlwaysUseInternalKeyboard' and self._always_use_internal_keyboard_check_box is not None ): sel = self._always_use_internal_keyboard_check_box.widget elif ( sel_name == 'DisableGyro' and self._disable_gyro_check_box is not None ): sel = self._disable_gyro_check_box.widget elif ( sel_name == 'Languages' and self._language_popup is not None ): sel = self._language_popup.get_button() elif sel_name == 'TranslationEditor': sel = self._translation_editor_button elif sel_name == 'ShowUserMods': sel = self._show_user_mods_button elif sel_name == 'Plugins': sel = self._plugins_button elif sel_name == 'DevTools': sel = self._dev_tools_button elif sel_name == 'ModdingGuide': sel = self._modding_guide_button elif sel_name == 'LangInform': sel = self._language_inform_checkbox else: sel = None if sel is not None: bui.containerwidget( edit=self._subcontainer, selected_child=sel, visible_child=sel, ) except Exception: logging.exception('Error restoring state for %s.', self) def _on_menu_open(self) -> None: self._menu_open = True def _on_menu_close(self) -> None: self._menu_open = False def _on_menu_choice(self, choice: str) -> None: bui.app.lang.setlanguage(None if choice == 'Auto' else choice) self._save_state() bui.apptimer(0.1, bui.WeakCall(self._rebuild)) def _completed_langs_cb(self, results: dict[str, Any] | None) -> None: if results is not None and results['langs'] is not None: self._complete_langs_list = results['langs'] self._complete_langs_error = False else: self._complete_langs_list = None self._complete_langs_error = True bui.apptimer(0.001, bui.WeakCall(self._update_lang_status))