bauiv1lib.ingamemenu

Implements the in-gmae menu window.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Implements the in-gmae 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 InGameMenuWindow(bui.MainWindow):
 18    """The menu that can be invoked while in a game."""
 19
 20    def __init__(
 21        self,
 22        transition: str | None = 'in_right',
 23        origin_widget: bui.Widget | None = None,
 24    ):
 25
 26        # Make a vanilla container; we'll modify it to our needs in
 27        # refresh.
 28        super().__init__(
 29            root_widget=bui.containerwidget(
 30                toolbar_visibility=('menu_in_game')
 31            ),
 32            transition=transition,
 33            origin_widget=origin_widget,
 34        )
 35
 36        # Grab this stuff in case it changes.
 37        self._is_demo = bui.app.env.demo
 38        self._is_arcade = bui.app.env.arcade
 39
 40        self._p_index = 0
 41        self._use_autoselect = True
 42        self._button_width = 200.0
 43        self._button_height = 45.0
 44        self._width = 100.0
 45        self._height = 100.0
 46
 47        self._refresh()
 48
 49    @override
 50    def get_main_window_state(self) -> bui.MainWindowState:
 51        # Support recreating our window for back/refresh purposes.
 52        return self.do_get_main_window_state()
 53
 54    @classmethod
 55    def do_get_main_window_state(cls) -> bui.MainWindowState:
 56        """Classmethod to gen a windowstate for the main menu."""
 57        return bui.BasicMainWindowState(
 58            create_call=lambda transition, origin_widget: cls(
 59                transition=transition, origin_widget=origin_widget
 60            )
 61        )
 62
 63    def _refresh(self) -> None:
 64
 65        # Clear everything that was there.
 66        children = self._root_widget.get_children()
 67        for child in children:
 68            child.delete()
 69
 70        self._r = 'mainMenu'
 71
 72        self._input_device = input_device = bs.get_ui_input_device()
 73
 74        # Are we connected to a local player?
 75        self._input_player = input_device.player if input_device else None
 76
 77        # Are we connected to a remote player?.
 78        self._connected_to_remote_player = (
 79            input_device.is_attached_to_player()
 80            if (input_device and self._input_player is None)
 81            else False
 82        )
 83
 84        positions: list[tuple[float, float, float]] = []
 85        self._p_index = 0
 86
 87        self._refresh_in_game(positions)
 88
 89        h, v, scale = positions[self._p_index]
 90        self._p_index += 1
 91
 92        # If we're in a replay, we have a 'Leave Replay' button.
 93        if bs.is_in_replay():
 94            bui.buttonwidget(
 95                parent=self._root_widget,
 96                position=(h - self._button_width * 0.5 * scale, v),
 97                scale=scale,
 98                size=(self._button_width, self._button_height),
 99                autoselect=self._use_autoselect,
100                label=bui.Lstr(resource='replayEndText'),
101                on_activate_call=self._confirm_end_replay,
102            )
103        elif bs.get_foreground_host_session() is not None:
104            bui.buttonwidget(
105                parent=self._root_widget,
106                position=(h - self._button_width * 0.5 * scale, v),
107                scale=scale,
108                size=(self._button_width, self._button_height),
109                autoselect=self._use_autoselect,
110                label=bui.Lstr(
111                    resource=self._r
112                    + (
113                        '.endTestText'
114                        if self._is_benchmark()
115                        else '.endGameText'
116                    )
117                ),
118                on_activate_call=(
119                    self._confirm_end_test
120                    if self._is_benchmark()
121                    else self._confirm_end_game
122                ),
123            )
124        else:
125            # Assume we're in a client-session.
126            bui.buttonwidget(
127                parent=self._root_widget,
128                position=(h - self._button_width * 0.5 * scale, v),
129                scale=scale,
130                size=(self._button_width, self._button_height),
131                autoselect=self._use_autoselect,
132                label=bui.Lstr(resource=f'{self._r}.leavePartyText'),
133                on_activate_call=self._confirm_leave_party,
134            )
135
136        # Add speed-up/slow-down buttons for replays. Ideally this
137        # should be part of a fading-out playback bar like most media
138        # players but this works for now.
139        if bs.is_in_replay():
140            b_size = 50.0
141            b_buffer_1 = 50.0
142            b_buffer_2 = 10.0
143            t_scale = 0.75
144            assert bui.app.classic is not None
145            uiscale = bui.app.ui_v1.uiscale
146            if uiscale is bui.UIScale.SMALL:
147                b_size *= 0.6
148                b_buffer_1 *= 0.8
149                b_buffer_2 *= 1.0
150                v_offs = -40
151                t_scale = 0.5
152            elif uiscale is bui.UIScale.MEDIUM:
153                v_offs = -70
154            else:
155                v_offs = -100
156            self._replay_speed_text = bui.textwidget(
157                parent=self._root_widget,
158                text=bui.Lstr(
159                    resource='watchWindow.playbackSpeedText',
160                    subs=[('${SPEED}', str(1.23))],
161                ),
162                position=(h, v + v_offs + 15 * t_scale),
163                h_align='center',
164                v_align='center',
165                size=(0, 0),
166                scale=t_scale,
167            )
168
169            # Update to current value.
170            self._change_replay_speed(0)
171
172            # Keep updating in a timer in case it gets changed elsewhere.
173            self._change_replay_speed_timer = bui.AppTimer(
174                0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True
175            )
176            btn = bui.buttonwidget(
177                parent=self._root_widget,
178                position=(
179                    h - b_size - b_buffer_1,
180                    v - b_size - b_buffer_2 + v_offs,
181                ),
182                button_type='square',
183                size=(b_size, b_size),
184                label='',
185                autoselect=True,
186                on_activate_call=bui.Call(self._change_replay_speed, -1),
187            )
188            bui.textwidget(
189                parent=self._root_widget,
190                draw_controller=btn,
191                text='-',
192                position=(
193                    h - b_size * 0.5 - b_buffer_1,
194                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
195                ),
196                h_align='center',
197                v_align='center',
198                size=(0, 0),
199                scale=3.0 * t_scale,
200            )
201            btn = bui.buttonwidget(
202                parent=self._root_widget,
203                position=(h + b_buffer_1, v - b_size - b_buffer_2 + v_offs),
204                button_type='square',
205                size=(b_size, b_size),
206                label='',
207                autoselect=True,
208                on_activate_call=bui.Call(self._change_replay_speed, 1),
209            )
210            bui.textwidget(
211                parent=self._root_widget,
212                draw_controller=btn,
213                text='+',
214                position=(
215                    h + b_size * 0.5 + b_buffer_1,
216                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
217                ),
218                h_align='center',
219                v_align='center',
220                size=(0, 0),
221                scale=3.0 * t_scale,
222            )
223            self._pause_resume_button = btn = bui.buttonwidget(
224                parent=self._root_widget,
225                position=(h - b_size * 0.5, v - b_size - b_buffer_2 + v_offs),
226                button_type='square',
227                size=(b_size, b_size),
228                label=bui.charstr(
229                    bui.SpecialChar.PLAY_BUTTON
230                    if bs.is_replay_paused()
231                    else bui.SpecialChar.PAUSE_BUTTON
232                ),
233                autoselect=True,
234                on_activate_call=bui.Call(self._pause_or_resume_replay),
235            )
236            btn = bui.buttonwidget(
237                parent=self._root_widget,
238                position=(
239                    h - b_size * 1.5 - b_buffer_1 * 2,
240                    v - b_size - b_buffer_2 + v_offs,
241                ),
242                button_type='square',
243                size=(b_size, b_size),
244                label='',
245                autoselect=True,
246                on_activate_call=bui.WeakCall(self._rewind_replay),
247            )
248            bui.textwidget(
249                parent=self._root_widget,
250                draw_controller=btn,
251                # text='<<',
252                text=bui.charstr(bui.SpecialChar.REWIND_BUTTON),
253                position=(
254                    h - b_size - b_buffer_1 * 2,
255                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
256                ),
257                h_align='center',
258                v_align='center',
259                size=(0, 0),
260                scale=2.0 * t_scale,
261            )
262            btn = bui.buttonwidget(
263                parent=self._root_widget,
264                position=(
265                    h + b_size * 0.5 + b_buffer_1 * 2,
266                    v - b_size - b_buffer_2 + v_offs,
267                ),
268                button_type='square',
269                size=(b_size, b_size),
270                label='',
271                autoselect=True,
272                on_activate_call=bui.WeakCall(self._forward_replay),
273            )
274            bui.textwidget(
275                parent=self._root_widget,
276                draw_controller=btn,
277                # text='>>',
278                text=bui.charstr(bui.SpecialChar.FAST_FORWARD_BUTTON),
279                position=(
280                    h + b_size + b_buffer_1 * 2,
281                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
282                ),
283                h_align='center',
284                v_align='center',
285                size=(0, 0),
286                scale=2.0 * t_scale,
287            )
288
289    def _rewind_replay(self) -> None:
290        bs.seek_replay(-2 * pow(2, bs.get_replay_speed_exponent()))
291
292    def _forward_replay(self) -> None:
293        bs.seek_replay(2 * pow(2, bs.get_replay_speed_exponent()))
294
295    def _refresh_in_game(
296        self, positions: list[tuple[float, float, float]]
297    ) -> tuple[float, float, float]:
298        # pylint: disable=too-many-branches
299        # pylint: disable=too-many-locals
300        # pylint: disable=too-many-statements
301        assert bui.app.classic is not None
302        custom_menu_entries: list[dict[str, Any]] = []
303        session = bs.get_foreground_host_session()
304        if session is not None:
305            try:
306                custom_menu_entries = session.get_custom_menu_entries()
307                for cme in custom_menu_entries:
308                    cme_any: Any = cme  # Type check may not hold true.
309                    if (
310                        not isinstance(cme_any, dict)
311                        or 'label' not in cme
312                        or not isinstance(cme['label'], (str, bui.Lstr))
313                        or 'call' not in cme
314                        or not callable(cme['call'])
315                    ):
316                        raise ValueError(
317                            'invalid custom menu entry: ' + str(cme)
318                        )
319            except Exception:
320                custom_menu_entries = []
321                logging.exception(
322                    'Error getting custom menu entries for %s.', session
323                )
324        self._width = 250.0
325        self._height = 250.0 if self._input_player else 180.0
326        if (self._is_demo or self._is_arcade) and self._input_player:
327            self._height -= 40
328        # if not self._have_settings_button:
329        self._height -= 50
330        if self._connected_to_remote_player:
331            # In this case we have a leave *and* a disconnect button.
332            self._height += 50
333        self._height += 50 * (len(custom_menu_entries))
334        uiscale = bui.app.ui_v1.uiscale
335        bui.containerwidget(
336            edit=self._root_widget,
337            size=(self._width, self._height),
338            scale=(
339                2.15
340                if uiscale is bui.UIScale.SMALL
341                else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0
342            ),
343        )
344        h = 125.0
345        v = self._height - 80.0 if self._input_player else self._height - 60
346        h_offset = 0
347        d_h_offset = 0
348        v_offset = -50
349        for _i in range(6 + len(custom_menu_entries)):
350            positions.append((h, v, 1.0))
351            v += v_offset
352            h += h_offset
353            h_offset += d_h_offset
354        # self._play_button = None
355        bui.app.classic.pause()
356
357        # Player name if applicable.
358        if self._input_player:
359            player_name = self._input_player.getname()
360            h, v, scale = positions[self._p_index]
361            v += 35
362            bui.textwidget(
363                parent=self._root_widget,
364                position=(h - self._button_width / 2, v),
365                size=(self._button_width, self._button_height),
366                color=(1, 1, 1, 0.5),
367                scale=0.7,
368                h_align='center',
369                text=bui.Lstr(value=player_name),
370            )
371        else:
372            player_name = ''
373        h, v, scale = positions[self._p_index]
374        self._p_index += 1
375        btn = bui.buttonwidget(
376            parent=self._root_widget,
377            position=(h - self._button_width / 2, v),
378            size=(self._button_width, self._button_height),
379            scale=scale,
380            label=bui.Lstr(resource=f'{self._r}.resumeText'),
381            autoselect=self._use_autoselect,
382            on_activate_call=self._resume,
383        )
384        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
385
386        # Add any custom options defined by the current game.
387        for entry in custom_menu_entries:
388            h, v, scale = positions[self._p_index]
389            self._p_index += 1
390
391            # Ask the entry whether we should resume when we call
392            # it (defaults to true).
393            resume = bool(entry.get('resume_on_call', True))
394
395            if resume:
396                call = bui.Call(self._resume_and_call, entry['call'])
397            else:
398                call = bui.Call(entry['call'], bui.WeakCall(self._resume))
399
400            bui.buttonwidget(
401                parent=self._root_widget,
402                position=(h - self._button_width / 2, v),
403                size=(self._button_width, self._button_height),
404                scale=scale,
405                on_activate_call=call,
406                label=entry['label'],
407                autoselect=self._use_autoselect,
408            )
409
410        # Add a 'leave' button if the menu-owner has a player.
411        if (self._input_player or self._connected_to_remote_player) and not (
412            self._is_demo or self._is_arcade
413        ):
414            h, v, scale = positions[self._p_index]
415            self._p_index += 1
416            btn = bui.buttonwidget(
417                parent=self._root_widget,
418                position=(h - self._button_width / 2, v),
419                size=(self._button_width, self._button_height),
420                scale=scale,
421                on_activate_call=self._leave,
422                label='',
423                autoselect=self._use_autoselect,
424            )
425
426            if (
427                player_name != ''
428                and player_name[0] != '<'
429                and player_name[-1] != '>'
430            ):
431                txt = bui.Lstr(
432                    resource=f'{self._r}.justPlayerText',
433                    subs=[('${NAME}', player_name)],
434                )
435            else:
436                txt = bui.Lstr(value=player_name)
437            bui.textwidget(
438                parent=self._root_widget,
439                position=(
440                    h,
441                    v
442                    + self._button_height
443                    * (0.64 if player_name != '' else 0.5),
444                ),
445                size=(0, 0),
446                text=bui.Lstr(resource=f'{self._r}.leaveGameText'),
447                scale=(0.83 if player_name != '' else 1.0),
448                color=(0.75, 1.0, 0.7),
449                h_align='center',
450                v_align='center',
451                draw_controller=btn,
452                maxwidth=self._button_width * 0.9,
453            )
454            bui.textwidget(
455                parent=self._root_widget,
456                position=(h, v + self._button_height * 0.27),
457                size=(0, 0),
458                text=txt,
459                color=(0.75, 1.0, 0.7),
460                h_align='center',
461                v_align='center',
462                draw_controller=btn,
463                scale=0.45,
464                maxwidth=self._button_width * 0.9,
465            )
466        return h, v, scale
467
468    def _change_replay_speed(self, offs: int) -> None:
469        if not self._replay_speed_text:
470            if bui.do_once():
471                print('_change_replay_speed called without widget')
472            return
473        bs.set_replay_speed_exponent(bs.get_replay_speed_exponent() + offs)
474        actual_speed = pow(2.0, bs.get_replay_speed_exponent())
475        bui.textwidget(
476            edit=self._replay_speed_text,
477            text=bui.Lstr(
478                resource='watchWindow.playbackSpeedText',
479                subs=[('${SPEED}', str(actual_speed))],
480            ),
481        )
482
483    def _pause_or_resume_replay(self) -> None:
484        if bs.is_replay_paused():
485            bs.resume_replay()
486            bui.buttonwidget(
487                edit=self._pause_resume_button,
488                label=bui.charstr(bui.SpecialChar.PAUSE_BUTTON),
489            )
490        else:
491            bs.pause_replay()
492            bui.buttonwidget(
493                edit=self._pause_resume_button,
494                label=bui.charstr(bui.SpecialChar.PLAY_BUTTON),
495            )
496
497    def _is_benchmark(self) -> bool:
498        session = bs.get_foreground_host_session()
499        return getattr(session, 'benchmark_type', None) == 'cpu' or (
500            bui.app.classic is not None
501            and bui.app.classic.stress_test_update_timer is not None
502        )
503
504    def _confirm_end_game(self) -> None:
505        # pylint: disable=cyclic-import
506        from bauiv1lib.confirm import ConfirmWindow
507
508        # FIXME: Currently we crash calling this on client-sessions.
509
510        # Select cancel by default; this occasionally gets called by accident
511        # in a fit of button mashing and this will help reduce damage.
512        ConfirmWindow(
513            bui.Lstr(resource=f'{self._r}.exitToMenuText'),
514            self._end_game,
515            cancel_is_selected=True,
516        )
517
518    def _confirm_end_test(self) -> None:
519        # pylint: disable=cyclic-import
520        from bauiv1lib.confirm import ConfirmWindow
521
522        # Select cancel by default; this occasionally gets called by accident
523        # in a fit of button mashing and this will help reduce damage.
524        ConfirmWindow(
525            bui.Lstr(resource=f'{self._r}.exitToMenuText'),
526            self._end_game,
527            cancel_is_selected=True,
528        )
529
530    def _confirm_end_replay(self) -> None:
531        # pylint: disable=cyclic-import
532        from bauiv1lib.confirm import ConfirmWindow
533
534        # Select cancel by default; this occasionally gets called by accident
535        # in a fit of button mashing and this will help reduce damage.
536        ConfirmWindow(
537            bui.Lstr(resource=f'{self._r}.exitToMenuText'),
538            self._end_game,
539            cancel_is_selected=True,
540        )
541
542    def _confirm_leave_party(self) -> None:
543        # pylint: disable=cyclic-import
544        from bauiv1lib.confirm import ConfirmWindow
545
546        # Select cancel by default; this occasionally gets called by accident
547        # in a fit of button mashing and this will help reduce damage.
548        ConfirmWindow(
549            bui.Lstr(resource=f'{self._r}.leavePartyConfirmText'),
550            self._leave_party,
551            cancel_is_selected=True,
552        )
553
554    def _leave_party(self) -> None:
555        bs.disconnect_from_host()
556
557    def _end_game(self) -> None:
558        assert bui.app.classic is not None
559
560        # no-op if our underlying widget is dead or on its way out.
561        if not self._root_widget or self._root_widget.transitioning_out:
562            return
563
564        bui.containerwidget(edit=self._root_widget, transition='out_left')
565        bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False)
566
567    def _leave(self) -> None:
568        if self._input_player:
569            self._input_player.remove_from_game()
570        elif self._connected_to_remote_player:
571            if self._input_device:
572                self._input_device.detach_from_player()
573        self._resume()
574
575    def _resume_and_call(self, call: Callable[[], Any]) -> None:
576        self._resume()
577        call()
578
579    def _resume(self) -> None:
580        classic = bui.app.classic
581
582        assert classic is not None
583        classic.resume()
584
585        bui.app.ui_v1.clear_main_window()
586
587        # If there's callbacks waiting for us to resume, call them.
588        for call in classic.main_menu_resume_callbacks:
589            try:
590                call()
591            except Exception:
592                logging.exception('Error in classic resume callback.')
593
594        classic.main_menu_resume_callbacks.clear()
595
596    # def __del__(self) -> None:
597    #     self._resume()
class InGameMenuWindow(bauiv1._uitypes.MainWindow):
 18class InGameMenuWindow(bui.MainWindow):
 19    """The menu that can be invoked while in a game."""
 20
 21    def __init__(
 22        self,
 23        transition: str | None = 'in_right',
 24        origin_widget: bui.Widget | None = None,
 25    ):
 26
 27        # Make a vanilla container; we'll modify it to our needs in
 28        # refresh.
 29        super().__init__(
 30            root_widget=bui.containerwidget(
 31                toolbar_visibility=('menu_in_game')
 32            ),
 33            transition=transition,
 34            origin_widget=origin_widget,
 35        )
 36
 37        # Grab this stuff in case it changes.
 38        self._is_demo = bui.app.env.demo
 39        self._is_arcade = bui.app.env.arcade
 40
 41        self._p_index = 0
 42        self._use_autoselect = True
 43        self._button_width = 200.0
 44        self._button_height = 45.0
 45        self._width = 100.0
 46        self._height = 100.0
 47
 48        self._refresh()
 49
 50    @override
 51    def get_main_window_state(self) -> bui.MainWindowState:
 52        # Support recreating our window for back/refresh purposes.
 53        return self.do_get_main_window_state()
 54
 55    @classmethod
 56    def do_get_main_window_state(cls) -> bui.MainWindowState:
 57        """Classmethod to gen a windowstate for the main menu."""
 58        return bui.BasicMainWindowState(
 59            create_call=lambda transition, origin_widget: cls(
 60                transition=transition, origin_widget=origin_widget
 61            )
 62        )
 63
 64    def _refresh(self) -> None:
 65
 66        # Clear everything that was there.
 67        children = self._root_widget.get_children()
 68        for child in children:
 69            child.delete()
 70
 71        self._r = 'mainMenu'
 72
 73        self._input_device = input_device = bs.get_ui_input_device()
 74
 75        # Are we connected to a local player?
 76        self._input_player = input_device.player if input_device else None
 77
 78        # Are we connected to a remote player?.
 79        self._connected_to_remote_player = (
 80            input_device.is_attached_to_player()
 81            if (input_device and self._input_player is None)
 82            else False
 83        )
 84
 85        positions: list[tuple[float, float, float]] = []
 86        self._p_index = 0
 87
 88        self._refresh_in_game(positions)
 89
 90        h, v, scale = positions[self._p_index]
 91        self._p_index += 1
 92
 93        # If we're in a replay, we have a 'Leave Replay' button.
 94        if bs.is_in_replay():
 95            bui.buttonwidget(
 96                parent=self._root_widget,
 97                position=(h - self._button_width * 0.5 * scale, v),
 98                scale=scale,
 99                size=(self._button_width, self._button_height),
100                autoselect=self._use_autoselect,
101                label=bui.Lstr(resource='replayEndText'),
102                on_activate_call=self._confirm_end_replay,
103            )
104        elif bs.get_foreground_host_session() is not None:
105            bui.buttonwidget(
106                parent=self._root_widget,
107                position=(h - self._button_width * 0.5 * scale, v),
108                scale=scale,
109                size=(self._button_width, self._button_height),
110                autoselect=self._use_autoselect,
111                label=bui.Lstr(
112                    resource=self._r
113                    + (
114                        '.endTestText'
115                        if self._is_benchmark()
116                        else '.endGameText'
117                    )
118                ),
119                on_activate_call=(
120                    self._confirm_end_test
121                    if self._is_benchmark()
122                    else self._confirm_end_game
123                ),
124            )
125        else:
126            # Assume we're in a client-session.
127            bui.buttonwidget(
128                parent=self._root_widget,
129                position=(h - self._button_width * 0.5 * scale, v),
130                scale=scale,
131                size=(self._button_width, self._button_height),
132                autoselect=self._use_autoselect,
133                label=bui.Lstr(resource=f'{self._r}.leavePartyText'),
134                on_activate_call=self._confirm_leave_party,
135            )
136
137        # Add speed-up/slow-down buttons for replays. Ideally this
138        # should be part of a fading-out playback bar like most media
139        # players but this works for now.
140        if bs.is_in_replay():
141            b_size = 50.0
142            b_buffer_1 = 50.0
143            b_buffer_2 = 10.0
144            t_scale = 0.75
145            assert bui.app.classic is not None
146            uiscale = bui.app.ui_v1.uiscale
147            if uiscale is bui.UIScale.SMALL:
148                b_size *= 0.6
149                b_buffer_1 *= 0.8
150                b_buffer_2 *= 1.0
151                v_offs = -40
152                t_scale = 0.5
153            elif uiscale is bui.UIScale.MEDIUM:
154                v_offs = -70
155            else:
156                v_offs = -100
157            self._replay_speed_text = bui.textwidget(
158                parent=self._root_widget,
159                text=bui.Lstr(
160                    resource='watchWindow.playbackSpeedText',
161                    subs=[('${SPEED}', str(1.23))],
162                ),
163                position=(h, v + v_offs + 15 * t_scale),
164                h_align='center',
165                v_align='center',
166                size=(0, 0),
167                scale=t_scale,
168            )
169
170            # Update to current value.
171            self._change_replay_speed(0)
172
173            # Keep updating in a timer in case it gets changed elsewhere.
174            self._change_replay_speed_timer = bui.AppTimer(
175                0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True
176            )
177            btn = bui.buttonwidget(
178                parent=self._root_widget,
179                position=(
180                    h - b_size - b_buffer_1,
181                    v - b_size - b_buffer_2 + v_offs,
182                ),
183                button_type='square',
184                size=(b_size, b_size),
185                label='',
186                autoselect=True,
187                on_activate_call=bui.Call(self._change_replay_speed, -1),
188            )
189            bui.textwidget(
190                parent=self._root_widget,
191                draw_controller=btn,
192                text='-',
193                position=(
194                    h - b_size * 0.5 - b_buffer_1,
195                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
196                ),
197                h_align='center',
198                v_align='center',
199                size=(0, 0),
200                scale=3.0 * t_scale,
201            )
202            btn = bui.buttonwidget(
203                parent=self._root_widget,
204                position=(h + b_buffer_1, v - b_size - b_buffer_2 + v_offs),
205                button_type='square',
206                size=(b_size, b_size),
207                label='',
208                autoselect=True,
209                on_activate_call=bui.Call(self._change_replay_speed, 1),
210            )
211            bui.textwidget(
212                parent=self._root_widget,
213                draw_controller=btn,
214                text='+',
215                position=(
216                    h + b_size * 0.5 + b_buffer_1,
217                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
218                ),
219                h_align='center',
220                v_align='center',
221                size=(0, 0),
222                scale=3.0 * t_scale,
223            )
224            self._pause_resume_button = btn = bui.buttonwidget(
225                parent=self._root_widget,
226                position=(h - b_size * 0.5, v - b_size - b_buffer_2 + v_offs),
227                button_type='square',
228                size=(b_size, b_size),
229                label=bui.charstr(
230                    bui.SpecialChar.PLAY_BUTTON
231                    if bs.is_replay_paused()
232                    else bui.SpecialChar.PAUSE_BUTTON
233                ),
234                autoselect=True,
235                on_activate_call=bui.Call(self._pause_or_resume_replay),
236            )
237            btn = bui.buttonwidget(
238                parent=self._root_widget,
239                position=(
240                    h - b_size * 1.5 - b_buffer_1 * 2,
241                    v - b_size - b_buffer_2 + v_offs,
242                ),
243                button_type='square',
244                size=(b_size, b_size),
245                label='',
246                autoselect=True,
247                on_activate_call=bui.WeakCall(self._rewind_replay),
248            )
249            bui.textwidget(
250                parent=self._root_widget,
251                draw_controller=btn,
252                # text='<<',
253                text=bui.charstr(bui.SpecialChar.REWIND_BUTTON),
254                position=(
255                    h - b_size - b_buffer_1 * 2,
256                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
257                ),
258                h_align='center',
259                v_align='center',
260                size=(0, 0),
261                scale=2.0 * t_scale,
262            )
263            btn = bui.buttonwidget(
264                parent=self._root_widget,
265                position=(
266                    h + b_size * 0.5 + b_buffer_1 * 2,
267                    v - b_size - b_buffer_2 + v_offs,
268                ),
269                button_type='square',
270                size=(b_size, b_size),
271                label='',
272                autoselect=True,
273                on_activate_call=bui.WeakCall(self._forward_replay),
274            )
275            bui.textwidget(
276                parent=self._root_widget,
277                draw_controller=btn,
278                # text='>>',
279                text=bui.charstr(bui.SpecialChar.FAST_FORWARD_BUTTON),
280                position=(
281                    h + b_size + b_buffer_1 * 2,
282                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
283                ),
284                h_align='center',
285                v_align='center',
286                size=(0, 0),
287                scale=2.0 * t_scale,
288            )
289
290    def _rewind_replay(self) -> None:
291        bs.seek_replay(-2 * pow(2, bs.get_replay_speed_exponent()))
292
293    def _forward_replay(self) -> None:
294        bs.seek_replay(2 * pow(2, bs.get_replay_speed_exponent()))
295
296    def _refresh_in_game(
297        self, positions: list[tuple[float, float, float]]
298    ) -> tuple[float, float, float]:
299        # pylint: disable=too-many-branches
300        # pylint: disable=too-many-locals
301        # pylint: disable=too-many-statements
302        assert bui.app.classic is not None
303        custom_menu_entries: list[dict[str, Any]] = []
304        session = bs.get_foreground_host_session()
305        if session is not None:
306            try:
307                custom_menu_entries = session.get_custom_menu_entries()
308                for cme in custom_menu_entries:
309                    cme_any: Any = cme  # Type check may not hold true.
310                    if (
311                        not isinstance(cme_any, dict)
312                        or 'label' not in cme
313                        or not isinstance(cme['label'], (str, bui.Lstr))
314                        or 'call' not in cme
315                        or not callable(cme['call'])
316                    ):
317                        raise ValueError(
318                            'invalid custom menu entry: ' + str(cme)
319                        )
320            except Exception:
321                custom_menu_entries = []
322                logging.exception(
323                    'Error getting custom menu entries for %s.', session
324                )
325        self._width = 250.0
326        self._height = 250.0 if self._input_player else 180.0
327        if (self._is_demo or self._is_arcade) and self._input_player:
328            self._height -= 40
329        # if not self._have_settings_button:
330        self._height -= 50
331        if self._connected_to_remote_player:
332            # In this case we have a leave *and* a disconnect button.
333            self._height += 50
334        self._height += 50 * (len(custom_menu_entries))
335        uiscale = bui.app.ui_v1.uiscale
336        bui.containerwidget(
337            edit=self._root_widget,
338            size=(self._width, self._height),
339            scale=(
340                2.15
341                if uiscale is bui.UIScale.SMALL
342                else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0
343            ),
344        )
345        h = 125.0
346        v = self._height - 80.0 if self._input_player else self._height - 60
347        h_offset = 0
348        d_h_offset = 0
349        v_offset = -50
350        for _i in range(6 + len(custom_menu_entries)):
351            positions.append((h, v, 1.0))
352            v += v_offset
353            h += h_offset
354            h_offset += d_h_offset
355        # self._play_button = None
356        bui.app.classic.pause()
357
358        # Player name if applicable.
359        if self._input_player:
360            player_name = self._input_player.getname()
361            h, v, scale = positions[self._p_index]
362            v += 35
363            bui.textwidget(
364                parent=self._root_widget,
365                position=(h - self._button_width / 2, v),
366                size=(self._button_width, self._button_height),
367                color=(1, 1, 1, 0.5),
368                scale=0.7,
369                h_align='center',
370                text=bui.Lstr(value=player_name),
371            )
372        else:
373            player_name = ''
374        h, v, scale = positions[self._p_index]
375        self._p_index += 1
376        btn = bui.buttonwidget(
377            parent=self._root_widget,
378            position=(h - self._button_width / 2, v),
379            size=(self._button_width, self._button_height),
380            scale=scale,
381            label=bui.Lstr(resource=f'{self._r}.resumeText'),
382            autoselect=self._use_autoselect,
383            on_activate_call=self._resume,
384        )
385        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
386
387        # Add any custom options defined by the current game.
388        for entry in custom_menu_entries:
389            h, v, scale = positions[self._p_index]
390            self._p_index += 1
391
392            # Ask the entry whether we should resume when we call
393            # it (defaults to true).
394            resume = bool(entry.get('resume_on_call', True))
395
396            if resume:
397                call = bui.Call(self._resume_and_call, entry['call'])
398            else:
399                call = bui.Call(entry['call'], bui.WeakCall(self._resume))
400
401            bui.buttonwidget(
402                parent=self._root_widget,
403                position=(h - self._button_width / 2, v),
404                size=(self._button_width, self._button_height),
405                scale=scale,
406                on_activate_call=call,
407                label=entry['label'],
408                autoselect=self._use_autoselect,
409            )
410
411        # Add a 'leave' button if the menu-owner has a player.
412        if (self._input_player or self._connected_to_remote_player) and not (
413            self._is_demo or self._is_arcade
414        ):
415            h, v, scale = positions[self._p_index]
416            self._p_index += 1
417            btn = bui.buttonwidget(
418                parent=self._root_widget,
419                position=(h - self._button_width / 2, v),
420                size=(self._button_width, self._button_height),
421                scale=scale,
422                on_activate_call=self._leave,
423                label='',
424                autoselect=self._use_autoselect,
425            )
426
427            if (
428                player_name != ''
429                and player_name[0] != '<'
430                and player_name[-1] != '>'
431            ):
432                txt = bui.Lstr(
433                    resource=f'{self._r}.justPlayerText',
434                    subs=[('${NAME}', player_name)],
435                )
436            else:
437                txt = bui.Lstr(value=player_name)
438            bui.textwidget(
439                parent=self._root_widget,
440                position=(
441                    h,
442                    v
443                    + self._button_height
444                    * (0.64 if player_name != '' else 0.5),
445                ),
446                size=(0, 0),
447                text=bui.Lstr(resource=f'{self._r}.leaveGameText'),
448                scale=(0.83 if player_name != '' else 1.0),
449                color=(0.75, 1.0, 0.7),
450                h_align='center',
451                v_align='center',
452                draw_controller=btn,
453                maxwidth=self._button_width * 0.9,
454            )
455            bui.textwidget(
456                parent=self._root_widget,
457                position=(h, v + self._button_height * 0.27),
458                size=(0, 0),
459                text=txt,
460                color=(0.75, 1.0, 0.7),
461                h_align='center',
462                v_align='center',
463                draw_controller=btn,
464                scale=0.45,
465                maxwidth=self._button_width * 0.9,
466            )
467        return h, v, scale
468
469    def _change_replay_speed(self, offs: int) -> None:
470        if not self._replay_speed_text:
471            if bui.do_once():
472                print('_change_replay_speed called without widget')
473            return
474        bs.set_replay_speed_exponent(bs.get_replay_speed_exponent() + offs)
475        actual_speed = pow(2.0, bs.get_replay_speed_exponent())
476        bui.textwidget(
477            edit=self._replay_speed_text,
478            text=bui.Lstr(
479                resource='watchWindow.playbackSpeedText',
480                subs=[('${SPEED}', str(actual_speed))],
481            ),
482        )
483
484    def _pause_or_resume_replay(self) -> None:
485        if bs.is_replay_paused():
486            bs.resume_replay()
487            bui.buttonwidget(
488                edit=self._pause_resume_button,
489                label=bui.charstr(bui.SpecialChar.PAUSE_BUTTON),
490            )
491        else:
492            bs.pause_replay()
493            bui.buttonwidget(
494                edit=self._pause_resume_button,
495                label=bui.charstr(bui.SpecialChar.PLAY_BUTTON),
496            )
497
498    def _is_benchmark(self) -> bool:
499        session = bs.get_foreground_host_session()
500        return getattr(session, 'benchmark_type', None) == 'cpu' or (
501            bui.app.classic is not None
502            and bui.app.classic.stress_test_update_timer is not None
503        )
504
505    def _confirm_end_game(self) -> None:
506        # pylint: disable=cyclic-import
507        from bauiv1lib.confirm import ConfirmWindow
508
509        # FIXME: Currently we crash calling this on client-sessions.
510
511        # Select cancel by default; this occasionally gets called by accident
512        # in a fit of button mashing and this will help reduce damage.
513        ConfirmWindow(
514            bui.Lstr(resource=f'{self._r}.exitToMenuText'),
515            self._end_game,
516            cancel_is_selected=True,
517        )
518
519    def _confirm_end_test(self) -> None:
520        # pylint: disable=cyclic-import
521        from bauiv1lib.confirm import ConfirmWindow
522
523        # Select cancel by default; this occasionally gets called by accident
524        # in a fit of button mashing and this will help reduce damage.
525        ConfirmWindow(
526            bui.Lstr(resource=f'{self._r}.exitToMenuText'),
527            self._end_game,
528            cancel_is_selected=True,
529        )
530
531    def _confirm_end_replay(self) -> None:
532        # pylint: disable=cyclic-import
533        from bauiv1lib.confirm import ConfirmWindow
534
535        # Select cancel by default; this occasionally gets called by accident
536        # in a fit of button mashing and this will help reduce damage.
537        ConfirmWindow(
538            bui.Lstr(resource=f'{self._r}.exitToMenuText'),
539            self._end_game,
540            cancel_is_selected=True,
541        )
542
543    def _confirm_leave_party(self) -> None:
544        # pylint: disable=cyclic-import
545        from bauiv1lib.confirm import ConfirmWindow
546
547        # Select cancel by default; this occasionally gets called by accident
548        # in a fit of button mashing and this will help reduce damage.
549        ConfirmWindow(
550            bui.Lstr(resource=f'{self._r}.leavePartyConfirmText'),
551            self._leave_party,
552            cancel_is_selected=True,
553        )
554
555    def _leave_party(self) -> None:
556        bs.disconnect_from_host()
557
558    def _end_game(self) -> None:
559        assert bui.app.classic is not None
560
561        # no-op if our underlying widget is dead or on its way out.
562        if not self._root_widget or self._root_widget.transitioning_out:
563            return
564
565        bui.containerwidget(edit=self._root_widget, transition='out_left')
566        bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False)
567
568    def _leave(self) -> None:
569        if self._input_player:
570            self._input_player.remove_from_game()
571        elif self._connected_to_remote_player:
572            if self._input_device:
573                self._input_device.detach_from_player()
574        self._resume()
575
576    def _resume_and_call(self, call: Callable[[], Any]) -> None:
577        self._resume()
578        call()
579
580    def _resume(self) -> None:
581        classic = bui.app.classic
582
583        assert classic is not None
584        classic.resume()
585
586        bui.app.ui_v1.clear_main_window()
587
588        # If there's callbacks waiting for us to resume, call them.
589        for call in classic.main_menu_resume_callbacks:
590            try:
591                call()
592            except Exception:
593                logging.exception('Error in classic resume callback.')
594
595        classic.main_menu_resume_callbacks.clear()
596
597    # def __del__(self) -> None:
598    #     self._resume()

The menu that can be invoked while in a game.

InGameMenuWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
21    def __init__(
22        self,
23        transition: str | None = 'in_right',
24        origin_widget: bui.Widget | None = None,
25    ):
26
27        # Make a vanilla container; we'll modify it to our needs in
28        # refresh.
29        super().__init__(
30            root_widget=bui.containerwidget(
31                toolbar_visibility=('menu_in_game')
32            ),
33            transition=transition,
34            origin_widget=origin_widget,
35        )
36
37        # Grab this stuff in case it changes.
38        self._is_demo = bui.app.env.demo
39        self._is_arcade = bui.app.env.arcade
40
41        self._p_index = 0
42        self._use_autoselect = True
43        self._button_width = 200.0
44        self._button_height = 45.0
45        self._width = 100.0
46        self._height = 100.0
47
48        self._refresh()

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
50    @override
51    def get_main_window_state(self) -> bui.MainWindowState:
52        # Support recreating our window for back/refresh purposes.
53        return self.do_get_main_window_state()

Return a WindowState to recreate this window, if supported.

@classmethod
def do_get_main_window_state(cls) -> bauiv1.MainWindowState:
55    @classmethod
56    def do_get_main_window_state(cls) -> bui.MainWindowState:
57        """Classmethod to gen a windowstate for the main menu."""
58        return bui.BasicMainWindowState(
59            create_call=lambda transition, origin_widget: cls(
60                transition=transition, origin_widget=origin_widget
61            )
62        )

Classmethod to gen a windowstate for the main menu.