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