bauiv1lib.mainmenu

Implements the main menu window.

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