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