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