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# pylint: disable=too-many-lines
   5
   6from __future__ import annotations
   7
   8from typing import TYPE_CHECKING
   9import logging
  10
  11import bauiv1 as bui
  12import bascenev1 as bs
  13
  14if TYPE_CHECKING:
  15    from typing import Any, Callable
  16
  17
  18class MainMenuWindow(bui.Window):
  19    """The main menu window, both in-game and in the main menu session."""
  20
  21    def __init__(self, transition: str | None = 'in_right'):
  22        # pylint: disable=cyclic-import
  23        import threading
  24        from bascenev1lib.mainmenu import MainMenuSession
  25
  26        plus = bui.app.plus
  27        assert plus is not None
  28
  29        self._in_game = not isinstance(
  30            bs.get_foreground_host_session(),
  31            MainMenuSession,
  32        )
  33
  34        # Preload some modules we use in a background thread so we won't
  35        # have a visual hitch when the user taps them.
  36        threading.Thread(target=self._preload_modules).start()
  37
  38        if not self._in_game:
  39            bui.set_analytics_screen('Main Menu')
  40            self._show_remote_app_info_on_first_launch()
  41
  42        # Make a vanilla container; we'll modify it to our needs in refresh.
  43        super().__init__(
  44            root_widget=bui.containerwidget(
  45                transition=transition,
  46                toolbar_visibility='menu_minimal_no_back'
  47                if self._in_game
  48                else 'menu_minimal_no_back',
  49            )
  50        )
  51
  52        # Grab this stuff in case it changes.
  53        self._is_demo = bui.app.env.demo
  54        self._is_arcade = bui.app.env.arcade
  55
  56        self._tdelay = 0.0
  57        self._t_delay_inc = 0.02
  58        self._t_delay_play = 1.7
  59        self._p_index = 0
  60        self._use_autoselect = True
  61        self._button_width = 200.0
  62        self._button_height = 45.0
  63        self._width = 100.0
  64        self._height = 100.0
  65        self._demo_menu_button: bui.Widget | None = None
  66        self._gather_button: bui.Widget | None = None
  67        self._start_button: bui.Widget | None = None
  68        self._watch_button: bui.Widget | None = None
  69        self._account_button: bui.Widget | None = None
  70        self._how_to_play_button: bui.Widget | None = None
  71        self._credits_button: bui.Widget | None = None
  72        self._settings_button: bui.Widget | None = None
  73        self._next_refresh_allow_time = 0.0
  74
  75        self._store_char_tex = self._get_store_char_tex()
  76
  77        self._refresh()
  78        self._restore_state()
  79
  80        # Keep an eye on a few things and refresh if they change.
  81        self._account_state = plus.get_v1_account_state()
  82        self._account_state_num = plus.get_v1_account_state_num()
  83        self._account_type = (
  84            plus.get_v1_account_type()
  85            if self._account_state == 'signed_in'
  86            else None
  87        )
  88        self._refresh_timer = bui.AppTimer(
  89            0.27, bui.WeakCall(self._check_refresh), repeat=True
  90        )
  91
  92    # noinspection PyUnresolvedReferences
  93    @staticmethod
  94    def _preload_modules() -> None:
  95        """Preload modules we use; avoids hitches (called in bg thread)."""
  96        import bauiv1lib.getremote as _unused
  97        import bauiv1lib.confirm as _unused2
  98        import bauiv1lib.store.button as _unused3
  99        import bauiv1lib.kiosk as _unused4
 100        import bauiv1lib.account.settings as _unused5
 101        import bauiv1lib.store.browser as _unused6
 102        import bauiv1lib.creditslist as _unused7
 103        import bauiv1lib.helpui as _unused8
 104        import bauiv1lib.settings.allsettings as _unused9
 105        import bauiv1lib.gather as _unused10
 106        import bauiv1lib.watch as _unused11
 107        import bauiv1lib.play as _unused12
 108
 109    def _show_remote_app_info_on_first_launch(self) -> None:
 110        app = bui.app
 111        assert app.classic is not None
 112        # The first time the non-in-game menu pops up, we might wanna show
 113        # a 'get-remote-app' dialog in front of it.
 114        if app.classic.first_main_menu:
 115            app.classic.first_main_menu = False
 116            try:
 117                force_test = False
 118                bs.get_local_active_input_devices_count()
 119                if (
 120                    (app.env.tv or app.classic.platform == 'mac')
 121                    and bui.app.config.get('launchCount', 0) <= 1
 122                ) or force_test:
 123
 124                    def _check_show_bs_remote_window() -> None:
 125                        try:
 126                            from bauiv1lib.getremote import GetBSRemoteWindow
 127
 128                            bui.getsound('swish').play()
 129                            GetBSRemoteWindow()
 130                        except Exception:
 131                            logging.exception(
 132                                'Error showing get-remote window.'
 133                            )
 134
 135                    bui.apptimer(2.5, _check_show_bs_remote_window)
 136            except Exception:
 137                logging.exception('Error showing get-remote-app info.')
 138
 139    def _get_store_char_tex(self) -> str:
 140        plus = bui.app.plus
 141        assert plus is not None
 142        return (
 143            'storeCharacterXmas'
 144            if plus.get_v1_account_misc_read_val('xmas', False)
 145            else 'storeCharacterEaster'
 146            if plus.get_v1_account_misc_read_val('easter', False)
 147            else 'storeCharacter'
 148        )
 149
 150    def _check_refresh(self) -> None:
 151        plus = bui.app.plus
 152        assert plus is not None
 153
 154        if not self._root_widget:
 155            return
 156
 157        now = bui.apptime()
 158        if now < self._next_refresh_allow_time:
 159            return
 160
 161        # Don't refresh for the first few seconds the game is up so we don't
 162        # interrupt the transition in.
 163        # bui.app.main_menu_window_refresh_check_count += 1
 164        # if bui.app.main_menu_window_refresh_check_count < 4:
 165        #     return
 166
 167        store_char_tex = self._get_store_char_tex()
 168        account_state_num = plus.get_v1_account_state_num()
 169        if (
 170            account_state_num != self._account_state_num
 171            or store_char_tex != self._store_char_tex
 172        ):
 173            self._store_char_tex = store_char_tex
 174            self._account_state_num = account_state_num
 175            account_state = self._account_state = plus.get_v1_account_state()
 176            self._account_type = (
 177                plus.get_v1_account_type()
 178                if account_state == 'signed_in'
 179                else None
 180            )
 181            self._save_state()
 182            self._refresh()
 183            self._restore_state()
 184
 185    def get_play_button(self) -> bui.Widget | None:
 186        """Return the play button."""
 187        return self._start_button
 188
 189    def _refresh(self) -> None:
 190        # pylint: disable=too-many-branches
 191        # pylint: disable=too-many-locals
 192        # pylint: disable=too-many-statements
 193        from bauiv1lib.store.button import StoreButton
 194
 195        plus = bui.app.plus
 196        assert plus is not None
 197
 198        # Clear everything that was there.
 199        children = self._root_widget.get_children()
 200        for child in children:
 201            child.delete()
 202
 203        self._tdelay = 0.0
 204        self._t_delay_inc = 0.0
 205        self._t_delay_play = 0.0
 206        self._button_width = 200.0
 207        self._button_height = 45.0
 208
 209        self._r = 'mainMenu'
 210
 211        app = bui.app
 212        assert app.classic is not None
 213        self._have_quit_button = app.ui_v1.uiscale is bui.UIScale.LARGE or (
 214            app.classic.platform == 'windows'
 215            and app.classic.subplatform == 'oculus'
 216        )
 217
 218        self._have_store_button = not self._in_game
 219
 220        self._have_settings_button = (
 221            not self._in_game or not app.ui_v1.use_toolbars
 222        ) and not (self._is_demo or self._is_arcade)
 223
 224        self._input_device = input_device = bs.get_ui_input_device()
 225
 226        # Are we connected to a local player?
 227        self._input_player = input_device.player if input_device else None
 228
 229        # Are we connected to a remote player?.
 230        self._connected_to_remote_player = (
 231            input_device.is_attached_to_player()
 232            if (input_device and self._input_player is None)
 233            else False
 234        )
 235
 236        positions: list[tuple[float, float, float]] = []
 237        self._p_index = 0
 238
 239        if self._in_game:
 240            h, v, scale = self._refresh_in_game(positions)
 241        else:
 242            h, v, scale = self._refresh_not_in_game(positions)
 243
 244        if self._have_settings_button:
 245            h, v, scale = positions[self._p_index]
 246            self._p_index += 1
 247            self._settings_button = bui.buttonwidget(
 248                parent=self._root_widget,
 249                position=(h - self._button_width * 0.5 * scale, v),
 250                size=(self._button_width, self._button_height),
 251                scale=scale,
 252                autoselect=self._use_autoselect,
 253                label=bui.Lstr(resource=self._r + '.settingsText'),
 254                transition_delay=self._tdelay,
 255                on_activate_call=self._settings,
 256            )
 257
 258        # Scattered eggs on easter.
 259        if (
 260            plus.get_v1_account_misc_read_val('easter', False)
 261            and not self._in_game
 262        ):
 263            icon_size = 34
 264            bui.imagewidget(
 265                parent=self._root_widget,
 266                position=(
 267                    h - icon_size * 0.5 - 15,
 268                    v + self._button_height * scale - icon_size * 0.24 + 1.5,
 269                ),
 270                transition_delay=self._tdelay,
 271                size=(icon_size, icon_size),
 272                texture=bui.gettexture('egg3'),
 273                tilt_scale=0.0,
 274            )
 275
 276        self._tdelay += self._t_delay_inc
 277
 278        if self._in_game:
 279            h, v, scale = positions[self._p_index]
 280            self._p_index += 1
 281
 282            # If we're in a replay, we have a 'Leave Replay' button.
 283            if bs.is_in_replay():
 284                bui.buttonwidget(
 285                    parent=self._root_widget,
 286                    position=(h - self._button_width * 0.5 * scale, v),
 287                    scale=scale,
 288                    size=(self._button_width, self._button_height),
 289                    autoselect=self._use_autoselect,
 290                    label=bui.Lstr(resource='replayEndText'),
 291                    on_activate_call=self._confirm_end_replay,
 292                )
 293            elif bs.get_foreground_host_session() is not None:
 294                bui.buttonwidget(
 295                    parent=self._root_widget,
 296                    position=(h - self._button_width * 0.5 * scale, v),
 297                    scale=scale,
 298                    size=(self._button_width, self._button_height),
 299                    autoselect=self._use_autoselect,
 300                    label=bui.Lstr(
 301                        resource=self._r
 302                        + (
 303                            '.endTestText'
 304                            if self._is_benchmark()
 305                            else '.endGameText'
 306                        )
 307                    ),
 308                    on_activate_call=(
 309                        self._confirm_end_test
 310                        if self._is_benchmark()
 311                        else self._confirm_end_game
 312                    ),
 313                )
 314            else:
 315                # Assume we're in a client-session.
 316                bui.buttonwidget(
 317                    parent=self._root_widget,
 318                    position=(h - self._button_width * 0.5 * scale, v),
 319                    scale=scale,
 320                    size=(self._button_width, self._button_height),
 321                    autoselect=self._use_autoselect,
 322                    label=bui.Lstr(resource=self._r + '.leavePartyText'),
 323                    on_activate_call=self._confirm_leave_party,
 324                )
 325
 326        self._store_button: bui.Widget | None
 327        if self._have_store_button:
 328            this_b_width = self._button_width
 329            h, v, scale = positions[self._p_index]
 330            self._p_index += 1
 331
 332            sbtn = self._store_button_instance = StoreButton(
 333                parent=self._root_widget,
 334                position=(h - this_b_width * 0.5 * scale, v),
 335                size=(this_b_width, self._button_height),
 336                scale=scale,
 337                on_activate_call=bui.WeakCall(self._on_store_pressed),
 338                sale_scale=1.3,
 339                transition_delay=self._tdelay,
 340            )
 341            self._store_button = store_button = sbtn.get_button()
 342            assert bui.app.classic is not None
 343            uiscale = bui.app.ui_v1.uiscale
 344            icon_size = (
 345                55
 346                if uiscale is bui.UIScale.SMALL
 347                else 55
 348                if uiscale is bui.UIScale.MEDIUM
 349                else 70
 350            )
 351            bui.imagewidget(
 352                parent=self._root_widget,
 353                position=(
 354                    h - icon_size * 0.5,
 355                    v + self._button_height * scale - icon_size * 0.23,
 356                ),
 357                transition_delay=self._tdelay,
 358                size=(icon_size, icon_size),
 359                texture=bui.gettexture(self._store_char_tex),
 360                tilt_scale=0.0,
 361                draw_controller=store_button,
 362            )
 363            self._tdelay += self._t_delay_inc
 364        else:
 365            self._store_button = None
 366
 367        self._quit_button: bui.Widget | None
 368        if not self._in_game and self._have_quit_button:
 369            h, v, scale = positions[self._p_index]
 370            self._p_index += 1
 371            self._quit_button = quit_button = bui.buttonwidget(
 372                parent=self._root_widget,
 373                autoselect=self._use_autoselect,
 374                position=(h - self._button_width * 0.5 * scale, v),
 375                size=(self._button_width, self._button_height),
 376                scale=scale,
 377                label=bui.Lstr(
 378                    resource=self._r
 379                    + (
 380                        '.quitText'
 381                        if 'Mac' in app.classic.legacy_user_agent_string
 382                        else '.exitGameText'
 383                    )
 384                ),
 385                on_activate_call=self._quit,
 386                transition_delay=self._tdelay,
 387            )
 388
 389            # Scattered eggs on easter.
 390            if plus.get_v1_account_misc_read_val('easter', False):
 391                icon_size = 30
 392                bui.imagewidget(
 393                    parent=self._root_widget,
 394                    position=(
 395                        h - icon_size * 0.5 + 25,
 396                        v
 397                        + self._button_height * scale
 398                        - icon_size * 0.24
 399                        + 1.5,
 400                    ),
 401                    transition_delay=self._tdelay,
 402                    size=(icon_size, icon_size),
 403                    texture=bui.gettexture('egg1'),
 404                    tilt_scale=0.0,
 405                )
 406
 407            bui.containerwidget(
 408                edit=self._root_widget, cancel_button=quit_button
 409            )
 410            self._tdelay += self._t_delay_inc
 411        else:
 412            self._quit_button = None
 413
 414            # If we're not in-game, have no quit button, and this is android,
 415            # we want back presses to quit our activity.
 416            if (
 417                not self._in_game
 418                and not self._have_quit_button
 419                and app.classic.platform == 'android'
 420            ):
 421
 422                def _do_quit() -> None:
 423                    bui.quit(confirm=True, quit_type=bui.QuitType.BACK)
 424
 425                bui.containerwidget(
 426                    edit=self._root_widget, on_cancel_call=_do_quit
 427                )
 428
 429        # Add speed-up/slow-down buttons for replays.
 430        # (ideally this should be part of a fading-out playback bar like most
 431        # media players but this works for now).
 432        if bs.is_in_replay():
 433            b_size = 50.0
 434            b_buffer_1 = 50.0
 435            b_buffer_2 = 10.0
 436            t_scale = 0.75
 437            assert bui.app.classic is not None
 438            uiscale = bui.app.ui_v1.uiscale
 439            if uiscale is bui.UIScale.SMALL:
 440                b_size *= 0.6
 441                b_buffer_1 *= 0.8
 442                b_buffer_2 *= 1.0
 443                v_offs = -40
 444                t_scale = 0.5
 445            elif uiscale is bui.UIScale.MEDIUM:
 446                v_offs = -70
 447            else:
 448                v_offs = -100
 449            self._replay_speed_text = bui.textwidget(
 450                parent=self._root_widget,
 451                text=bui.Lstr(
 452                    resource='watchWindow.playbackSpeedText',
 453                    subs=[('${SPEED}', str(1.23))],
 454                ),
 455                position=(h, v + v_offs + 7 * t_scale),
 456                h_align='center',
 457                v_align='center',
 458                size=(0, 0),
 459                scale=t_scale,
 460            )
 461
 462            # Update to current value.
 463            self._change_replay_speed(0)
 464
 465            # Keep updating in a timer in case it gets changed elsewhere.
 466            self._change_replay_speed_timer = bui.AppTimer(
 467                0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True
 468            )
 469            btn = bui.buttonwidget(
 470                parent=self._root_widget,
 471                position=(
 472                    h - b_size - b_buffer_1,
 473                    v - b_size - b_buffer_2 + v_offs,
 474                ),
 475                button_type='square',
 476                size=(b_size, b_size),
 477                label='',
 478                autoselect=True,
 479                on_activate_call=bui.Call(self._change_replay_speed, -1),
 480            )
 481            bui.textwidget(
 482                parent=self._root_widget,
 483                draw_controller=btn,
 484                text='-',
 485                position=(
 486                    h - b_size * 0.5 - b_buffer_1,
 487                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
 488                ),
 489                h_align='center',
 490                v_align='center',
 491                size=(0, 0),
 492                scale=3.0 * t_scale,
 493            )
 494            btn = bui.buttonwidget(
 495                parent=self._root_widget,
 496                position=(h + b_buffer_1, v - b_size - b_buffer_2 + v_offs),
 497                button_type='square',
 498                size=(b_size, b_size),
 499                label='',
 500                autoselect=True,
 501                on_activate_call=bui.Call(self._change_replay_speed, 1),
 502            )
 503            bui.textwidget(
 504                parent=self._root_widget,
 505                draw_controller=btn,
 506                text='+',
 507                position=(
 508                    h + b_size * 0.5 + b_buffer_1,
 509                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
 510                ),
 511                h_align='center',
 512                v_align='center',
 513                size=(0, 0),
 514                scale=3.0 * t_scale,
 515            )
 516            self._pause_resume_button = btn = bui.buttonwidget(
 517                parent=self._root_widget,
 518                position=(h - b_size * 0.5, v - b_size - b_buffer_2 + v_offs),
 519                button_type='square',
 520                size=(b_size, b_size),
 521                label=bui.charstr(
 522                    bui.SpecialChar.PLAY_BUTTON
 523                    if bs.is_replay_paused()
 524                    else bui.SpecialChar.PAUSE_BUTTON
 525                ),
 526                autoselect=True,
 527                on_activate_call=bui.Call(self._pause_or_resume_replay),
 528            )
 529
 530    def _refresh_not_in_game(
 531        self, positions: list[tuple[float, float, float]]
 532    ) -> tuple[float, float, float]:
 533        # pylint: disable=too-many-branches
 534        # pylint: disable=too-many-locals
 535        # pylint: disable=too-many-statements
 536        plus = bui.app.plus
 537        assert plus is not None
 538
 539        assert bui.app.classic is not None
 540        if not bui.app.classic.did_menu_intro:
 541            self._tdelay = 2.0
 542            self._t_delay_inc = 0.02
 543            self._t_delay_play = 1.7
 544
 545            def _set_allow_time() -> None:
 546                self._next_refresh_allow_time = bui.apptime() + 2.5
 547
 548            # Slight hack: widget transitions currently only progress when
 549            # frames are being drawn, but this tends to get called before
 550            # frame drawing even starts, meaning we don't know exactly how
 551            # long we should wait before refreshing to avoid interrupting
 552            # the transition. To make things a bit better, let's do a
 553            # redundant set of the time in a deferred call which hopefully
 554            # happens closer to actual frame draw times.
 555            _set_allow_time()
 556            bui.pushcall(_set_allow_time)
 557
 558            bui.app.classic.did_menu_intro = True
 559        self._width = 400.0
 560        self._height = 200.0
 561        enable_account_button = True
 562        account_type_name: str | bui.Lstr
 563        if plus.get_v1_account_state() == 'signed_in':
 564            account_type_name = plus.get_v1_account_display_string()
 565            account_type_icon = None
 566            account_textcolor = (1.0, 1.0, 1.0)
 567        else:
 568            account_type_name = bui.Lstr(
 569                resource='notSignedInText',
 570                fallback_resource='accountSettingsWindow.titleText',
 571            )
 572            account_type_icon = None
 573            account_textcolor = (1.0, 0.2, 0.2)
 574        account_type_icon_color = (1.0, 1.0, 1.0)
 575        account_type_call = self._show_account_window
 576        account_type_enable_button_sound = True
 577        b_count = 3  # play, help, credits
 578        if self._have_settings_button:
 579            b_count += 1
 580        if enable_account_button:
 581            b_count += 1
 582        if self._have_quit_button:
 583            b_count += 1
 584        if self._have_store_button:
 585            b_count += 1
 586        uiscale = bui.app.ui_v1.uiscale
 587        if uiscale is bui.UIScale.SMALL:
 588            root_widget_scale = 1.6
 589            play_button_width = self._button_width * 0.65
 590            play_button_height = self._button_height * 1.1
 591            small_button_scale = 0.51 if b_count > 6 else 0.63
 592            button_y_offs = -20.0
 593            button_y_offs2 = -60.0
 594            self._button_height *= 1.3
 595            button_spacing = 1.04
 596        elif uiscale is bui.UIScale.MEDIUM:
 597            root_widget_scale = 1.3
 598            play_button_width = self._button_width * 0.65
 599            play_button_height = self._button_height * 1.1
 600            small_button_scale = 0.6
 601            button_y_offs = -55.0
 602            button_y_offs2 = -75.0
 603            self._button_height *= 1.25
 604            button_spacing = 1.1
 605        else:
 606            root_widget_scale = 1.0
 607            play_button_width = self._button_width * 0.65
 608            play_button_height = self._button_height * 1.1
 609            small_button_scale = 0.75
 610            button_y_offs = -80.0
 611            button_y_offs2 = -100.0
 612            self._button_height *= 1.2
 613            button_spacing = 1.1
 614        spc = self._button_width * small_button_scale * button_spacing
 615        bui.containerwidget(
 616            edit=self._root_widget,
 617            size=(self._width, self._height),
 618            background=False,
 619            scale=root_widget_scale,
 620        )
 621        assert not positions
 622        positions.append((self._width * 0.5, button_y_offs, 1.7))
 623        x_offs = self._width * 0.5 - (spc * (b_count - 1) * 0.5) + (spc * 0.5)
 624        for i in range(b_count - 1):
 625            positions.append(
 626                (
 627                    x_offs + spc * i - 1.0,
 628                    button_y_offs + button_y_offs2,
 629                    small_button_scale,
 630                )
 631            )
 632        # In kiosk mode, provide a button to get back to the kiosk menu.
 633        if bui.app.env.demo or bui.app.env.arcade:
 634            h, v, scale = positions[self._p_index]
 635            this_b_width = self._button_width * 0.4 * scale
 636            demo_menu_delay = (
 637                0.0
 638                if self._t_delay_play == 0.0
 639                else max(0, self._t_delay_play + 0.1)
 640            )
 641            self._demo_menu_button = bui.buttonwidget(
 642                parent=self._root_widget,
 643                position=(self._width * 0.5 - this_b_width * 0.5, v + 90),
 644                size=(this_b_width, 45),
 645                autoselect=True,
 646                color=(0.45, 0.55, 0.45),
 647                textcolor=(0.7, 0.8, 0.7),
 648                label=bui.Lstr(
 649                    resource='modeArcadeText'
 650                    if bui.app.env.arcade
 651                    else 'modeDemoText'
 652                ),
 653                transition_delay=demo_menu_delay,
 654                on_activate_call=self._demo_menu_press,
 655            )
 656        else:
 657            self._demo_menu_button = None
 658        uiscale = bui.app.ui_v1.uiscale
 659        foof = (
 660            -1
 661            if uiscale is bui.UIScale.SMALL
 662            else 1
 663            if uiscale is bui.UIScale.MEDIUM
 664            else 3
 665        )
 666        h, v, scale = positions[self._p_index]
 667        v = v + foof
 668        gather_delay = (
 669            0.0
 670            if self._t_delay_play == 0.0
 671            else max(0.0, self._t_delay_play + 0.1)
 672        )
 673        assert play_button_width is not None
 674        assert play_button_height is not None
 675        this_h = h - play_button_width * 0.5 * scale - 40 * scale
 676        this_b_width = self._button_width * 0.25 * scale
 677        this_b_height = self._button_height * 0.82 * scale
 678        self._gather_button = btn = bui.buttonwidget(
 679            parent=self._root_widget,
 680            position=(this_h - this_b_width * 0.5, v),
 681            size=(this_b_width, this_b_height),
 682            autoselect=self._use_autoselect,
 683            button_type='square',
 684            label='',
 685            transition_delay=gather_delay,
 686            on_activate_call=self._gather_press,
 687        )
 688        bui.textwidget(
 689            parent=self._root_widget,
 690            position=(this_h, v + self._button_height * 0.33),
 691            size=(0, 0),
 692            scale=0.75,
 693            transition_delay=gather_delay,
 694            draw_controller=btn,
 695            color=(0.75, 1.0, 0.7),
 696            maxwidth=self._button_width * 0.33,
 697            text=bui.Lstr(resource='gatherWindow.titleText'),
 698            h_align='center',
 699            v_align='center',
 700        )
 701        icon_size = this_b_width * 0.6
 702        bui.imagewidget(
 703            parent=self._root_widget,
 704            size=(icon_size, icon_size),
 705            draw_controller=btn,
 706            transition_delay=gather_delay,
 707            position=(this_h - 0.5 * icon_size, v + 0.31 * this_b_height),
 708            texture=bui.gettexture('usersButton'),
 709        )
 710
 711        # Play button.
 712        h, v, scale = positions[self._p_index]
 713        self._p_index += 1
 714        self._start_button = start_button = bui.buttonwidget(
 715            parent=self._root_widget,
 716            position=(h - play_button_width * 0.5 * scale, v),
 717            size=(play_button_width, play_button_height),
 718            autoselect=self._use_autoselect,
 719            scale=scale,
 720            text_res_scale=2.0,
 721            label=bui.Lstr(resource='playText'),
 722            transition_delay=self._t_delay_play,
 723            on_activate_call=self._play_press,
 724        )
 725        bui.containerwidget(
 726            edit=self._root_widget,
 727            start_button=start_button,
 728            selected_child=start_button,
 729        )
 730        v = v + foof
 731        watch_delay = (
 732            0.0
 733            if self._t_delay_play == 0.0
 734            else max(0.0, self._t_delay_play - 0.1)
 735        )
 736        this_h = h + play_button_width * 0.5 * scale + 40 * scale
 737        this_b_width = self._button_width * 0.25 * scale
 738        this_b_height = self._button_height * 0.82 * scale
 739        self._watch_button = btn = bui.buttonwidget(
 740            parent=self._root_widget,
 741            position=(this_h - this_b_width * 0.5, v),
 742            size=(this_b_width, this_b_height),
 743            autoselect=self._use_autoselect,
 744            button_type='square',
 745            label='',
 746            transition_delay=watch_delay,
 747            on_activate_call=self._watch_press,
 748        )
 749        bui.textwidget(
 750            parent=self._root_widget,
 751            position=(this_h, v + self._button_height * 0.33),
 752            size=(0, 0),
 753            scale=0.75,
 754            transition_delay=watch_delay,
 755            color=(0.75, 1.0, 0.7),
 756            draw_controller=btn,
 757            maxwidth=self._button_width * 0.33,
 758            text=bui.Lstr(resource='watchWindow.titleText'),
 759            h_align='center',
 760            v_align='center',
 761        )
 762        icon_size = this_b_width * 0.55
 763        bui.imagewidget(
 764            parent=self._root_widget,
 765            size=(icon_size, icon_size),
 766            draw_controller=btn,
 767            transition_delay=watch_delay,
 768            position=(this_h - 0.5 * icon_size, v + 0.33 * this_b_height),
 769            texture=bui.gettexture('tv'),
 770        )
 771        if not self._in_game and enable_account_button:
 772            this_b_width = self._button_width
 773            h, v, scale = positions[self._p_index]
 774            self._p_index += 1
 775            self._account_button = bui.buttonwidget(
 776                parent=self._root_widget,
 777                position=(h - this_b_width * 0.5 * scale, v),
 778                size=(this_b_width, self._button_height),
 779                scale=scale,
 780                label=account_type_name,
 781                autoselect=self._use_autoselect,
 782                on_activate_call=account_type_call,
 783                textcolor=account_textcolor,
 784                icon=account_type_icon,
 785                icon_color=account_type_icon_color,
 786                transition_delay=self._tdelay,
 787                enable_sound=account_type_enable_button_sound,
 788            )
 789
 790            # Scattered eggs on easter.
 791            if (
 792                plus.get_v1_account_misc_read_val('easter', False)
 793                and not self._in_game
 794            ):
 795                icon_size = 32
 796                bui.imagewidget(
 797                    parent=self._root_widget,
 798                    position=(
 799                        h - icon_size * 0.5 + 35,
 800                        v
 801                        + self._button_height * scale
 802                        - icon_size * 0.24
 803                        + 1.5,
 804                    ),
 805                    transition_delay=self._tdelay,
 806                    size=(icon_size, icon_size),
 807                    texture=bui.gettexture('egg2'),
 808                    tilt_scale=0.0,
 809                )
 810            self._tdelay += self._t_delay_inc
 811        else:
 812            self._account_button = None
 813
 814        # How-to-play button.
 815        h, v, scale = positions[self._p_index]
 816        self._p_index += 1
 817        btn = bui.buttonwidget(
 818            parent=self._root_widget,
 819            position=(h - self._button_width * 0.5 * scale, v),
 820            scale=scale,
 821            autoselect=self._use_autoselect,
 822            size=(self._button_width, self._button_height),
 823            label=bui.Lstr(resource=self._r + '.howToPlayText'),
 824            transition_delay=self._tdelay,
 825            on_activate_call=self._howtoplay,
 826        )
 827        self._how_to_play_button = btn
 828
 829        # Scattered eggs on easter.
 830        if (
 831            plus.get_v1_account_misc_read_val('easter', False)
 832            and not self._in_game
 833        ):
 834            icon_size = 28
 835            bui.imagewidget(
 836                parent=self._root_widget,
 837                position=(
 838                    h - icon_size * 0.5 + 30,
 839                    v + self._button_height * scale - icon_size * 0.24 + 1.5,
 840                ),
 841                transition_delay=self._tdelay,
 842                size=(icon_size, icon_size),
 843                texture=bui.gettexture('egg4'),
 844                tilt_scale=0.0,
 845            )
 846        # Credits button.
 847        self._tdelay += self._t_delay_inc
 848        h, v, scale = positions[self._p_index]
 849        self._p_index += 1
 850        self._credits_button = bui.buttonwidget(
 851            parent=self._root_widget,
 852            position=(h - self._button_width * 0.5 * scale, v),
 853            size=(self._button_width, self._button_height),
 854            autoselect=self._use_autoselect,
 855            label=bui.Lstr(resource=self._r + '.creditsText'),
 856            scale=scale,
 857            transition_delay=self._tdelay,
 858            on_activate_call=self._credits,
 859        )
 860        self._tdelay += self._t_delay_inc
 861        return h, v, scale
 862
 863    def _refresh_in_game(
 864        self, positions: list[tuple[float, float, float]]
 865    ) -> tuple[float, float, float]:
 866        # pylint: disable=too-many-branches
 867        # pylint: disable=too-many-locals
 868        # pylint: disable=too-many-statements
 869        assert bui.app.classic is not None
 870        custom_menu_entries: list[dict[str, Any]] = []
 871        session = bs.get_foreground_host_session()
 872        if session is not None:
 873            try:
 874                custom_menu_entries = session.get_custom_menu_entries()
 875                for cme in custom_menu_entries:
 876                    cme_any: Any = cme  # Type check may not hold true.
 877                    if (
 878                        not isinstance(cme_any, dict)
 879                        or 'label' not in cme
 880                        or not isinstance(cme['label'], (str, bui.Lstr))
 881                        or 'call' not in cme
 882                        or not callable(cme['call'])
 883                    ):
 884                        raise ValueError(
 885                            'invalid custom menu entry: ' + str(cme)
 886                        )
 887            except Exception:
 888                custom_menu_entries = []
 889                logging.exception(
 890                    'Error getting custom menu entries for %s.', session
 891                )
 892        self._width = 250.0
 893        self._height = 250.0 if self._input_player else 180.0
 894        if (self._is_demo or self._is_arcade) and self._input_player:
 895            self._height -= 40
 896        if not self._have_settings_button:
 897            self._height -= 50
 898        if self._connected_to_remote_player:
 899            # In this case we have a leave *and* a disconnect button.
 900            self._height += 50
 901        self._height += 50 * (len(custom_menu_entries))
 902        uiscale = bui.app.ui_v1.uiscale
 903        bui.containerwidget(
 904            edit=self._root_widget,
 905            size=(self._width, self._height),
 906            scale=(
 907                2.15
 908                if uiscale is bui.UIScale.SMALL
 909                else 1.6
 910                if uiscale is bui.UIScale.MEDIUM
 911                else 1.0
 912            ),
 913        )
 914        h = 125.0
 915        v = self._height - 80.0 if self._input_player else self._height - 60
 916        h_offset = 0
 917        d_h_offset = 0
 918        v_offset = -50
 919        for _i in range(6 + len(custom_menu_entries)):
 920            positions.append((h, v, 1.0))
 921            v += v_offset
 922            h += h_offset
 923            h_offset += d_h_offset
 924        self._start_button = None
 925        bui.app.classic.pause()
 926
 927        # Player name if applicable.
 928        if self._input_player:
 929            player_name = self._input_player.getname()
 930            h, v, scale = positions[self._p_index]
 931            v += 35
 932            bui.textwidget(
 933                parent=self._root_widget,
 934                position=(h - self._button_width / 2, v),
 935                size=(self._button_width, self._button_height),
 936                color=(1, 1, 1, 0.5),
 937                scale=0.7,
 938                h_align='center',
 939                text=bui.Lstr(value=player_name),
 940            )
 941        else:
 942            player_name = ''
 943        h, v, scale = positions[self._p_index]
 944        self._p_index += 1
 945        btn = bui.buttonwidget(
 946            parent=self._root_widget,
 947            position=(h - self._button_width / 2, v),
 948            size=(self._button_width, self._button_height),
 949            scale=scale,
 950            label=bui.Lstr(resource=self._r + '.resumeText'),
 951            autoselect=self._use_autoselect,
 952            on_activate_call=self._resume,
 953        )
 954        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 955
 956        # Add any custom options defined by the current game.
 957        for entry in custom_menu_entries:
 958            h, v, scale = positions[self._p_index]
 959            self._p_index += 1
 960
 961            # Ask the entry whether we should resume when we call
 962            # it (defaults to true).
 963            resume = bool(entry.get('resume_on_call', True))
 964
 965            if resume:
 966                call = bui.Call(self._resume_and_call, entry['call'])
 967            else:
 968                call = bui.Call(entry['call'], bui.WeakCall(self._resume))
 969
 970            bui.buttonwidget(
 971                parent=self._root_widget,
 972                position=(h - self._button_width / 2, v),
 973                size=(self._button_width, self._button_height),
 974                scale=scale,
 975                on_activate_call=call,
 976                label=entry['label'],
 977                autoselect=self._use_autoselect,
 978            )
 979        # Add a 'leave' button if the menu-owner has a player.
 980        if (self._input_player or self._connected_to_remote_player) and not (
 981            self._is_demo or self._is_arcade
 982        ):
 983            h, v, scale = positions[self._p_index]
 984            self._p_index += 1
 985            btn = bui.buttonwidget(
 986                parent=self._root_widget,
 987                position=(h - self._button_width / 2, v),
 988                size=(self._button_width, self._button_height),
 989                scale=scale,
 990                on_activate_call=self._leave,
 991                label='',
 992                autoselect=self._use_autoselect,
 993            )
 994
 995            if (
 996                player_name != ''
 997                and player_name[0] != '<'
 998                and player_name[-1] != '>'
 999            ):
1000                txt = bui.Lstr(
1001                    resource=self._r + '.justPlayerText',
1002                    subs=[('${NAME}', player_name)],
1003                )
1004            else:
1005                txt = bui.Lstr(value=player_name)
1006            bui.textwidget(
1007                parent=self._root_widget,
1008                position=(
1009                    h,
1010                    v
1011                    + self._button_height
1012                    * (0.64 if player_name != '' else 0.5),
1013                ),
1014                size=(0, 0),
1015                text=bui.Lstr(resource=self._r + '.leaveGameText'),
1016                scale=(0.83 if player_name != '' else 1.0),
1017                color=(0.75, 1.0, 0.7),
1018                h_align='center',
1019                v_align='center',
1020                draw_controller=btn,
1021                maxwidth=self._button_width * 0.9,
1022            )
1023            bui.textwidget(
1024                parent=self._root_widget,
1025                position=(h, v + self._button_height * 0.27),
1026                size=(0, 0),
1027                text=txt,
1028                color=(0.75, 1.0, 0.7),
1029                h_align='center',
1030                v_align='center',
1031                draw_controller=btn,
1032                scale=0.45,
1033                maxwidth=self._button_width * 0.9,
1034            )
1035        return h, v, scale
1036
1037    def _change_replay_speed(self, offs: int) -> None:
1038        if not self._replay_speed_text:
1039            if bui.do_once():
1040                print('_change_replay_speed called without widget')
1041            return
1042        bs.set_replay_speed_exponent(bs.get_replay_speed_exponent() + offs)
1043        actual_speed = pow(2.0, bs.get_replay_speed_exponent())
1044        bui.textwidget(
1045            edit=self._replay_speed_text,
1046            text=bui.Lstr(
1047                resource='watchWindow.playbackSpeedText',
1048                subs=[('${SPEED}', str(actual_speed))],
1049            ),
1050        )
1051
1052    def _pause_or_resume_replay(self) -> None:
1053        if bs.is_replay_paused():
1054            bs.resume_replay()
1055            bui.buttonwidget(
1056                edit=self._pause_resume_button,
1057                label=bui.charstr(bui.SpecialChar.PAUSE_BUTTON),
1058            )
1059        else:
1060            bs.pause_replay()
1061            bui.buttonwidget(
1062                edit=self._pause_resume_button,
1063                label=bui.charstr(bui.SpecialChar.PLAY_BUTTON),
1064            )
1065
1066    def _quit(self) -> None:
1067        # pylint: disable=cyclic-import
1068        from bauiv1lib.confirm import QuitWindow
1069
1070        # no-op if our underlying widget is dead or on its way out.
1071        if not self._root_widget or self._root_widget.transitioning_out:
1072            return
1073
1074        # Note: Normally we should go through bui.quit(confirm=True) but
1075        # invoking the window directly lets us scale it up from the
1076        # button.
1077        QuitWindow(origin_widget=self._quit_button)
1078
1079    def _demo_menu_press(self) -> None:
1080        # pylint: disable=cyclic-import
1081        from bauiv1lib.kiosk import KioskWindow
1082
1083        # no-op if our underlying widget is dead or on its way out.
1084        if not self._root_widget or self._root_widget.transitioning_out:
1085            return
1086
1087        self._save_state()
1088        bui.containerwidget(edit=self._root_widget, transition='out_right')
1089        assert bui.app.classic is not None
1090        bui.app.ui_v1.set_main_menu_window(
1091            KioskWindow(transition='in_left').get_root_widget(),
1092            from_window=self._root_widget,
1093        )
1094
1095    def _show_account_window(self) -> None:
1096        # pylint: disable=cyclic-import
1097        from bauiv1lib.account.settings import AccountSettingsWindow
1098
1099        # no-op if our underlying widget is dead or on its way out.
1100        if not self._root_widget or self._root_widget.transitioning_out:
1101            return
1102
1103        self._save_state()
1104        bui.containerwidget(edit=self._root_widget, transition='out_left')
1105        assert bui.app.classic is not None
1106        bui.app.ui_v1.set_main_menu_window(
1107            AccountSettingsWindow(
1108                origin_widget=self._account_button
1109            ).get_root_widget(),
1110            from_window=self._root_widget,
1111        )
1112
1113    def _on_store_pressed(self) -> None:
1114        # pylint: disable=cyclic-import
1115        from bauiv1lib.store.browser import StoreBrowserWindow
1116        from bauiv1lib.account import show_sign_in_prompt
1117
1118        # no-op if our underlying widget is dead or on its way out.
1119        if not self._root_widget or self._root_widget.transitioning_out:
1120            return
1121
1122        plus = bui.app.plus
1123        assert plus is not None
1124
1125        if plus.get_v1_account_state() != 'signed_in':
1126            show_sign_in_prompt()
1127            return
1128        self._save_state()
1129        bui.containerwidget(edit=self._root_widget, transition='out_left')
1130        assert bui.app.classic is not None
1131        bui.app.ui_v1.set_main_menu_window(
1132            StoreBrowserWindow(
1133                origin_widget=self._store_button
1134            ).get_root_widget(),
1135            from_window=self._root_widget,
1136        )
1137
1138    def _is_benchmark(self) -> bool:
1139        session = bs.get_foreground_host_session()
1140        return getattr(session, 'benchmark_type', None) == 'cpu' or (
1141            bui.app.classic is not None
1142            and bui.app.classic.stress_test_update_timer is not None
1143        )
1144
1145    def _confirm_end_game(self) -> None:
1146        # pylint: disable=cyclic-import
1147        from bauiv1lib.confirm import ConfirmWindow
1148
1149        # FIXME: Currently we crash calling this on client-sessions.
1150
1151        # Select cancel by default; this occasionally gets called by accident
1152        # in a fit of button mashing and this will help reduce damage.
1153        ConfirmWindow(
1154            bui.Lstr(resource=self._r + '.exitToMenuText'),
1155            self._end_game,
1156            cancel_is_selected=True,
1157        )
1158
1159    def _confirm_end_test(self) -> None:
1160        # pylint: disable=cyclic-import
1161        from bauiv1lib.confirm import ConfirmWindow
1162
1163        # Select cancel by default; this occasionally gets called by accident
1164        # in a fit of button mashing and this will help reduce damage.
1165        ConfirmWindow(
1166            bui.Lstr(resource=self._r + '.exitToMenuText'),
1167            self._end_game,
1168            cancel_is_selected=True,
1169        )
1170
1171    def _confirm_end_replay(self) -> None:
1172        # pylint: disable=cyclic-import
1173        from bauiv1lib.confirm import ConfirmWindow
1174
1175        # Select cancel by default; this occasionally gets called by accident
1176        # in a fit of button mashing and this will help reduce damage.
1177        ConfirmWindow(
1178            bui.Lstr(resource=self._r + '.exitToMenuText'),
1179            self._end_game,
1180            cancel_is_selected=True,
1181        )
1182
1183    def _confirm_leave_party(self) -> None:
1184        # pylint: disable=cyclic-import
1185        from bauiv1lib.confirm import ConfirmWindow
1186
1187        # Select cancel by default; this occasionally gets called by accident
1188        # in a fit of button mashing and this will help reduce damage.
1189        ConfirmWindow(
1190            bui.Lstr(resource=self._r + '.leavePartyConfirmText'),
1191            self._leave_party,
1192            cancel_is_selected=True,
1193        )
1194
1195    def _leave_party(self) -> None:
1196        bs.disconnect_from_host()
1197
1198    def _end_game(self) -> None:
1199        assert bui.app.classic is not None
1200
1201        # no-op if our underlying widget is dead or on its way out.
1202        if not self._root_widget or self._root_widget.transitioning_out:
1203            return
1204
1205        bui.containerwidget(edit=self._root_widget, transition='out_left')
1206        bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False)
1207
1208    def _leave(self) -> None:
1209        if self._input_player:
1210            self._input_player.remove_from_game()
1211        elif self._connected_to_remote_player:
1212            if self._input_device:
1213                self._input_device.detach_from_player()
1214        self._resume()
1215
1216    def _credits(self) -> None:
1217        # pylint: disable=cyclic-import
1218        from bauiv1lib.creditslist import CreditsListWindow
1219
1220        # no-op if our underlying widget is dead or on its way out.
1221        if not self._root_widget or self._root_widget.transitioning_out:
1222            return
1223
1224        self._save_state()
1225        bui.containerwidget(edit=self._root_widget, transition='out_left')
1226        assert bui.app.classic is not None
1227        bui.app.ui_v1.set_main_menu_window(
1228            CreditsListWindow(
1229                origin_widget=self._credits_button
1230            ).get_root_widget(),
1231            from_window=self._root_widget,
1232        )
1233
1234    def _howtoplay(self) -> None:
1235        # pylint: disable=cyclic-import
1236        from bauiv1lib.helpui import HelpWindow
1237
1238        # no-op if our underlying widget is dead or on its way out.
1239        if not self._root_widget or self._root_widget.transitioning_out:
1240            return
1241
1242        self._save_state()
1243        bui.containerwidget(edit=self._root_widget, transition='out_left')
1244        assert bui.app.classic is not None
1245        bui.app.ui_v1.set_main_menu_window(
1246            HelpWindow(
1247                main_menu=True, origin_widget=self._how_to_play_button
1248            ).get_root_widget(),
1249            from_window=self._root_widget,
1250        )
1251
1252    def _settings(self) -> None:
1253        # pylint: disable=cyclic-import
1254        from bauiv1lib.settings.allsettings import AllSettingsWindow
1255
1256        # no-op if our underlying widget is dead or on its way out.
1257        if not self._root_widget or self._root_widget.transitioning_out:
1258            return
1259
1260        self._save_state()
1261        bui.containerwidget(edit=self._root_widget, transition='out_left')
1262        assert bui.app.classic is not None
1263        bui.app.ui_v1.set_main_menu_window(
1264            AllSettingsWindow(
1265                origin_widget=self._settings_button
1266            ).get_root_widget(),
1267            from_window=self._root_widget,
1268        )
1269
1270    def _resume_and_call(self, call: Callable[[], Any]) -> None:
1271        self._resume()
1272        call()
1273
1274    def _do_game_service_press(self) -> None:
1275        self._save_state()
1276        if bui.app.plus is not None:
1277            bui.app.plus.show_game_service_ui()
1278        else:
1279            logging.warning(
1280                'plus feature-set is required to show game service ui'
1281            )
1282
1283    def _save_state(self) -> None:
1284        # Don't do this for the in-game menu.
1285        if self._in_game:
1286            return
1287        assert bui.app.classic is not None
1288        ui = bui.app.ui_v1
1289        sel = self._root_widget.get_selected_child()
1290        if sel == self._start_button:
1291            ui.main_menu_selection = 'Start'
1292        elif sel == self._gather_button:
1293            ui.main_menu_selection = 'Gather'
1294        elif sel == self._watch_button:
1295            ui.main_menu_selection = 'Watch'
1296        elif sel == self._how_to_play_button:
1297            ui.main_menu_selection = 'HowToPlay'
1298        elif sel == self._credits_button:
1299            ui.main_menu_selection = 'Credits'
1300        elif sel == self._settings_button:
1301            ui.main_menu_selection = 'Settings'
1302        elif sel == self._account_button:
1303            ui.main_menu_selection = 'Account'
1304        elif sel == self._store_button:
1305            ui.main_menu_selection = 'Store'
1306        elif sel == self._quit_button:
1307            ui.main_menu_selection = 'Quit'
1308        elif sel == self._demo_menu_button:
1309            ui.main_menu_selection = 'DemoMenu'
1310        else:
1311            print('unknown widget in main menu store selection:', sel)
1312            ui.main_menu_selection = 'Start'
1313
1314    def _restore_state(self) -> None:
1315        # pylint: disable=too-many-branches
1316
1317        # Don't do this for the in-game menu.
1318        if self._in_game:
1319            return
1320        assert bui.app.classic is not None
1321        sel_name = bui.app.ui_v1.main_menu_selection
1322        sel: bui.Widget | None
1323        if sel_name is None:
1324            sel_name = 'Start'
1325        if sel_name == 'HowToPlay':
1326            sel = self._how_to_play_button
1327        elif sel_name == 'Gather':
1328            sel = self._gather_button
1329        elif sel_name == 'Watch':
1330            sel = self._watch_button
1331        elif sel_name == 'Credits':
1332            sel = self._credits_button
1333        elif sel_name == 'Settings':
1334            sel = self._settings_button
1335        elif sel_name == 'Account':
1336            sel = self._account_button
1337        elif sel_name == 'Store':
1338            sel = self._store_button
1339        elif sel_name == 'Quit':
1340            sel = self._quit_button
1341        elif sel_name == 'DemoMenu':
1342            sel = self._demo_menu_button
1343        else:
1344            sel = self._start_button
1345        if sel is not None:
1346            bui.containerwidget(edit=self._root_widget, selected_child=sel)
1347
1348    def _gather_press(self) -> None:
1349        # pylint: disable=cyclic-import
1350        from bauiv1lib.gather import GatherWindow
1351
1352        # no-op if our underlying widget is dead or on its way out.
1353        if not self._root_widget or self._root_widget.transitioning_out:
1354            return
1355
1356        self._save_state()
1357        bui.containerwidget(edit=self._root_widget, transition='out_left')
1358        assert bui.app.classic is not None
1359        bui.app.ui_v1.set_main_menu_window(
1360            GatherWindow(origin_widget=self._gather_button).get_root_widget(),
1361            from_window=self._root_widget,
1362        )
1363
1364    def _watch_press(self) -> None:
1365        # pylint: disable=cyclic-import
1366        from bauiv1lib.watch import WatchWindow
1367
1368        # no-op if our underlying widget is dead or on its way out.
1369        if not self._root_widget or self._root_widget.transitioning_out:
1370            return
1371
1372        self._save_state()
1373        bui.containerwidget(edit=self._root_widget, transition='out_left')
1374        assert bui.app.classic is not None
1375        bui.app.ui_v1.set_main_menu_window(
1376            WatchWindow(origin_widget=self._watch_button).get_root_widget(),
1377            from_window=self._root_widget,
1378        )
1379
1380    def _play_press(self) -> None:
1381        # pylint: disable=cyclic-import
1382        from bauiv1lib.play import PlayWindow
1383
1384        # no-op if our underlying widget is dead or on its way out.
1385        if not self._root_widget or self._root_widget.transitioning_out:
1386            return
1387
1388        self._save_state()
1389        bui.containerwidget(edit=self._root_widget, transition='out_left')
1390
1391        assert bui.app.classic is not None
1392        bui.app.ui_v1.selecting_private_party_playlist = False
1393        bui.app.ui_v1.set_main_menu_window(
1394            PlayWindow(origin_widget=self._start_button).get_root_widget(),
1395            from_window=self._root_widget,
1396        )
1397
1398    def _resume(self) -> None:
1399        assert bui.app.classic is not None
1400        bui.app.classic.resume()
1401        if self._root_widget:
1402            bui.containerwidget(edit=self._root_widget, transition='out_right')
1403        bui.app.ui_v1.clear_main_menu_window(transition='out_right')
1404
1405        # If there's callbacks waiting for this window to go away, call them.
1406        for call in bui.app.ui_v1.main_menu_resume_callbacks:
1407            call()
1408        del bui.app.ui_v1.main_menu_resume_callbacks[:]