bauiv1lib.mainmenu

Implements the main menu window.

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