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