bauiv1lib.mainmenu
Implements the main menu window.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Implements the main menu window.""" 4 5from __future__ import annotations 6 7from typing import TYPE_CHECKING, override 8import logging 9 10import bauiv1 as bui 11import bascenev1 as bs 12 13if TYPE_CHECKING: 14 from typing import Any, Callable 15 16 17class MainMenuWindow(bui.MainWindow): 18 """The main menu window.""" 19 20 def __init__( 21 self, 22 transition: str | None = 'in_right', 23 origin_widget: bui.Widget | None = None, 24 ): 25 26 # Preload some modules we use in a background thread so we won't 27 # have a visual hitch when the user taps them. 28 bui.app.threadpool.submit_no_wait(self._preload_modules) 29 30 bui.set_analytics_screen('Main Menu') 31 self._show_remote_app_info_on_first_launch() 32 33 # Make a vanilla container; we'll modify it to our needs in 34 # refresh. 35 super().__init__( 36 root_widget=bui.containerwidget( 37 toolbar_visibility=('menu_full_no_back') 38 ), 39 transition=transition, 40 origin_widget=origin_widget, 41 ) 42 43 # Grab this stuff in case it changes. 44 self._is_demo = bui.app.env.demo 45 self._is_arcade = bui.app.env.arcade 46 47 self._tdelay = 0.0 48 self._t_delay_inc = 0.02 49 self._t_delay_play = 1.7 50 self._use_autoselect = True 51 self._button_width = 200.0 52 self._button_height = 45.0 53 self._width = 100.0 54 self._height = 100.0 55 self._demo_menu_button: bui.Widget | None = None 56 self._gather_button: bui.Widget | None = None 57 self._play_button: bui.Widget | None = None 58 self._watch_button: bui.Widget | None = None 59 self._how_to_play_button: bui.Widget | None = None 60 self._credits_button: bui.Widget | None = None 61 62 self._refresh() 63 64 self._restore_state() 65 66 @override 67 def on_main_window_close(self) -> None: 68 self._save_state() 69 70 @override 71 def get_main_window_state(self) -> bui.MainWindowState: 72 # Support recreating our window for back/refresh purposes. 73 cls = type(self) 74 return bui.BasicMainWindowState( 75 create_call=lambda transition, origin_widget: cls( 76 transition=transition, origin_widget=origin_widget 77 ) 78 ) 79 80 @staticmethod 81 def _preload_modules() -> None: 82 """Preload modules we use; avoids hitches (called in bg thread).""" 83 # pylint: disable=cyclic-import 84 import bauiv1lib.getremote as _unused 85 import bauiv1lib.confirm as _unused2 86 import bauiv1lib.account.settings as _unused5 87 import bauiv1lib.store.browser as _unused6 88 import bauiv1lib.credits as _unused7 89 import bauiv1lib.help as _unused8 90 import bauiv1lib.settings.allsettings as _unused9 91 import bauiv1lib.gather as _unused10 92 import bauiv1lib.watch as _unused11 93 import bauiv1lib.play as _unused12 94 95 def _show_remote_app_info_on_first_launch(self) -> None: 96 app = bui.app 97 assert app.classic is not None 98 99 # The first time the non-in-game menu pops up, we might wanna 100 # show a 'get-remote-app' dialog in front of it. 101 if app.classic.first_main_menu: 102 app.classic.first_main_menu = False 103 try: 104 force_test = False 105 bs.get_local_active_input_devices_count() 106 if ( 107 (app.env.tv or app.classic.platform == 'mac') 108 and bui.app.config.get('launchCount', 0) <= 1 109 ) or force_test: 110 111 def _check_show_bs_remote_window() -> None: 112 try: 113 from bauiv1lib.getremote import GetBSRemoteWindow 114 115 bui.getsound('swish').play() 116 GetBSRemoteWindow() 117 except Exception: 118 logging.exception( 119 'Error showing get-remote window.' 120 ) 121 122 bui.apptimer(2.5, _check_show_bs_remote_window) 123 except Exception: 124 logging.exception('Error showing get-remote-app info.') 125 126 def get_play_button(self) -> bui.Widget | None: 127 """Return the play button.""" 128 return self._play_button 129 130 def _refresh(self) -> None: 131 # pylint: disable=too-many-statements 132 # pylint: disable=too-many-locals 133 134 classic = bui.app.classic 135 assert classic is not None 136 137 # Clear everything that was there. 138 children = self._root_widget.get_children() 139 for child in children: 140 child.delete() 141 142 self._tdelay = 0.0 143 self._t_delay_inc = 0.0 144 self._t_delay_play = 0.0 145 self._button_width = 200.0 146 self._button_height = 45.0 147 148 self._r = 'mainMenu' 149 150 app = bui.app 151 assert app.classic is not None 152 uiscale = app.ui_v1.uiscale 153 154 # Temp note about UI changes. 155 if bool(False): 156 bui.textwidget( 157 parent=self._root_widget, 158 position=( 159 (-400, 400) 160 if uiscale is bui.UIScale.LARGE 161 else ( 162 (-270, 320) 163 if uiscale is bui.UIScale.MEDIUM 164 else (-280, 280) 165 ) 166 ), 167 size=(0, 0), 168 scale=0.4, 169 flatness=1.0, 170 text=( 171 'WARNING: This build contains a revamped UI\n' 172 'which is still a work-in-progress. A number\n' 173 'of features are not currently functional or\n' 174 'contain bugs. To go back to the stable legacy UI,\n' 175 'grab version 1.7.36 from ballistica.net' 176 ), 177 h_align='left', 178 v_align='top', 179 ) 180 181 self._have_quit_button = app.classic.platform in ( 182 'windows', 183 'mac', 184 'linux', 185 ) 186 187 if not classic.did_menu_intro: 188 self._tdelay = 1.6 189 self._t_delay_inc = 0.03 190 classic.did_menu_intro = True 191 192 td1 = 2 193 td2 = 1 194 td3 = 0 195 td4 = -1 196 td5 = -2 197 198 self._width = 400.0 199 self._height = 200.0 200 201 play_button_width = self._button_width * 0.65 202 play_button_height = self._button_height * 1.1 203 play_button_scale = 1.7 204 hspace = 20.0 205 side_button_width = self._button_width * 0.4 206 side_button_height = side_button_width 207 side_button_scale = 0.95 208 side_button_y_offs = 5.0 209 hspace2 = 15.0 210 side_button_2_width = self._button_width * 1.0 211 side_button_2_height = side_button_2_width * 0.3 212 side_button_2_y_offs = 10.0 213 side_button_2_scale = 0.5 214 215 if uiscale is bui.UIScale.SMALL: 216 root_widget_scale = 1.3 217 button_y_offs = -20.0 218 self._button_height *= 1.3 219 elif uiscale is bui.UIScale.MEDIUM: 220 root_widget_scale = 1.3 221 button_y_offs = -55.0 222 self._button_height *= 1.25 223 else: 224 root_widget_scale = 1.0 225 button_y_offs = -90.0 226 self._button_height *= 1.2 227 228 bui.containerwidget( 229 edit=self._root_widget, 230 size=(self._width, self._height), 231 background=False, 232 scale=root_widget_scale, 233 ) 234 235 # Version/copyright info. 236 thistdelay = self._tdelay + td3 * self._t_delay_inc 237 bui.textwidget( 238 parent=self._root_widget, 239 position=(self._width * 0.5, button_y_offs - 10), 240 size=(0, 0), 241 scale=0.4, 242 flatness=1.0, 243 color=(1, 1, 1, 0.3), 244 text=( 245 f'{app.env.engine_version}' 246 f' build {app.env.engine_build_number}.' 247 f' Copyright 2024 Eric Froemling.' 248 ), 249 h_align='center', 250 v_align='center', 251 # transition_delay=self._t_delay_play, 252 transition_delay=thistdelay, 253 ) 254 255 # In kiosk mode, provide a button to get back to the kiosk menu. 256 if bui.app.env.demo or bui.app.env.arcade: 257 # h, v, scale = positions[self._p_index] 258 h = self._width * 0.5 259 v = button_y_offs 260 scale = 1.0 261 this_b_width = self._button_width * 0.4 * scale 262 # demo_menu_delay = ( 263 # 0.0 264 # if self._t_delay_play == 0.0 265 # else max(0, self._t_delay_play + 0.1) 266 # ) 267 demo_menu_delay = 0.0 268 self._demo_menu_button = bui.buttonwidget( 269 parent=self._root_widget, 270 id='demo', 271 position=(self._width * 0.5 - this_b_width * 0.5, v + 90), 272 size=(this_b_width, 45), 273 autoselect=True, 274 color=(0.45, 0.55, 0.45), 275 textcolor=(0.7, 0.8, 0.7), 276 label=bui.Lstr( 277 resource=( 278 'modeArcadeText' 279 if bui.app.env.arcade 280 else 'modeDemoText' 281 ) 282 ), 283 transition_delay=demo_menu_delay, 284 on_activate_call=self.main_window_back, 285 ) 286 else: 287 self._demo_menu_button = None 288 289 # Gather button 290 h = self._width * 0.5 291 h = ( 292 self._width * 0.5 293 - play_button_width * play_button_scale * 0.5 294 - hspace 295 - side_button_width * side_button_scale * 0.5 296 ) 297 v = button_y_offs + side_button_y_offs 298 299 thistdelay = self._tdelay + td2 * self._t_delay_inc 300 self._gather_button = btn = bui.buttonwidget( 301 parent=self._root_widget, 302 position=(h - side_button_width * side_button_scale * 0.5, v), 303 size=(side_button_width, side_button_height), 304 scale=side_button_scale, 305 autoselect=self._use_autoselect, 306 button_type='square', 307 label='', 308 transition_delay=thistdelay, 309 on_activate_call=self._gather_press, 310 ) 311 bui.textwidget( 312 parent=self._root_widget, 313 position=(h, v + side_button_height * side_button_scale * 0.25), 314 size=(0, 0), 315 scale=0.75, 316 transition_delay=thistdelay, 317 draw_controller=btn, 318 color=(0.75, 1.0, 0.7), 319 maxwidth=side_button_width * side_button_scale * 0.8, 320 text=bui.Lstr(resource='gatherWindow.titleText'), 321 h_align='center', 322 v_align='center', 323 ) 324 icon_size = side_button_width * side_button_scale * 0.63 325 bui.imagewidget( 326 parent=self._root_widget, 327 size=(icon_size, icon_size), 328 draw_controller=btn, 329 transition_delay=thistdelay, 330 position=( 331 h - 0.5 * icon_size, 332 v 333 + 0.65 * side_button_height * side_button_scale 334 - 0.5 * icon_size, 335 ), 336 texture=bui.gettexture('usersButton'), 337 ) 338 thistdelay = self._tdelay + td1 * self._t_delay_inc 339 340 h -= ( 341 side_button_width * side_button_scale * 0.5 342 + hspace2 343 + side_button_2_width * side_button_2_scale 344 ) 345 v = button_y_offs + side_button_2_y_offs 346 347 btn = bui.buttonwidget( 348 parent=self._root_widget, 349 id='howtoplay', 350 position=(h, v), 351 autoselect=self._use_autoselect, 352 size=(side_button_2_width, side_button_2_height * 2.0), 353 button_type='square', 354 scale=side_button_2_scale, 355 label=bui.Lstr(resource=f'{self._r}.howToPlayText'), 356 transition_delay=thistdelay, 357 on_activate_call=self._howtoplay, 358 ) 359 self._how_to_play_button = btn 360 361 # Play button. 362 h = self._width * 0.5 363 v = button_y_offs 364 assert play_button_width is not None 365 assert play_button_height is not None 366 thistdelay = self._tdelay + td3 * self._t_delay_inc 367 self._play_button = start_button = bui.buttonwidget( 368 parent=self._root_widget, 369 position=(h - play_button_width * 0.5 * play_button_scale, v), 370 size=(play_button_width, play_button_height), 371 autoselect=self._use_autoselect, 372 scale=play_button_scale, 373 text_res_scale=2.0, 374 label=bui.Lstr(resource='playText'), 375 transition_delay=thistdelay, 376 on_activate_call=self._play_press, 377 ) 378 bui.containerwidget( 379 edit=self._root_widget, 380 start_button=start_button, 381 selected_child=start_button, 382 ) 383 384 # self._tdelay += self._t_delay_inc 385 386 h = ( 387 self._width * 0.5 388 + play_button_width * play_button_scale * 0.5 389 + hspace 390 + side_button_width * side_button_scale * 0.5 391 ) 392 v = button_y_offs + side_button_y_offs 393 thistdelay = self._tdelay + td4 * self._t_delay_inc 394 self._watch_button = btn = bui.buttonwidget( 395 parent=self._root_widget, 396 position=(h - side_button_width * side_button_scale * 0.5, v), 397 size=(side_button_width, side_button_height), 398 scale=side_button_scale, 399 autoselect=self._use_autoselect, 400 button_type='square', 401 label='', 402 transition_delay=thistdelay, 403 on_activate_call=self._watch_press, 404 ) 405 bui.textwidget( 406 parent=self._root_widget, 407 position=(h, v + side_button_height * side_button_scale * 0.25), 408 size=(0, 0), 409 scale=0.75, 410 transition_delay=thistdelay, 411 color=(0.75, 1.0, 0.7), 412 draw_controller=btn, 413 maxwidth=side_button_width * side_button_scale * 0.8, 414 text=bui.Lstr(resource='watchWindow.titleText'), 415 h_align='center', 416 v_align='center', 417 ) 418 icon_size = side_button_width * side_button_scale * 0.63 419 bui.imagewidget( 420 parent=self._root_widget, 421 size=(icon_size, icon_size), 422 draw_controller=btn, 423 transition_delay=thistdelay, 424 position=( 425 h - 0.5 * icon_size, 426 v 427 + 0.65 * side_button_height * side_button_scale 428 - 0.5 * icon_size, 429 ), 430 texture=bui.gettexture('tv'), 431 ) 432 433 # Credits button. 434 # self._tdelay += self._t_delay_inc 435 thistdelay = self._tdelay + td5 * self._t_delay_inc 436 437 h += side_button_width * side_button_scale * 0.5 + hspace2 438 v = button_y_offs + side_button_2_y_offs 439 440 if self._have_quit_button: 441 v += 1.17 * side_button_2_height * side_button_2_scale 442 443 self._credits_button = bui.buttonwidget( 444 parent=self._root_widget, 445 position=(h, v), 446 button_type=None if self._have_quit_button else 'square', 447 size=( 448 side_button_2_width, 449 side_button_2_height * (1.0 if self._have_quit_button else 2.0), 450 ), 451 scale=side_button_2_scale, 452 autoselect=self._use_autoselect, 453 label=bui.Lstr(resource=f'{self._r}.creditsText'), 454 transition_delay=thistdelay, 455 on_activate_call=self._credits, 456 ) 457 # self._tdelay += self._t_delay_inc 458 459 self._quit_button: bui.Widget | None 460 if self._have_quit_button: 461 v -= 1.1 * side_button_2_height * side_button_2_scale 462 self._quit_button = quit_button = bui.buttonwidget( 463 parent=self._root_widget, 464 autoselect=self._use_autoselect, 465 position=(h, v), 466 size=(side_button_2_width, side_button_2_height), 467 scale=side_button_2_scale, 468 label=bui.Lstr( 469 resource=self._r 470 + ( 471 '.quitText' 472 if 'Mac' in app.classic.legacy_user_agent_string 473 else '.exitGameText' 474 ) 475 ), 476 on_activate_call=self._quit, 477 transition_delay=thistdelay, 478 ) 479 480 bui.containerwidget( 481 edit=self._root_widget, cancel_button=quit_button 482 ) 483 # self._tdelay += self._t_delay_inc 484 else: 485 self._quit_button = None 486 487 # If we're not in-game, have no quit button, and this is 488 # android, we want back presses to quit our activity. 489 if app.classic.platform == 'android': 490 491 def _do_quit() -> None: 492 bui.quit(confirm=True, quit_type=bui.QuitType.BACK) 493 494 bui.containerwidget( 495 edit=self._root_widget, on_cancel_call=_do_quit 496 ) 497 498 def _quit(self) -> None: 499 # pylint: disable=cyclic-import 500 from bauiv1lib.confirm import QuitWindow 501 502 # no-op if we're not currently in control. 503 if not self.main_window_has_control(): 504 return 505 506 # Note: Normally we should go through bui.quit(confirm=True) but 507 # invoking the window directly lets us scale it up from the 508 # button. 509 QuitWindow(origin_widget=self._quit_button) 510 511 def _credits(self) -> None: 512 # pylint: disable=cyclic-import 513 from bauiv1lib.credits import CreditsWindow 514 515 # no-op if we're not currently in control. 516 if not self.main_window_has_control(): 517 return 518 519 self.main_window_replace( 520 CreditsWindow(origin_widget=self._credits_button), 521 ) 522 523 def _howtoplay(self) -> None: 524 # pylint: disable=cyclic-import 525 from bauiv1lib.help import HelpWindow 526 527 # no-op if we're not currently in control. 528 if not self.main_window_has_control(): 529 return 530 531 self.main_window_replace( 532 HelpWindow(origin_widget=self._how_to_play_button), 533 ) 534 535 def _save_state(self) -> None: 536 try: 537 sel = self._root_widget.get_selected_child() 538 if sel == self._play_button: 539 sel_name = 'Start' 540 elif sel == self._gather_button: 541 sel_name = 'Gather' 542 elif sel == self._watch_button: 543 sel_name = 'Watch' 544 elif sel == self._how_to_play_button: 545 sel_name = 'HowToPlay' 546 elif sel == self._credits_button: 547 sel_name = 'Credits' 548 elif sel == self._quit_button: 549 sel_name = 'Quit' 550 elif sel == self._demo_menu_button: 551 sel_name = 'DemoMenu' 552 else: 553 print(f'Unknown widget in main menu selection: {sel}.') 554 sel_name = 'Start' 555 bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} 556 except Exception: 557 logging.exception('Error saving state for %s.', self) 558 559 def _restore_state(self) -> None: 560 try: 561 562 sel: bui.Widget | None 563 564 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 565 'sel_name' 566 ) 567 assert isinstance(sel_name, (str, type(None))) 568 if sel_name is None: 569 sel_name = 'Start' 570 if sel_name == 'HowToPlay': 571 sel = self._how_to_play_button 572 elif sel_name == 'Gather': 573 sel = self._gather_button 574 elif sel_name == 'Watch': 575 sel = self._watch_button 576 elif sel_name == 'Credits': 577 sel = self._credits_button 578 elif sel_name == 'Quit': 579 sel = self._quit_button 580 elif sel_name == 'DemoMenu': 581 sel = self._demo_menu_button 582 else: 583 sel = self._play_button 584 if sel is not None: 585 bui.containerwidget(edit=self._root_widget, selected_child=sel) 586 587 except Exception: 588 logging.exception('Error restoring state for %s.', self) 589 590 def _gather_press(self) -> None: 591 # pylint: disable=cyclic-import 592 from bauiv1lib.gather import GatherWindow 593 594 # no-op if we're not currently in control. 595 if not self.main_window_has_control(): 596 return 597 598 self.main_window_replace( 599 GatherWindow(origin_widget=self._gather_button) 600 ) 601 602 def _watch_press(self) -> None: 603 # pylint: disable=cyclic-import 604 from bauiv1lib.watch import WatchWindow 605 606 # no-op if we're not currently in control. 607 if not self.main_window_has_control(): 608 return 609 610 self.main_window_replace( 611 WatchWindow(origin_widget=self._watch_button), 612 ) 613 614 def _play_press(self) -> None: 615 # pylint: disable=cyclic-import 616 from bauiv1lib.play import PlayWindow 617 618 # no-op if we're not currently in control. 619 if not self.main_window_has_control(): 620 return 621 622 self.main_window_replace(PlayWindow(origin_widget=self._play_button))
class
MainMenuWindow(bauiv1._uitypes.MainWindow):
18class MainMenuWindow(bui.MainWindow): 19 """The main menu window.""" 20 21 def __init__( 22 self, 23 transition: str | None = 'in_right', 24 origin_widget: bui.Widget | None = None, 25 ): 26 27 # Preload some modules we use in a background thread so we won't 28 # have a visual hitch when the user taps them. 29 bui.app.threadpool.submit_no_wait(self._preload_modules) 30 31 bui.set_analytics_screen('Main Menu') 32 self._show_remote_app_info_on_first_launch() 33 34 # Make a vanilla container; we'll modify it to our needs in 35 # refresh. 36 super().__init__( 37 root_widget=bui.containerwidget( 38 toolbar_visibility=('menu_full_no_back') 39 ), 40 transition=transition, 41 origin_widget=origin_widget, 42 ) 43 44 # Grab this stuff in case it changes. 45 self._is_demo = bui.app.env.demo 46 self._is_arcade = bui.app.env.arcade 47 48 self._tdelay = 0.0 49 self._t_delay_inc = 0.02 50 self._t_delay_play = 1.7 51 self._use_autoselect = True 52 self._button_width = 200.0 53 self._button_height = 45.0 54 self._width = 100.0 55 self._height = 100.0 56 self._demo_menu_button: bui.Widget | None = None 57 self._gather_button: bui.Widget | None = None 58 self._play_button: bui.Widget | None = None 59 self._watch_button: bui.Widget | None = None 60 self._how_to_play_button: bui.Widget | None = None 61 self._credits_button: bui.Widget | None = None 62 63 self._refresh() 64 65 self._restore_state() 66 67 @override 68 def on_main_window_close(self) -> None: 69 self._save_state() 70 71 @override 72 def get_main_window_state(self) -> bui.MainWindowState: 73 # Support recreating our window for back/refresh purposes. 74 cls = type(self) 75 return bui.BasicMainWindowState( 76 create_call=lambda transition, origin_widget: cls( 77 transition=transition, origin_widget=origin_widget 78 ) 79 ) 80 81 @staticmethod 82 def _preload_modules() -> None: 83 """Preload modules we use; avoids hitches (called in bg thread).""" 84 # pylint: disable=cyclic-import 85 import bauiv1lib.getremote as _unused 86 import bauiv1lib.confirm as _unused2 87 import bauiv1lib.account.settings as _unused5 88 import bauiv1lib.store.browser as _unused6 89 import bauiv1lib.credits as _unused7 90 import bauiv1lib.help as _unused8 91 import bauiv1lib.settings.allsettings as _unused9 92 import bauiv1lib.gather as _unused10 93 import bauiv1lib.watch as _unused11 94 import bauiv1lib.play as _unused12 95 96 def _show_remote_app_info_on_first_launch(self) -> None: 97 app = bui.app 98 assert app.classic is not None 99 100 # The first time the non-in-game menu pops up, we might wanna 101 # show a 'get-remote-app' dialog in front of it. 102 if app.classic.first_main_menu: 103 app.classic.first_main_menu = False 104 try: 105 force_test = False 106 bs.get_local_active_input_devices_count() 107 if ( 108 (app.env.tv or app.classic.platform == 'mac') 109 and bui.app.config.get('launchCount', 0) <= 1 110 ) or force_test: 111 112 def _check_show_bs_remote_window() -> None: 113 try: 114 from bauiv1lib.getremote import GetBSRemoteWindow 115 116 bui.getsound('swish').play() 117 GetBSRemoteWindow() 118 except Exception: 119 logging.exception( 120 'Error showing get-remote window.' 121 ) 122 123 bui.apptimer(2.5, _check_show_bs_remote_window) 124 except Exception: 125 logging.exception('Error showing get-remote-app info.') 126 127 def get_play_button(self) -> bui.Widget | None: 128 """Return the play button.""" 129 return self._play_button 130 131 def _refresh(self) -> None: 132 # pylint: disable=too-many-statements 133 # pylint: disable=too-many-locals 134 135 classic = bui.app.classic 136 assert classic is not None 137 138 # Clear everything that was there. 139 children = self._root_widget.get_children() 140 for child in children: 141 child.delete() 142 143 self._tdelay = 0.0 144 self._t_delay_inc = 0.0 145 self._t_delay_play = 0.0 146 self._button_width = 200.0 147 self._button_height = 45.0 148 149 self._r = 'mainMenu' 150 151 app = bui.app 152 assert app.classic is not None 153 uiscale = app.ui_v1.uiscale 154 155 # Temp note about UI changes. 156 if bool(False): 157 bui.textwidget( 158 parent=self._root_widget, 159 position=( 160 (-400, 400) 161 if uiscale is bui.UIScale.LARGE 162 else ( 163 (-270, 320) 164 if uiscale is bui.UIScale.MEDIUM 165 else (-280, 280) 166 ) 167 ), 168 size=(0, 0), 169 scale=0.4, 170 flatness=1.0, 171 text=( 172 'WARNING: This build contains a revamped UI\n' 173 'which is still a work-in-progress. A number\n' 174 'of features are not currently functional or\n' 175 'contain bugs. To go back to the stable legacy UI,\n' 176 'grab version 1.7.36 from ballistica.net' 177 ), 178 h_align='left', 179 v_align='top', 180 ) 181 182 self._have_quit_button = app.classic.platform in ( 183 'windows', 184 'mac', 185 'linux', 186 ) 187 188 if not classic.did_menu_intro: 189 self._tdelay = 1.6 190 self._t_delay_inc = 0.03 191 classic.did_menu_intro = True 192 193 td1 = 2 194 td2 = 1 195 td3 = 0 196 td4 = -1 197 td5 = -2 198 199 self._width = 400.0 200 self._height = 200.0 201 202 play_button_width = self._button_width * 0.65 203 play_button_height = self._button_height * 1.1 204 play_button_scale = 1.7 205 hspace = 20.0 206 side_button_width = self._button_width * 0.4 207 side_button_height = side_button_width 208 side_button_scale = 0.95 209 side_button_y_offs = 5.0 210 hspace2 = 15.0 211 side_button_2_width = self._button_width * 1.0 212 side_button_2_height = side_button_2_width * 0.3 213 side_button_2_y_offs = 10.0 214 side_button_2_scale = 0.5 215 216 if uiscale is bui.UIScale.SMALL: 217 root_widget_scale = 1.3 218 button_y_offs = -20.0 219 self._button_height *= 1.3 220 elif uiscale is bui.UIScale.MEDIUM: 221 root_widget_scale = 1.3 222 button_y_offs = -55.0 223 self._button_height *= 1.25 224 else: 225 root_widget_scale = 1.0 226 button_y_offs = -90.0 227 self._button_height *= 1.2 228 229 bui.containerwidget( 230 edit=self._root_widget, 231 size=(self._width, self._height), 232 background=False, 233 scale=root_widget_scale, 234 ) 235 236 # Version/copyright info. 237 thistdelay = self._tdelay + td3 * self._t_delay_inc 238 bui.textwidget( 239 parent=self._root_widget, 240 position=(self._width * 0.5, button_y_offs - 10), 241 size=(0, 0), 242 scale=0.4, 243 flatness=1.0, 244 color=(1, 1, 1, 0.3), 245 text=( 246 f'{app.env.engine_version}' 247 f' build {app.env.engine_build_number}.' 248 f' Copyright 2024 Eric Froemling.' 249 ), 250 h_align='center', 251 v_align='center', 252 # transition_delay=self._t_delay_play, 253 transition_delay=thistdelay, 254 ) 255 256 # In kiosk mode, provide a button to get back to the kiosk menu. 257 if bui.app.env.demo or bui.app.env.arcade: 258 # h, v, scale = positions[self._p_index] 259 h = self._width * 0.5 260 v = button_y_offs 261 scale = 1.0 262 this_b_width = self._button_width * 0.4 * scale 263 # demo_menu_delay = ( 264 # 0.0 265 # if self._t_delay_play == 0.0 266 # else max(0, self._t_delay_play + 0.1) 267 # ) 268 demo_menu_delay = 0.0 269 self._demo_menu_button = bui.buttonwidget( 270 parent=self._root_widget, 271 id='demo', 272 position=(self._width * 0.5 - this_b_width * 0.5, v + 90), 273 size=(this_b_width, 45), 274 autoselect=True, 275 color=(0.45, 0.55, 0.45), 276 textcolor=(0.7, 0.8, 0.7), 277 label=bui.Lstr( 278 resource=( 279 'modeArcadeText' 280 if bui.app.env.arcade 281 else 'modeDemoText' 282 ) 283 ), 284 transition_delay=demo_menu_delay, 285 on_activate_call=self.main_window_back, 286 ) 287 else: 288 self._demo_menu_button = None 289 290 # Gather button 291 h = self._width * 0.5 292 h = ( 293 self._width * 0.5 294 - play_button_width * play_button_scale * 0.5 295 - hspace 296 - side_button_width * side_button_scale * 0.5 297 ) 298 v = button_y_offs + side_button_y_offs 299 300 thistdelay = self._tdelay + td2 * self._t_delay_inc 301 self._gather_button = btn = bui.buttonwidget( 302 parent=self._root_widget, 303 position=(h - side_button_width * side_button_scale * 0.5, v), 304 size=(side_button_width, side_button_height), 305 scale=side_button_scale, 306 autoselect=self._use_autoselect, 307 button_type='square', 308 label='', 309 transition_delay=thistdelay, 310 on_activate_call=self._gather_press, 311 ) 312 bui.textwidget( 313 parent=self._root_widget, 314 position=(h, v + side_button_height * side_button_scale * 0.25), 315 size=(0, 0), 316 scale=0.75, 317 transition_delay=thistdelay, 318 draw_controller=btn, 319 color=(0.75, 1.0, 0.7), 320 maxwidth=side_button_width * side_button_scale * 0.8, 321 text=bui.Lstr(resource='gatherWindow.titleText'), 322 h_align='center', 323 v_align='center', 324 ) 325 icon_size = side_button_width * side_button_scale * 0.63 326 bui.imagewidget( 327 parent=self._root_widget, 328 size=(icon_size, icon_size), 329 draw_controller=btn, 330 transition_delay=thistdelay, 331 position=( 332 h - 0.5 * icon_size, 333 v 334 + 0.65 * side_button_height * side_button_scale 335 - 0.5 * icon_size, 336 ), 337 texture=bui.gettexture('usersButton'), 338 ) 339 thistdelay = self._tdelay + td1 * self._t_delay_inc 340 341 h -= ( 342 side_button_width * side_button_scale * 0.5 343 + hspace2 344 + side_button_2_width * side_button_2_scale 345 ) 346 v = button_y_offs + side_button_2_y_offs 347 348 btn = bui.buttonwidget( 349 parent=self._root_widget, 350 id='howtoplay', 351 position=(h, v), 352 autoselect=self._use_autoselect, 353 size=(side_button_2_width, side_button_2_height * 2.0), 354 button_type='square', 355 scale=side_button_2_scale, 356 label=bui.Lstr(resource=f'{self._r}.howToPlayText'), 357 transition_delay=thistdelay, 358 on_activate_call=self._howtoplay, 359 ) 360 self._how_to_play_button = btn 361 362 # Play button. 363 h = self._width * 0.5 364 v = button_y_offs 365 assert play_button_width is not None 366 assert play_button_height is not None 367 thistdelay = self._tdelay + td3 * self._t_delay_inc 368 self._play_button = start_button = bui.buttonwidget( 369 parent=self._root_widget, 370 position=(h - play_button_width * 0.5 * play_button_scale, v), 371 size=(play_button_width, play_button_height), 372 autoselect=self._use_autoselect, 373 scale=play_button_scale, 374 text_res_scale=2.0, 375 label=bui.Lstr(resource='playText'), 376 transition_delay=thistdelay, 377 on_activate_call=self._play_press, 378 ) 379 bui.containerwidget( 380 edit=self._root_widget, 381 start_button=start_button, 382 selected_child=start_button, 383 ) 384 385 # self._tdelay += self._t_delay_inc 386 387 h = ( 388 self._width * 0.5 389 + play_button_width * play_button_scale * 0.5 390 + hspace 391 + side_button_width * side_button_scale * 0.5 392 ) 393 v = button_y_offs + side_button_y_offs 394 thistdelay = self._tdelay + td4 * self._t_delay_inc 395 self._watch_button = btn = bui.buttonwidget( 396 parent=self._root_widget, 397 position=(h - side_button_width * side_button_scale * 0.5, v), 398 size=(side_button_width, side_button_height), 399 scale=side_button_scale, 400 autoselect=self._use_autoselect, 401 button_type='square', 402 label='', 403 transition_delay=thistdelay, 404 on_activate_call=self._watch_press, 405 ) 406 bui.textwidget( 407 parent=self._root_widget, 408 position=(h, v + side_button_height * side_button_scale * 0.25), 409 size=(0, 0), 410 scale=0.75, 411 transition_delay=thistdelay, 412 color=(0.75, 1.0, 0.7), 413 draw_controller=btn, 414 maxwidth=side_button_width * side_button_scale * 0.8, 415 text=bui.Lstr(resource='watchWindow.titleText'), 416 h_align='center', 417 v_align='center', 418 ) 419 icon_size = side_button_width * side_button_scale * 0.63 420 bui.imagewidget( 421 parent=self._root_widget, 422 size=(icon_size, icon_size), 423 draw_controller=btn, 424 transition_delay=thistdelay, 425 position=( 426 h - 0.5 * icon_size, 427 v 428 + 0.65 * side_button_height * side_button_scale 429 - 0.5 * icon_size, 430 ), 431 texture=bui.gettexture('tv'), 432 ) 433 434 # Credits button. 435 # self._tdelay += self._t_delay_inc 436 thistdelay = self._tdelay + td5 * self._t_delay_inc 437 438 h += side_button_width * side_button_scale * 0.5 + hspace2 439 v = button_y_offs + side_button_2_y_offs 440 441 if self._have_quit_button: 442 v += 1.17 * side_button_2_height * side_button_2_scale 443 444 self._credits_button = bui.buttonwidget( 445 parent=self._root_widget, 446 position=(h, v), 447 button_type=None if self._have_quit_button else 'square', 448 size=( 449 side_button_2_width, 450 side_button_2_height * (1.0 if self._have_quit_button else 2.0), 451 ), 452 scale=side_button_2_scale, 453 autoselect=self._use_autoselect, 454 label=bui.Lstr(resource=f'{self._r}.creditsText'), 455 transition_delay=thistdelay, 456 on_activate_call=self._credits, 457 ) 458 # self._tdelay += self._t_delay_inc 459 460 self._quit_button: bui.Widget | None 461 if self._have_quit_button: 462 v -= 1.1 * side_button_2_height * side_button_2_scale 463 self._quit_button = quit_button = bui.buttonwidget( 464 parent=self._root_widget, 465 autoselect=self._use_autoselect, 466 position=(h, v), 467 size=(side_button_2_width, side_button_2_height), 468 scale=side_button_2_scale, 469 label=bui.Lstr( 470 resource=self._r 471 + ( 472 '.quitText' 473 if 'Mac' in app.classic.legacy_user_agent_string 474 else '.exitGameText' 475 ) 476 ), 477 on_activate_call=self._quit, 478 transition_delay=thistdelay, 479 ) 480 481 bui.containerwidget( 482 edit=self._root_widget, cancel_button=quit_button 483 ) 484 # self._tdelay += self._t_delay_inc 485 else: 486 self._quit_button = None 487 488 # If we're not in-game, have no quit button, and this is 489 # android, we want back presses to quit our activity. 490 if app.classic.platform == 'android': 491 492 def _do_quit() -> None: 493 bui.quit(confirm=True, quit_type=bui.QuitType.BACK) 494 495 bui.containerwidget( 496 edit=self._root_widget, on_cancel_call=_do_quit 497 ) 498 499 def _quit(self) -> None: 500 # pylint: disable=cyclic-import 501 from bauiv1lib.confirm import QuitWindow 502 503 # no-op if we're not currently in control. 504 if not self.main_window_has_control(): 505 return 506 507 # Note: Normally we should go through bui.quit(confirm=True) but 508 # invoking the window directly lets us scale it up from the 509 # button. 510 QuitWindow(origin_widget=self._quit_button) 511 512 def _credits(self) -> None: 513 # pylint: disable=cyclic-import 514 from bauiv1lib.credits import CreditsWindow 515 516 # no-op if we're not currently in control. 517 if not self.main_window_has_control(): 518 return 519 520 self.main_window_replace( 521 CreditsWindow(origin_widget=self._credits_button), 522 ) 523 524 def _howtoplay(self) -> None: 525 # pylint: disable=cyclic-import 526 from bauiv1lib.help import HelpWindow 527 528 # no-op if we're not currently in control. 529 if not self.main_window_has_control(): 530 return 531 532 self.main_window_replace( 533 HelpWindow(origin_widget=self._how_to_play_button), 534 ) 535 536 def _save_state(self) -> None: 537 try: 538 sel = self._root_widget.get_selected_child() 539 if sel == self._play_button: 540 sel_name = 'Start' 541 elif sel == self._gather_button: 542 sel_name = 'Gather' 543 elif sel == self._watch_button: 544 sel_name = 'Watch' 545 elif sel == self._how_to_play_button: 546 sel_name = 'HowToPlay' 547 elif sel == self._credits_button: 548 sel_name = 'Credits' 549 elif sel == self._quit_button: 550 sel_name = 'Quit' 551 elif sel == self._demo_menu_button: 552 sel_name = 'DemoMenu' 553 else: 554 print(f'Unknown widget in main menu selection: {sel}.') 555 sel_name = 'Start' 556 bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} 557 except Exception: 558 logging.exception('Error saving state for %s.', self) 559 560 def _restore_state(self) -> None: 561 try: 562 563 sel: bui.Widget | None 564 565 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 566 'sel_name' 567 ) 568 assert isinstance(sel_name, (str, type(None))) 569 if sel_name is None: 570 sel_name = 'Start' 571 if sel_name == 'HowToPlay': 572 sel = self._how_to_play_button 573 elif sel_name == 'Gather': 574 sel = self._gather_button 575 elif sel_name == 'Watch': 576 sel = self._watch_button 577 elif sel_name == 'Credits': 578 sel = self._credits_button 579 elif sel_name == 'Quit': 580 sel = self._quit_button 581 elif sel_name == 'DemoMenu': 582 sel = self._demo_menu_button 583 else: 584 sel = self._play_button 585 if sel is not None: 586 bui.containerwidget(edit=self._root_widget, selected_child=sel) 587 588 except Exception: 589 logging.exception('Error restoring state for %s.', self) 590 591 def _gather_press(self) -> None: 592 # pylint: disable=cyclic-import 593 from bauiv1lib.gather import GatherWindow 594 595 # no-op if we're not currently in control. 596 if not self.main_window_has_control(): 597 return 598 599 self.main_window_replace( 600 GatherWindow(origin_widget=self._gather_button) 601 ) 602 603 def _watch_press(self) -> None: 604 # pylint: disable=cyclic-import 605 from bauiv1lib.watch import WatchWindow 606 607 # no-op if we're not currently in control. 608 if not self.main_window_has_control(): 609 return 610 611 self.main_window_replace( 612 WatchWindow(origin_widget=self._watch_button), 613 ) 614 615 def _play_press(self) -> None: 616 # pylint: disable=cyclic-import 617 from bauiv1lib.play import PlayWindow 618 619 # no-op if we're not currently in control. 620 if not self.main_window_has_control(): 621 return 622 623 self.main_window_replace(PlayWindow(origin_widget=self._play_button))
The main menu window.
MainMenuWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
21 def __init__( 22 self, 23 transition: str | None = 'in_right', 24 origin_widget: bui.Widget | None = None, 25 ): 26 27 # Preload some modules we use in a background thread so we won't 28 # have a visual hitch when the user taps them. 29 bui.app.threadpool.submit_no_wait(self._preload_modules) 30 31 bui.set_analytics_screen('Main Menu') 32 self._show_remote_app_info_on_first_launch() 33 34 # Make a vanilla container; we'll modify it to our needs in 35 # refresh. 36 super().__init__( 37 root_widget=bui.containerwidget( 38 toolbar_visibility=('menu_full_no_back') 39 ), 40 transition=transition, 41 origin_widget=origin_widget, 42 ) 43 44 # Grab this stuff in case it changes. 45 self._is_demo = bui.app.env.demo 46 self._is_arcade = bui.app.env.arcade 47 48 self._tdelay = 0.0 49 self._t_delay_inc = 0.02 50 self._t_delay_play = 1.7 51 self._use_autoselect = True 52 self._button_width = 200.0 53 self._button_height = 45.0 54 self._width = 100.0 55 self._height = 100.0 56 self._demo_menu_button: bui.Widget | None = None 57 self._gather_button: bui.Widget | None = None 58 self._play_button: bui.Widget | None = None 59 self._watch_button: bui.Widget | None = None 60 self._how_to_play_button: bui.Widget | None = None 61 self._credits_button: bui.Widget | None = None 62 63 self._refresh() 64 65 self._restore_state()
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.
@override
def
on_main_window_close(self) -> None:
Called before transitioning out a main window.
A good opportunity to save window state/etc.
71 @override 72 def get_main_window_state(self) -> bui.MainWindowState: 73 # Support recreating our window for back/refresh purposes. 74 cls = type(self) 75 return bui.BasicMainWindowState( 76 create_call=lambda transition, origin_widget: cls( 77 transition=transition, origin_widget=origin_widget 78 ) 79 )
Return a WindowState to recreate this window, if supported.