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