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.helpui 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.7 192 self._t_delay_inc = 0.05 193 self._t_delay_play = 1.7 194 classic.did_menu_intro = True 195 196 self._width = 400.0 197 self._height = 200.0 198 199 play_button_width = self._button_width * 0.65 200 play_button_height = self._button_height * 1.1 201 play_button_scale = 1.7 202 hspace = 20.0 203 side_button_width = self._button_width * 0.4 204 side_button_height = side_button_width 205 side_button_scale = 0.95 206 side_button_y_offs = 5.0 207 hspace2 = 15.0 208 side_button_2_width = self._button_width * 1.0 209 side_button_2_height = side_button_2_width * 0.3 210 side_button_2_y_offs = 10.0 211 side_button_2_scale = 0.5 212 213 if uiscale is bui.UIScale.SMALL: 214 root_widget_scale = 1.3 215 button_y_offs = -20.0 216 self._button_height *= 1.3 217 elif uiscale is bui.UIScale.MEDIUM: 218 root_widget_scale = 1.3 219 button_y_offs = -55.0 220 self._button_height *= 1.25 221 else: 222 root_widget_scale = 1.0 223 button_y_offs = -90.0 224 self._button_height *= 1.2 225 226 bui.containerwidget( 227 edit=self._root_widget, 228 size=(self._width, self._height), 229 background=False, 230 scale=root_widget_scale, 231 ) 232 233 # Version/copyright info. 234 bui.textwidget( 235 parent=self._root_widget, 236 position=(self._width * 0.5, button_y_offs - 10), 237 size=(0, 0), 238 scale=0.4, 239 flatness=1.0, 240 color=(1, 1, 1, 0.3), 241 text=( 242 f'{app.env.engine_version}' 243 f' build {app.env.engine_build_number}.' 244 f' Copyright 2024 Eric Froemling.' 245 ), 246 h_align='center', 247 v_align='center', 248 transition_delay=self._t_delay_play, 249 ) 250 251 # In kiosk mode, provide a button to get back to the kiosk menu. 252 if bui.app.env.demo or bui.app.env.arcade: 253 # h, v, scale = positions[self._p_index] 254 h = self._width * 0.5 255 v = button_y_offs 256 scale = 1.0 257 this_b_width = self._button_width * 0.4 * scale 258 demo_menu_delay = ( 259 0.0 260 if self._t_delay_play == 0.0 261 else max(0, self._t_delay_play + 0.1) 262 ) 263 self._demo_menu_button = bui.buttonwidget( 264 parent=self._root_widget, 265 position=(self._width * 0.5 - this_b_width * 0.5, v + 90), 266 size=(this_b_width, 45), 267 autoselect=True, 268 color=(0.45, 0.55, 0.45), 269 textcolor=(0.7, 0.8, 0.7), 270 label=bui.Lstr( 271 resource=( 272 'modeArcadeText' 273 if bui.app.env.arcade 274 else 'modeDemoText' 275 ) 276 ), 277 transition_delay=demo_menu_delay, 278 on_activate_call=self.main_window_back, 279 ) 280 else: 281 self._demo_menu_button = None 282 283 # Gather button 284 h = self._width * 0.5 285 h = ( 286 self._width * 0.5 287 - play_button_width * play_button_scale * 0.5 288 - hspace 289 - side_button_width * side_button_scale * 0.5 290 ) 291 v = button_y_offs + side_button_y_offs 292 self._gather_button = btn = bui.buttonwidget( 293 parent=self._root_widget, 294 position=(h - side_button_width * side_button_scale * 0.5, v), 295 size=(side_button_width, side_button_height), 296 scale=side_button_scale, 297 autoselect=self._use_autoselect, 298 button_type='square', 299 label='', 300 transition_delay=self._tdelay, 301 on_activate_call=self._gather_press, 302 ) 303 bui.textwidget( 304 parent=self._root_widget, 305 position=(h, v + side_button_height * side_button_scale * 0.25), 306 size=(0, 0), 307 scale=0.75, 308 transition_delay=self._tdelay, 309 draw_controller=btn, 310 color=(0.75, 1.0, 0.7), 311 maxwidth=side_button_width * side_button_scale * 0.8, 312 text=bui.Lstr(resource='gatherWindow.titleText'), 313 h_align='center', 314 v_align='center', 315 ) 316 icon_size = side_button_width * side_button_scale * 0.63 317 bui.imagewidget( 318 parent=self._root_widget, 319 size=(icon_size, icon_size), 320 draw_controller=btn, 321 transition_delay=self._tdelay, 322 position=( 323 h - 0.5 * icon_size, 324 v 325 + 0.65 * side_button_height * side_button_scale 326 - 0.5 * icon_size, 327 ), 328 texture=bui.gettexture('usersButton'), 329 ) 330 331 h -= ( 332 side_button_width * side_button_scale * 0.5 333 + hspace2 334 + side_button_2_width * side_button_2_scale 335 ) 336 v = button_y_offs + side_button_2_y_offs 337 338 btn = bui.buttonwidget( 339 parent=self._root_widget, 340 position=(h, v), 341 autoselect=self._use_autoselect, 342 size=(side_button_2_width, side_button_2_height * 2.0), 343 button_type='square', 344 scale=side_button_2_scale, 345 label=bui.Lstr(resource=f'{self._r}.howToPlayText'), 346 transition_delay=self._tdelay, 347 on_activate_call=self._howtoplay, 348 ) 349 self._how_to_play_button = btn 350 351 # Play button. 352 h = self._width * 0.5 353 v = button_y_offs 354 assert play_button_width is not None 355 assert play_button_height is not None 356 self._play_button = start_button = bui.buttonwidget( 357 parent=self._root_widget, 358 position=(h - play_button_width * 0.5 * play_button_scale, v), 359 size=(play_button_width, play_button_height), 360 autoselect=self._use_autoselect, 361 scale=play_button_scale, 362 text_res_scale=2.0, 363 label=bui.Lstr(resource='playText'), 364 transition_delay=self._t_delay_play, 365 on_activate_call=self._play_press, 366 ) 367 bui.containerwidget( 368 edit=self._root_widget, 369 start_button=start_button, 370 selected_child=start_button, 371 ) 372 373 self._tdelay += self._t_delay_inc 374 375 h = ( 376 self._width * 0.5 377 + play_button_width * play_button_scale * 0.5 378 + hspace 379 + side_button_width * side_button_scale * 0.5 380 ) 381 v = button_y_offs + side_button_y_offs 382 self._watch_button = btn = bui.buttonwidget( 383 parent=self._root_widget, 384 position=(h - side_button_width * side_button_scale * 0.5, v), 385 size=(side_button_width, side_button_height), 386 scale=side_button_scale, 387 autoselect=self._use_autoselect, 388 button_type='square', 389 label='', 390 transition_delay=self._tdelay, 391 on_activate_call=self._watch_press, 392 ) 393 bui.textwidget( 394 parent=self._root_widget, 395 position=(h, v + side_button_height * side_button_scale * 0.25), 396 size=(0, 0), 397 scale=0.75, 398 transition_delay=self._tdelay, 399 color=(0.75, 1.0, 0.7), 400 draw_controller=btn, 401 maxwidth=side_button_width * side_button_scale * 0.8, 402 text=bui.Lstr(resource='watchWindow.titleText'), 403 h_align='center', 404 v_align='center', 405 ) 406 icon_size = side_button_width * side_button_scale * 0.63 407 bui.imagewidget( 408 parent=self._root_widget, 409 size=(icon_size, icon_size), 410 draw_controller=btn, 411 transition_delay=self._tdelay, 412 position=( 413 h - 0.5 * icon_size, 414 v 415 + 0.65 * side_button_height * side_button_scale 416 - 0.5 * icon_size, 417 ), 418 texture=bui.gettexture('tv'), 419 ) 420 421 # Credits button. 422 self._tdelay += self._t_delay_inc 423 424 h += side_button_width * side_button_scale * 0.5 + hspace2 425 v = button_y_offs + side_button_2_y_offs 426 427 if self._have_quit_button: 428 v += 1.17 * side_button_2_height * side_button_2_scale 429 430 self._credits_button = bui.buttonwidget( 431 parent=self._root_widget, 432 position=(h, v), 433 button_type=None if self._have_quit_button else 'square', 434 size=( 435 side_button_2_width, 436 side_button_2_height * (1.0 if self._have_quit_button else 2.0), 437 ), 438 scale=side_button_2_scale, 439 autoselect=self._use_autoselect, 440 label=bui.Lstr(resource=f'{self._r}.creditsText'), 441 transition_delay=self._tdelay, 442 on_activate_call=self._credits, 443 ) 444 self._tdelay += self._t_delay_inc 445 446 self._quit_button: bui.Widget | None 447 if self._have_quit_button: 448 v -= 1.1 * side_button_2_height * side_button_2_scale 449 self._quit_button = quit_button = bui.buttonwidget( 450 parent=self._root_widget, 451 autoselect=self._use_autoselect, 452 position=(h, v), 453 size=(side_button_2_width, side_button_2_height), 454 scale=side_button_2_scale, 455 label=bui.Lstr( 456 resource=self._r 457 + ( 458 '.quitText' 459 if 'Mac' in app.classic.legacy_user_agent_string 460 else '.exitGameText' 461 ) 462 ), 463 on_activate_call=self._quit, 464 transition_delay=self._tdelay, 465 ) 466 467 bui.containerwidget( 468 edit=self._root_widget, cancel_button=quit_button 469 ) 470 self._tdelay += self._t_delay_inc 471 else: 472 self._quit_button = None 473 474 # If we're not in-game, have no quit button, and this is 475 # android, we want back presses to quit our activity. 476 if app.classic.platform == 'android': 477 478 def _do_quit() -> None: 479 bui.quit(confirm=True, quit_type=bui.QuitType.BACK) 480 481 bui.containerwidget( 482 edit=self._root_widget, on_cancel_call=_do_quit 483 ) 484 485 def _quit(self) -> None: 486 # pylint: disable=cyclic-import 487 from bauiv1lib.confirm import QuitWindow 488 489 # no-op if we're not currently in control. 490 if not self.main_window_has_control(): 491 return 492 493 # Note: Normally we should go through bui.quit(confirm=True) but 494 # invoking the window directly lets us scale it up from the 495 # button. 496 QuitWindow(origin_widget=self._quit_button) 497 498 def _credits(self) -> None: 499 # pylint: disable=cyclic-import 500 from bauiv1lib.credits import CreditsWindow 501 502 # no-op if we're not currently in control. 503 if not self.main_window_has_control(): 504 return 505 506 self.main_window_replace( 507 CreditsWindow(origin_widget=self._credits_button), 508 ) 509 510 def _howtoplay(self) -> None: 511 # pylint: disable=cyclic-import 512 from bauiv1lib.helpui import HelpWindow 513 514 # no-op if we're not currently in control. 515 if not self.main_window_has_control(): 516 return 517 518 self.main_window_replace( 519 HelpWindow(origin_widget=self._how_to_play_button), 520 ) 521 522 def _save_state(self) -> None: 523 try: 524 sel = self._root_widget.get_selected_child() 525 if sel == self._play_button: 526 sel_name = 'Start' 527 elif sel == self._gather_button: 528 sel_name = 'Gather' 529 elif sel == self._watch_button: 530 sel_name = 'Watch' 531 elif sel == self._how_to_play_button: 532 sel_name = 'HowToPlay' 533 elif sel == self._credits_button: 534 sel_name = 'Credits' 535 elif sel == self._quit_button: 536 sel_name = 'Quit' 537 elif sel == self._demo_menu_button: 538 sel_name = 'DemoMenu' 539 else: 540 print(f'Unknown widget in main menu selection: {sel}.') 541 sel_name = 'Start' 542 bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} 543 except Exception: 544 logging.exception('Error saving state for %s.', self) 545 546 def _restore_state(self) -> None: 547 try: 548 549 sel: bui.Widget | None 550 551 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 552 'sel_name' 553 ) 554 assert isinstance(sel_name, (str, type(None))) 555 if sel_name is None: 556 sel_name = 'Start' 557 if sel_name == 'HowToPlay': 558 sel = self._how_to_play_button 559 elif sel_name == 'Gather': 560 sel = self._gather_button 561 elif sel_name == 'Watch': 562 sel = self._watch_button 563 elif sel_name == 'Credits': 564 sel = self._credits_button 565 elif sel_name == 'Quit': 566 sel = self._quit_button 567 elif sel_name == 'DemoMenu': 568 sel = self._demo_menu_button 569 else: 570 sel = self._play_button 571 if sel is not None: 572 bui.containerwidget(edit=self._root_widget, selected_child=sel) 573 574 except Exception: 575 logging.exception('Error restoring state for %s.', self) 576 577 def _gather_press(self) -> None: 578 # pylint: disable=cyclic-import 579 from bauiv1lib.gather import GatherWindow 580 581 # no-op if we're not currently in control. 582 if not self.main_window_has_control(): 583 return 584 585 self.main_window_replace( 586 GatherWindow(origin_widget=self._gather_button) 587 ) 588 589 def _watch_press(self) -> None: 590 # pylint: disable=cyclic-import 591 from bauiv1lib.watch import WatchWindow 592 593 # no-op if we're not currently in control. 594 if not self.main_window_has_control(): 595 return 596 597 self.main_window_replace( 598 WatchWindow(origin_widget=self._watch_button), 599 ) 600 601 def _play_press(self) -> None: 602 # pylint: disable=cyclic-import 603 from bauiv1lib.play import PlayWindow 604 605 # no-op if we're not currently in control. 606 if not self.main_window_has_control(): 607 return 608 609 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.helpui 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.7 193 self._t_delay_inc = 0.05 194 self._t_delay_play = 1.7 195 classic.did_menu_intro = True 196 197 self._width = 400.0 198 self._height = 200.0 199 200 play_button_width = self._button_width * 0.65 201 play_button_height = self._button_height * 1.1 202 play_button_scale = 1.7 203 hspace = 20.0 204 side_button_width = self._button_width * 0.4 205 side_button_height = side_button_width 206 side_button_scale = 0.95 207 side_button_y_offs = 5.0 208 hspace2 = 15.0 209 side_button_2_width = self._button_width * 1.0 210 side_button_2_height = side_button_2_width * 0.3 211 side_button_2_y_offs = 10.0 212 side_button_2_scale = 0.5 213 214 if uiscale is bui.UIScale.SMALL: 215 root_widget_scale = 1.3 216 button_y_offs = -20.0 217 self._button_height *= 1.3 218 elif uiscale is bui.UIScale.MEDIUM: 219 root_widget_scale = 1.3 220 button_y_offs = -55.0 221 self._button_height *= 1.25 222 else: 223 root_widget_scale = 1.0 224 button_y_offs = -90.0 225 self._button_height *= 1.2 226 227 bui.containerwidget( 228 edit=self._root_widget, 229 size=(self._width, self._height), 230 background=False, 231 scale=root_widget_scale, 232 ) 233 234 # Version/copyright info. 235 bui.textwidget( 236 parent=self._root_widget, 237 position=(self._width * 0.5, button_y_offs - 10), 238 size=(0, 0), 239 scale=0.4, 240 flatness=1.0, 241 color=(1, 1, 1, 0.3), 242 text=( 243 f'{app.env.engine_version}' 244 f' build {app.env.engine_build_number}.' 245 f' Copyright 2024 Eric Froemling.' 246 ), 247 h_align='center', 248 v_align='center', 249 transition_delay=self._t_delay_play, 250 ) 251 252 # In kiosk mode, provide a button to get back to the kiosk menu. 253 if bui.app.env.demo or bui.app.env.arcade: 254 # h, v, scale = positions[self._p_index] 255 h = self._width * 0.5 256 v = button_y_offs 257 scale = 1.0 258 this_b_width = self._button_width * 0.4 * scale 259 demo_menu_delay = ( 260 0.0 261 if self._t_delay_play == 0.0 262 else max(0, self._t_delay_play + 0.1) 263 ) 264 self._demo_menu_button = bui.buttonwidget( 265 parent=self._root_widget, 266 position=(self._width * 0.5 - this_b_width * 0.5, v + 90), 267 size=(this_b_width, 45), 268 autoselect=True, 269 color=(0.45, 0.55, 0.45), 270 textcolor=(0.7, 0.8, 0.7), 271 label=bui.Lstr( 272 resource=( 273 'modeArcadeText' 274 if bui.app.env.arcade 275 else 'modeDemoText' 276 ) 277 ), 278 transition_delay=demo_menu_delay, 279 on_activate_call=self.main_window_back, 280 ) 281 else: 282 self._demo_menu_button = None 283 284 # Gather button 285 h = self._width * 0.5 286 h = ( 287 self._width * 0.5 288 - play_button_width * play_button_scale * 0.5 289 - hspace 290 - side_button_width * side_button_scale * 0.5 291 ) 292 v = button_y_offs + side_button_y_offs 293 self._gather_button = btn = bui.buttonwidget( 294 parent=self._root_widget, 295 position=(h - side_button_width * side_button_scale * 0.5, v), 296 size=(side_button_width, side_button_height), 297 scale=side_button_scale, 298 autoselect=self._use_autoselect, 299 button_type='square', 300 label='', 301 transition_delay=self._tdelay, 302 on_activate_call=self._gather_press, 303 ) 304 bui.textwidget( 305 parent=self._root_widget, 306 position=(h, v + side_button_height * side_button_scale * 0.25), 307 size=(0, 0), 308 scale=0.75, 309 transition_delay=self._tdelay, 310 draw_controller=btn, 311 color=(0.75, 1.0, 0.7), 312 maxwidth=side_button_width * side_button_scale * 0.8, 313 text=bui.Lstr(resource='gatherWindow.titleText'), 314 h_align='center', 315 v_align='center', 316 ) 317 icon_size = side_button_width * side_button_scale * 0.63 318 bui.imagewidget( 319 parent=self._root_widget, 320 size=(icon_size, icon_size), 321 draw_controller=btn, 322 transition_delay=self._tdelay, 323 position=( 324 h - 0.5 * icon_size, 325 v 326 + 0.65 * side_button_height * side_button_scale 327 - 0.5 * icon_size, 328 ), 329 texture=bui.gettexture('usersButton'), 330 ) 331 332 h -= ( 333 side_button_width * side_button_scale * 0.5 334 + hspace2 335 + side_button_2_width * side_button_2_scale 336 ) 337 v = button_y_offs + side_button_2_y_offs 338 339 btn = bui.buttonwidget( 340 parent=self._root_widget, 341 position=(h, v), 342 autoselect=self._use_autoselect, 343 size=(side_button_2_width, side_button_2_height * 2.0), 344 button_type='square', 345 scale=side_button_2_scale, 346 label=bui.Lstr(resource=f'{self._r}.howToPlayText'), 347 transition_delay=self._tdelay, 348 on_activate_call=self._howtoplay, 349 ) 350 self._how_to_play_button = btn 351 352 # Play button. 353 h = self._width * 0.5 354 v = button_y_offs 355 assert play_button_width is not None 356 assert play_button_height is not None 357 self._play_button = start_button = bui.buttonwidget( 358 parent=self._root_widget, 359 position=(h - play_button_width * 0.5 * play_button_scale, v), 360 size=(play_button_width, play_button_height), 361 autoselect=self._use_autoselect, 362 scale=play_button_scale, 363 text_res_scale=2.0, 364 label=bui.Lstr(resource='playText'), 365 transition_delay=self._t_delay_play, 366 on_activate_call=self._play_press, 367 ) 368 bui.containerwidget( 369 edit=self._root_widget, 370 start_button=start_button, 371 selected_child=start_button, 372 ) 373 374 self._tdelay += self._t_delay_inc 375 376 h = ( 377 self._width * 0.5 378 + play_button_width * play_button_scale * 0.5 379 + hspace 380 + side_button_width * side_button_scale * 0.5 381 ) 382 v = button_y_offs + side_button_y_offs 383 self._watch_button = btn = bui.buttonwidget( 384 parent=self._root_widget, 385 position=(h - side_button_width * side_button_scale * 0.5, v), 386 size=(side_button_width, side_button_height), 387 scale=side_button_scale, 388 autoselect=self._use_autoselect, 389 button_type='square', 390 label='', 391 transition_delay=self._tdelay, 392 on_activate_call=self._watch_press, 393 ) 394 bui.textwidget( 395 parent=self._root_widget, 396 position=(h, v + side_button_height * side_button_scale * 0.25), 397 size=(0, 0), 398 scale=0.75, 399 transition_delay=self._tdelay, 400 color=(0.75, 1.0, 0.7), 401 draw_controller=btn, 402 maxwidth=side_button_width * side_button_scale * 0.8, 403 text=bui.Lstr(resource='watchWindow.titleText'), 404 h_align='center', 405 v_align='center', 406 ) 407 icon_size = side_button_width * side_button_scale * 0.63 408 bui.imagewidget( 409 parent=self._root_widget, 410 size=(icon_size, icon_size), 411 draw_controller=btn, 412 transition_delay=self._tdelay, 413 position=( 414 h - 0.5 * icon_size, 415 v 416 + 0.65 * side_button_height * side_button_scale 417 - 0.5 * icon_size, 418 ), 419 texture=bui.gettexture('tv'), 420 ) 421 422 # Credits button. 423 self._tdelay += self._t_delay_inc 424 425 h += side_button_width * side_button_scale * 0.5 + hspace2 426 v = button_y_offs + side_button_2_y_offs 427 428 if self._have_quit_button: 429 v += 1.17 * side_button_2_height * side_button_2_scale 430 431 self._credits_button = bui.buttonwidget( 432 parent=self._root_widget, 433 position=(h, v), 434 button_type=None if self._have_quit_button else 'square', 435 size=( 436 side_button_2_width, 437 side_button_2_height * (1.0 if self._have_quit_button else 2.0), 438 ), 439 scale=side_button_2_scale, 440 autoselect=self._use_autoselect, 441 label=bui.Lstr(resource=f'{self._r}.creditsText'), 442 transition_delay=self._tdelay, 443 on_activate_call=self._credits, 444 ) 445 self._tdelay += self._t_delay_inc 446 447 self._quit_button: bui.Widget | None 448 if self._have_quit_button: 449 v -= 1.1 * side_button_2_height * side_button_2_scale 450 self._quit_button = quit_button = bui.buttonwidget( 451 parent=self._root_widget, 452 autoselect=self._use_autoselect, 453 position=(h, v), 454 size=(side_button_2_width, side_button_2_height), 455 scale=side_button_2_scale, 456 label=bui.Lstr( 457 resource=self._r 458 + ( 459 '.quitText' 460 if 'Mac' in app.classic.legacy_user_agent_string 461 else '.exitGameText' 462 ) 463 ), 464 on_activate_call=self._quit, 465 transition_delay=self._tdelay, 466 ) 467 468 bui.containerwidget( 469 edit=self._root_widget, cancel_button=quit_button 470 ) 471 self._tdelay += self._t_delay_inc 472 else: 473 self._quit_button = None 474 475 # If we're not in-game, have no quit button, and this is 476 # android, we want back presses to quit our activity. 477 if app.classic.platform == 'android': 478 479 def _do_quit() -> None: 480 bui.quit(confirm=True, quit_type=bui.QuitType.BACK) 481 482 bui.containerwidget( 483 edit=self._root_widget, on_cancel_call=_do_quit 484 ) 485 486 def _quit(self) -> None: 487 # pylint: disable=cyclic-import 488 from bauiv1lib.confirm import QuitWindow 489 490 # no-op if we're not currently in control. 491 if not self.main_window_has_control(): 492 return 493 494 # Note: Normally we should go through bui.quit(confirm=True) but 495 # invoking the window directly lets us scale it up from the 496 # button. 497 QuitWindow(origin_widget=self._quit_button) 498 499 def _credits(self) -> None: 500 # pylint: disable=cyclic-import 501 from bauiv1lib.credits import CreditsWindow 502 503 # no-op if we're not currently in control. 504 if not self.main_window_has_control(): 505 return 506 507 self.main_window_replace( 508 CreditsWindow(origin_widget=self._credits_button), 509 ) 510 511 def _howtoplay(self) -> None: 512 # pylint: disable=cyclic-import 513 from bauiv1lib.helpui import HelpWindow 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 HelpWindow(origin_widget=self._how_to_play_button), 521 ) 522 523 def _save_state(self) -> None: 524 try: 525 sel = self._root_widget.get_selected_child() 526 if sel == self._play_button: 527 sel_name = 'Start' 528 elif sel == self._gather_button: 529 sel_name = 'Gather' 530 elif sel == self._watch_button: 531 sel_name = 'Watch' 532 elif sel == self._how_to_play_button: 533 sel_name = 'HowToPlay' 534 elif sel == self._credits_button: 535 sel_name = 'Credits' 536 elif sel == self._quit_button: 537 sel_name = 'Quit' 538 elif sel == self._demo_menu_button: 539 sel_name = 'DemoMenu' 540 else: 541 print(f'Unknown widget in main menu selection: {sel}.') 542 sel_name = 'Start' 543 bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} 544 except Exception: 545 logging.exception('Error saving state for %s.', self) 546 547 def _restore_state(self) -> None: 548 try: 549 550 sel: bui.Widget | None 551 552 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 553 'sel_name' 554 ) 555 assert isinstance(sel_name, (str, type(None))) 556 if sel_name is None: 557 sel_name = 'Start' 558 if sel_name == 'HowToPlay': 559 sel = self._how_to_play_button 560 elif sel_name == 'Gather': 561 sel = self._gather_button 562 elif sel_name == 'Watch': 563 sel = self._watch_button 564 elif sel_name == 'Credits': 565 sel = self._credits_button 566 elif sel_name == 'Quit': 567 sel = self._quit_button 568 elif sel_name == 'DemoMenu': 569 sel = self._demo_menu_button 570 else: 571 sel = self._play_button 572 if sel is not None: 573 bui.containerwidget(edit=self._root_widget, selected_child=sel) 574 575 except Exception: 576 logging.exception('Error restoring state for %s.', self) 577 578 def _gather_press(self) -> None: 579 # pylint: disable=cyclic-import 580 from bauiv1lib.gather import GatherWindow 581 582 # no-op if we're not currently in control. 583 if not self.main_window_has_control(): 584 return 585 586 self.main_window_replace( 587 GatherWindow(origin_widget=self._gather_button) 588 ) 589 590 def _watch_press(self) -> None: 591 # pylint: disable=cyclic-import 592 from bauiv1lib.watch import WatchWindow 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 WatchWindow(origin_widget=self._watch_button), 600 ) 601 602 def _play_press(self) -> None: 603 # pylint: disable=cyclic-import 604 from bauiv1lib.play import PlayWindow 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(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.
Inherited Members
- bauiv1._uitypes.MainWindow
- main_window_back_state
- main_window_is_top_level
- main_window_close
- main_window_has_control
- main_window_back
- main_window_replace
- bauiv1._uitypes.Window
- get_root_widget