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