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

Return a WindowState to recreate this window, if supported.