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