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