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

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        cls = type(self)
54        return bui.BasicMainWindowState(
55            create_call=lambda transition, origin_widget: cls(
56                transition=transition, origin_widget=origin_widget
57            )
58        )

Return a WindowState to recreate this window, if supported.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_close
can_change_main_window
main_window_back
main_window_replace
on_main_window_close
bauiv1._uitypes.Window
get_root_widget