bastd.ui.watch

Provides UI functionality for watching replays.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides UI functionality for watching replays."""
  4
  5from __future__ import annotations
  6
  7import os
  8from enum import Enum
  9from typing import TYPE_CHECKING, cast
 10
 11import ba
 12import ba.internal
 13
 14if TYPE_CHECKING:
 15    from typing import Any
 16
 17
 18class WatchWindow(ba.Window):
 19    """Window for watching replays."""
 20
 21    class TabID(Enum):
 22        """Our available tab types."""
 23
 24        MY_REPLAYS = 'my_replays'
 25        TEST_TAB = 'test_tab'
 26
 27    def __init__(
 28        self,
 29        transition: str | None = 'in_right',
 30        origin_widget: ba.Widget | None = None,
 31    ):
 32        # pylint: disable=too-many-locals
 33        # pylint: disable=too-many-statements
 34        from bastd.ui.tabs import TabRow
 35
 36        ba.set_analytics_screen('Watch Window')
 37        scale_origin: tuple[float, float] | None
 38        if origin_widget is not None:
 39            self._transition_out = 'out_scale'
 40            scale_origin = origin_widget.get_screen_space_center()
 41            transition = 'in_scale'
 42        else:
 43            self._transition_out = 'out_right'
 44            scale_origin = None
 45        ba.app.ui.set_main_menu_location('Watch')
 46        self._tab_data: dict[str, Any] = {}
 47        self._my_replays_scroll_width: float | None = None
 48        self._my_replays_watch_replay_button: ba.Widget | None = None
 49        self._scrollwidget: ba.Widget | None = None
 50        self._columnwidget: ba.Widget | None = None
 51        self._my_replay_selected: str | None = None
 52        self._my_replays_rename_window: ba.Widget | None = None
 53        self._my_replay_rename_text: ba.Widget | None = None
 54        self._r = 'watchWindow'
 55        uiscale = ba.app.ui.uiscale
 56        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
 57        x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
 58        self._height = (
 59            578
 60            if uiscale is ba.UIScale.SMALL
 61            else 670
 62            if uiscale is ba.UIScale.MEDIUM
 63            else 800
 64        )
 65        self._current_tab: WatchWindow.TabID | None = None
 66        extra_top = 20 if uiscale is ba.UIScale.SMALL else 0
 67
 68        super().__init__(
 69            root_widget=ba.containerwidget(
 70                size=(self._width, self._height + extra_top),
 71                transition=transition,
 72                toolbar_visibility='menu_minimal',
 73                scale_origin_stack_offset=scale_origin,
 74                scale=(
 75                    1.3
 76                    if uiscale is ba.UIScale.SMALL
 77                    else 0.97
 78                    if uiscale is ba.UIScale.MEDIUM
 79                    else 0.8
 80                ),
 81                stack_offset=(0, -10)
 82                if uiscale is ba.UIScale.SMALL
 83                else (0, 15)
 84                if uiscale is ba.UIScale.MEDIUM
 85                else (0, 0),
 86            )
 87        )
 88
 89        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
 90            ba.containerwidget(
 91                edit=self._root_widget, on_cancel_call=self._back
 92            )
 93            self._back_button = None
 94        else:
 95            self._back_button = btn = ba.buttonwidget(
 96                parent=self._root_widget,
 97                autoselect=True,
 98                position=(70 + x_inset, self._height - 74),
 99                size=(140, 60),
100                scale=1.1,
101                label=ba.Lstr(resource='backText'),
102                button_type='back',
103                on_activate_call=self._back,
104            )
105            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
106            ba.buttonwidget(
107                edit=btn,
108                button_type='backSmall',
109                size=(60, 60),
110                label=ba.charstr(ba.SpecialChar.BACK),
111            )
112
113        ba.textwidget(
114            parent=self._root_widget,
115            position=(self._width * 0.5, self._height - 38),
116            size=(0, 0),
117            color=ba.app.ui.title_color,
118            scale=1.5,
119            h_align='center',
120            v_align='center',
121            text=ba.Lstr(resource=self._r + '.titleText'),
122            maxwidth=400,
123        )
124
125        tabdefs = [
126            (
127                self.TabID.MY_REPLAYS,
128                ba.Lstr(resource=self._r + '.myReplaysText'),
129            ),
130            # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')),
131        ]
132
133        scroll_buffer_h = 130 + 2 * x_inset
134        tab_buffer_h = 750 + 2 * x_inset
135
136        self._tab_row = TabRow(
137            self._root_widget,
138            tabdefs,
139            pos=(tab_buffer_h * 0.5, self._height - 130),
140            size=(self._width - tab_buffer_h, 50),
141            on_select_call=self._set_tab,
142        )
143
144        if ba.app.ui.use_toolbars:
145            first_tab = self._tab_row.tabs[tabdefs[0][0]]
146            last_tab = self._tab_row.tabs[tabdefs[-1][0]]
147            ba.widget(
148                edit=last_tab.button,
149                right_widget=ba.internal.get_special_widget('party_button'),
150            )
151            if uiscale is ba.UIScale.SMALL:
152                bbtn = ba.internal.get_special_widget('back_button')
153                ba.widget(
154                    edit=first_tab.button, up_widget=bbtn, left_widget=bbtn
155                )
156
157        self._scroll_width = self._width - scroll_buffer_h
158        self._scroll_height = self._height - 180
159
160        # Not actually using a scroll widget anymore; just an image.
161        scroll_left = (self._width - self._scroll_width) * 0.5
162        scroll_bottom = self._height - self._scroll_height - 79 - 48
163        buffer_h = 10
164        buffer_v = 4
165        ba.imagewidget(
166            parent=self._root_widget,
167            position=(scroll_left - buffer_h, scroll_bottom - buffer_v),
168            size=(
169                self._scroll_width + 2 * buffer_h,
170                self._scroll_height + 2 * buffer_v,
171            ),
172            texture=ba.gettexture('scrollWidget'),
173            model_transparent=ba.getmodel('softEdgeOutside'),
174        )
175        self._tab_container: ba.Widget | None = None
176
177        self._restore_state()
178
179    def _set_tab(self, tab_id: TabID) -> None:
180        # pylint: disable=too-many-locals
181
182        if self._current_tab == tab_id:
183            return
184        self._current_tab = tab_id
185
186        # Preserve our current tab between runs.
187        cfg = ba.app.config
188        cfg['Watch Tab'] = tab_id.value
189        cfg.commit()
190
191        # Update tab colors based on which is selected.
192        # tabs.update_tab_button_colors(self._tab_buttons, tab)
193        self._tab_row.update_appearance(tab_id)
194
195        if self._tab_container:
196            self._tab_container.delete()
197        scroll_left = (self._width - self._scroll_width) * 0.5
198        scroll_bottom = self._height - self._scroll_height - 79 - 48
199
200        # A place where tabs can store data to get cleared when
201        # switching to a different tab
202        self._tab_data = {}
203
204        uiscale = ba.app.ui.uiscale
205        if tab_id is self.TabID.MY_REPLAYS:
206            c_width = self._scroll_width
207            c_height = self._scroll_height - 20
208            sub_scroll_height = c_height - 63
209            self._my_replays_scroll_width = sub_scroll_width = (
210                680 if uiscale is ba.UIScale.SMALL else 640
211            )
212
213            self._tab_container = cnt = ba.containerwidget(
214                parent=self._root_widget,
215                position=(
216                    scroll_left,
217                    scroll_bottom + (self._scroll_height - c_height) * 0.5,
218                ),
219                size=(c_width, c_height),
220                background=False,
221                selection_loops_to_parent=True,
222            )
223
224            v = c_height - 30
225            ba.textwidget(
226                parent=cnt,
227                position=(c_width * 0.5, v),
228                color=(0.6, 1.0, 0.6),
229                scale=0.7,
230                size=(0, 0),
231                maxwidth=c_width * 0.9,
232                h_align='center',
233                v_align='center',
234                text=ba.Lstr(
235                    resource='replayRenameWarningText',
236                    subs=[
237                        ('${REPLAY}', ba.Lstr(resource='replayNameDefaultText'))
238                    ],
239                ),
240            )
241
242            b_width = 140 if uiscale is ba.UIScale.SMALL else 178
243            b_height = (
244                107
245                if uiscale is ba.UIScale.SMALL
246                else 142
247                if uiscale is ba.UIScale.MEDIUM
248                else 190
249            )
250            b_space_extra = (
251                0
252                if uiscale is ba.UIScale.SMALL
253                else -2
254                if uiscale is ba.UIScale.MEDIUM
255                else -5
256            )
257
258            b_color = (0.6, 0.53, 0.63)
259            b_textcolor = (0.75, 0.7, 0.8)
260            btnv = (
261                c_height
262                - (
263                    48
264                    if uiscale is ba.UIScale.SMALL
265                    else 45
266                    if uiscale is ba.UIScale.MEDIUM
267                    else 40
268                )
269                - b_height
270            )
271            btnh = 40 if uiscale is ba.UIScale.SMALL else 40
272            smlh = 190 if uiscale is ba.UIScale.SMALL else 225
273            tscl = 1.0 if uiscale is ba.UIScale.SMALL else 1.2
274            self._my_replays_watch_replay_button = btn1 = ba.buttonwidget(
275                parent=cnt,
276                size=(b_width, b_height),
277                position=(btnh, btnv),
278                button_type='square',
279                color=b_color,
280                textcolor=b_textcolor,
281                on_activate_call=self._on_my_replay_play_press,
282                text_scale=tscl,
283                label=ba.Lstr(resource=self._r + '.watchReplayButtonText'),
284                autoselect=True,
285            )
286            ba.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button)
287            if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
288                ba.widget(
289                    edit=btn1,
290                    left_widget=ba.internal.get_special_widget('back_button'),
291                )
292            btnv -= b_height + b_space_extra
293            ba.buttonwidget(
294                parent=cnt,
295                size=(b_width, b_height),
296                position=(btnh, btnv),
297                button_type='square',
298                color=b_color,
299                textcolor=b_textcolor,
300                on_activate_call=self._on_my_replay_rename_press,
301                text_scale=tscl,
302                label=ba.Lstr(resource=self._r + '.renameReplayButtonText'),
303                autoselect=True,
304            )
305            btnv -= b_height + b_space_extra
306            ba.buttonwidget(
307                parent=cnt,
308                size=(b_width, b_height),
309                position=(btnh, btnv),
310                button_type='square',
311                color=b_color,
312                textcolor=b_textcolor,
313                on_activate_call=self._on_my_replay_delete_press,
314                text_scale=tscl,
315                label=ba.Lstr(resource=self._r + '.deleteReplayButtonText'),
316                autoselect=True,
317            )
318
319            v -= sub_scroll_height + 23
320            self._scrollwidget = scrlw = ba.scrollwidget(
321                parent=cnt,
322                position=(smlh, v),
323                size=(sub_scroll_width, sub_scroll_height),
324            )
325            ba.containerwidget(edit=cnt, selected_child=scrlw)
326            self._columnwidget = ba.columnwidget(
327                parent=scrlw, left_border=10, border=2, margin=0
328            )
329
330            ba.widget(
331                edit=scrlw,
332                autoselect=True,
333                left_widget=btn1,
334                up_widget=self._tab_row.tabs[tab_id].button,
335            )
336            ba.widget(edit=self._tab_row.tabs[tab_id].button, down_widget=scrlw)
337
338            self._my_replay_selected = None
339            self._refresh_my_replays()
340
341    def _no_replay_selected_error(self) -> None:
342        ba.screenmessage(
343            ba.Lstr(resource=self._r + '.noReplaySelectedErrorText'),
344            color=(1, 0, 0),
345        )
346        ba.playsound(ba.getsound('error'))
347
348    def _on_my_replay_play_press(self) -> None:
349        if self._my_replay_selected is None:
350            self._no_replay_selected_error()
351            return
352        ba.internal.increment_analytics_count('Replay watch')
353
354        def do_it() -> None:
355            try:
356                # Reset to normal speed.
357                ba.internal.set_replay_speed_exponent(0)
358                ba.internal.fade_screen(True)
359                assert self._my_replay_selected is not None
360                ba.internal.new_replay_session(
361                    ba.internal.get_replays_dir()
362                    + '/'
363                    + self._my_replay_selected
364                )
365            except Exception:
366                ba.print_exception('Error running replay session.')
367
368                # Drop back into a fresh main menu session
369                # in case we half-launched or something.
370                from bastd import mainmenu
371
372                ba.internal.new_host_session(mainmenu.MainMenuSession)
373
374        ba.internal.fade_screen(False, endcall=ba.Call(ba.pushcall, do_it))
375        ba.containerwidget(edit=self._root_widget, transition='out_left')
376
377    def _on_my_replay_rename_press(self) -> None:
378        if self._my_replay_selected is None:
379            self._no_replay_selected_error()
380            return
381        c_width = 600
382        c_height = 250
383        uiscale = ba.app.ui.uiscale
384        self._my_replays_rename_window = cnt = ba.containerwidget(
385            scale=(
386                1.8
387                if uiscale is ba.UIScale.SMALL
388                else 1.55
389                if uiscale is ba.UIScale.MEDIUM
390                else 1.0
391            ),
392            size=(c_width, c_height),
393            transition='in_scale',
394        )
395        dname = self._get_replay_display_name(self._my_replay_selected)
396        ba.textwidget(
397            parent=cnt,
398            size=(0, 0),
399            h_align='center',
400            v_align='center',
401            text=ba.Lstr(
402                resource=self._r + '.renameReplayText',
403                subs=[('${REPLAY}', dname)],
404            ),
405            maxwidth=c_width * 0.8,
406            position=(c_width * 0.5, c_height - 60),
407        )
408        self._my_replay_rename_text = txt = ba.textwidget(
409            parent=cnt,
410            size=(c_width * 0.8, 40),
411            h_align='left',
412            v_align='center',
413            text=dname,
414            editable=True,
415            description=ba.Lstr(resource=self._r + '.replayNameText'),
416            position=(c_width * 0.1, c_height - 140),
417            autoselect=True,
418            maxwidth=c_width * 0.7,
419            max_chars=200,
420        )
421        cbtn = ba.buttonwidget(
422            parent=cnt,
423            label=ba.Lstr(resource='cancelText'),
424            on_activate_call=ba.Call(
425                lambda c: ba.containerwidget(edit=c, transition='out_scale'),
426                cnt,
427            ),
428            size=(180, 60),
429            position=(30, 30),
430            autoselect=True,
431        )
432        okb = ba.buttonwidget(
433            parent=cnt,
434            label=ba.Lstr(resource=self._r + '.renameText'),
435            size=(180, 60),
436            position=(c_width - 230, 30),
437            on_activate_call=ba.Call(
438                self._rename_my_replay, self._my_replay_selected
439            ),
440            autoselect=True,
441        )
442        ba.widget(edit=cbtn, right_widget=okb)
443        ba.widget(edit=okb, left_widget=cbtn)
444        ba.textwidget(edit=txt, on_return_press_call=okb.activate)
445        ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb)
446
447    def _rename_my_replay(self, replay: str) -> None:
448        new_name = None
449        try:
450            if not self._my_replay_rename_text:
451                return
452            new_name_raw = cast(
453                str, ba.textwidget(query=self._my_replay_rename_text)
454            )
455            new_name = new_name_raw + '.brp'
456
457            # Ignore attempts to change it to what it already is
458            # (or what it looks like to the user).
459            if (
460                replay != new_name
461                and self._get_replay_display_name(replay) != new_name_raw
462            ):
463                old_name_full = (
464                    ba.internal.get_replays_dir() + '/' + replay
465                ).encode('utf-8')
466                new_name_full = (
467                    ba.internal.get_replays_dir() + '/' + new_name
468                ).encode('utf-8')
469                # False alarm; ba.textwidget can return non-None val.
470                # pylint: disable=unsupported-membership-test
471                if os.path.exists(new_name_full):
472                    ba.playsound(ba.getsound('error'))
473                    ba.screenmessage(
474                        ba.Lstr(
475                            resource=self._r
476                            + '.replayRenameErrorAlreadyExistsText'
477                        ),
478                        color=(1, 0, 0),
479                    )
480                elif any(char in new_name_raw for char in ['/', '\\', ':']):
481                    ba.playsound(ba.getsound('error'))
482                    ba.screenmessage(
483                        ba.Lstr(
484                            resource=self._r + '.replayRenameErrorInvalidName'
485                        ),
486                        color=(1, 0, 0),
487                    )
488                else:
489                    ba.internal.increment_analytics_count('Replay rename')
490                    os.rename(old_name_full, new_name_full)
491                    self._refresh_my_replays()
492                    ba.playsound(ba.getsound('gunCocking'))
493        except Exception:
494            ba.print_exception(
495                f"Error renaming replay '{replay}' to '{new_name}'."
496            )
497            ba.playsound(ba.getsound('error'))
498            ba.screenmessage(
499                ba.Lstr(resource=self._r + '.replayRenameErrorText'),
500                color=(1, 0, 0),
501            )
502
503        ba.containerwidget(
504            edit=self._my_replays_rename_window, transition='out_scale'
505        )
506
507    def _on_my_replay_delete_press(self) -> None:
508        from bastd.ui import confirm
509
510        if self._my_replay_selected is None:
511            self._no_replay_selected_error()
512            return
513        confirm.ConfirmWindow(
514            ba.Lstr(
515                resource=self._r + '.deleteConfirmText',
516                subs=[
517                    (
518                        '${REPLAY}',
519                        self._get_replay_display_name(self._my_replay_selected),
520                    )
521                ],
522            ),
523            ba.Call(self._delete_replay, self._my_replay_selected),
524            450,
525            150,
526        )
527
528    def _get_replay_display_name(self, replay: str) -> str:
529        if replay.endswith('.brp'):
530            replay = replay[:-4]
531        if replay == '__lastReplay':
532            return ba.Lstr(resource='replayNameDefaultText').evaluate()
533        return replay
534
535    def _delete_replay(self, replay: str) -> None:
536        try:
537            ba.internal.increment_analytics_count('Replay delete')
538            os.remove(
539                (ba.internal.get_replays_dir() + '/' + replay).encode('utf-8')
540            )
541            self._refresh_my_replays()
542            ba.playsound(ba.getsound('shieldDown'))
543            if replay == self._my_replay_selected:
544                self._my_replay_selected = None
545        except Exception:
546            ba.print_exception(f"Error deleting replay '{replay}'.")
547            ba.playsound(ba.getsound('error'))
548            ba.screenmessage(
549                ba.Lstr(resource=self._r + '.replayDeleteErrorText'),
550                color=(1, 0, 0),
551            )
552
553    def _on_my_replay_select(self, replay: str) -> None:
554        self._my_replay_selected = replay
555
556    def _refresh_my_replays(self) -> None:
557        assert self._columnwidget is not None
558        for child in self._columnwidget.get_children():
559            child.delete()
560        t_scale = 1.6
561        try:
562            names = os.listdir(ba.internal.get_replays_dir())
563
564            # Ignore random other files in there.
565            names = [n for n in names if n.endswith('.brp')]
566            names.sort(key=lambda x: x.lower())
567        except Exception:
568            ba.print_exception('Error listing replays dir.')
569            names = []
570
571        assert self._my_replays_scroll_width is not None
572        assert self._my_replays_watch_replay_button is not None
573        for i, name in enumerate(names):
574            txt = ba.textwidget(
575                parent=self._columnwidget,
576                size=(self._my_replays_scroll_width / t_scale, 30),
577                selectable=True,
578                color=(1.0, 1, 0.4)
579                if name == '__lastReplay.brp'
580                else (1, 1, 1),
581                always_highlight=True,
582                on_select_call=ba.Call(self._on_my_replay_select, name),
583                on_activate_call=self._my_replays_watch_replay_button.activate,
584                text=self._get_replay_display_name(name),
585                h_align='left',
586                v_align='center',
587                corner_scale=t_scale,
588                maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93,
589            )
590            if i == 0:
591                ba.widget(
592                    edit=txt,
593                    up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button,
594                )
595
596    def _save_state(self) -> None:
597        try:
598            sel = self._root_widget.get_selected_child()
599            selected_tab_ids = [
600                tab_id
601                for tab_id, tab in self._tab_row.tabs.items()
602                if sel == tab.button
603            ]
604            if sel == self._back_button:
605                sel_name = 'Back'
606            elif selected_tab_ids:
607                assert len(selected_tab_ids) == 1
608                sel_name = f'Tab:{selected_tab_ids[0].value}'
609            elif sel == self._tab_container:
610                sel_name = 'TabContainer'
611            else:
612                raise ValueError(f'unrecognized selection {sel}')
613            ba.app.ui.window_states[type(self)] = {'sel_name': sel_name}
614        except Exception:
615            ba.print_exception(f'Error saving state for {self}.')
616
617    def _restore_state(self) -> None:
618        from efro.util import enum_by_value
619
620        try:
621            sel: ba.Widget | None
622            sel_name = ba.app.ui.window_states.get(type(self), {}).get(
623                'sel_name'
624            )
625            assert isinstance(sel_name, (str, type(None)))
626            try:
627                current_tab = enum_by_value(
628                    self.TabID, ba.app.config.get('Watch Tab')
629                )
630            except ValueError:
631                current_tab = self.TabID.MY_REPLAYS
632            self._set_tab(current_tab)
633
634            if sel_name == 'Back':
635                sel = self._back_button
636            elif sel_name == 'TabContainer':
637                sel = self._tab_container
638            elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
639                try:
640                    sel_tab_id = enum_by_value(
641                        self.TabID, sel_name.split(':')[-1]
642                    )
643                except ValueError:
644                    sel_tab_id = self.TabID.MY_REPLAYS
645                sel = self._tab_row.tabs[sel_tab_id].button
646            else:
647                if self._tab_container is not None:
648                    sel = self._tab_container
649                else:
650                    sel = self._tab_row.tabs[current_tab].button
651            ba.containerwidget(edit=self._root_widget, selected_child=sel)
652        except Exception:
653            ba.print_exception(f'Error restoring state for {self}.')
654
655    def _back(self) -> None:
656        from bastd.ui.mainmenu import MainMenuWindow
657
658        self._save_state()
659        ba.containerwidget(
660            edit=self._root_widget, transition=self._transition_out
661        )
662        ba.app.ui.set_main_menu_window(
663            MainMenuWindow(transition='in_left').get_root_widget()
664        )
class WatchWindow(ba.ui.Window):
 19class WatchWindow(ba.Window):
 20    """Window for watching replays."""
 21
 22    class TabID(Enum):
 23        """Our available tab types."""
 24
 25        MY_REPLAYS = 'my_replays'
 26        TEST_TAB = 'test_tab'
 27
 28    def __init__(
 29        self,
 30        transition: str | None = 'in_right',
 31        origin_widget: ba.Widget | None = None,
 32    ):
 33        # pylint: disable=too-many-locals
 34        # pylint: disable=too-many-statements
 35        from bastd.ui.tabs import TabRow
 36
 37        ba.set_analytics_screen('Watch Window')
 38        scale_origin: tuple[float, float] | None
 39        if origin_widget is not None:
 40            self._transition_out = 'out_scale'
 41            scale_origin = origin_widget.get_screen_space_center()
 42            transition = 'in_scale'
 43        else:
 44            self._transition_out = 'out_right'
 45            scale_origin = None
 46        ba.app.ui.set_main_menu_location('Watch')
 47        self._tab_data: dict[str, Any] = {}
 48        self._my_replays_scroll_width: float | None = None
 49        self._my_replays_watch_replay_button: ba.Widget | None = None
 50        self._scrollwidget: ba.Widget | None = None
 51        self._columnwidget: ba.Widget | None = None
 52        self._my_replay_selected: str | None = None
 53        self._my_replays_rename_window: ba.Widget | None = None
 54        self._my_replay_rename_text: ba.Widget | None = None
 55        self._r = 'watchWindow'
 56        uiscale = ba.app.ui.uiscale
 57        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
 58        x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
 59        self._height = (
 60            578
 61            if uiscale is ba.UIScale.SMALL
 62            else 670
 63            if uiscale is ba.UIScale.MEDIUM
 64            else 800
 65        )
 66        self._current_tab: WatchWindow.TabID | None = None
 67        extra_top = 20 if uiscale is ba.UIScale.SMALL else 0
 68
 69        super().__init__(
 70            root_widget=ba.containerwidget(
 71                size=(self._width, self._height + extra_top),
 72                transition=transition,
 73                toolbar_visibility='menu_minimal',
 74                scale_origin_stack_offset=scale_origin,
 75                scale=(
 76                    1.3
 77                    if uiscale is ba.UIScale.SMALL
 78                    else 0.97
 79                    if uiscale is ba.UIScale.MEDIUM
 80                    else 0.8
 81                ),
 82                stack_offset=(0, -10)
 83                if uiscale is ba.UIScale.SMALL
 84                else (0, 15)
 85                if uiscale is ba.UIScale.MEDIUM
 86                else (0, 0),
 87            )
 88        )
 89
 90        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
 91            ba.containerwidget(
 92                edit=self._root_widget, on_cancel_call=self._back
 93            )
 94            self._back_button = None
 95        else:
 96            self._back_button = btn = ba.buttonwidget(
 97                parent=self._root_widget,
 98                autoselect=True,
 99                position=(70 + x_inset, self._height - 74),
100                size=(140, 60),
101                scale=1.1,
102                label=ba.Lstr(resource='backText'),
103                button_type='back',
104                on_activate_call=self._back,
105            )
106            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
107            ba.buttonwidget(
108                edit=btn,
109                button_type='backSmall',
110                size=(60, 60),
111                label=ba.charstr(ba.SpecialChar.BACK),
112            )
113
114        ba.textwidget(
115            parent=self._root_widget,
116            position=(self._width * 0.5, self._height - 38),
117            size=(0, 0),
118            color=ba.app.ui.title_color,
119            scale=1.5,
120            h_align='center',
121            v_align='center',
122            text=ba.Lstr(resource=self._r + '.titleText'),
123            maxwidth=400,
124        )
125
126        tabdefs = [
127            (
128                self.TabID.MY_REPLAYS,
129                ba.Lstr(resource=self._r + '.myReplaysText'),
130            ),
131            # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')),
132        ]
133
134        scroll_buffer_h = 130 + 2 * x_inset
135        tab_buffer_h = 750 + 2 * x_inset
136
137        self._tab_row = TabRow(
138            self._root_widget,
139            tabdefs,
140            pos=(tab_buffer_h * 0.5, self._height - 130),
141            size=(self._width - tab_buffer_h, 50),
142            on_select_call=self._set_tab,
143        )
144
145        if ba.app.ui.use_toolbars:
146            first_tab = self._tab_row.tabs[tabdefs[0][0]]
147            last_tab = self._tab_row.tabs[tabdefs[-1][0]]
148            ba.widget(
149                edit=last_tab.button,
150                right_widget=ba.internal.get_special_widget('party_button'),
151            )
152            if uiscale is ba.UIScale.SMALL:
153                bbtn = ba.internal.get_special_widget('back_button')
154                ba.widget(
155                    edit=first_tab.button, up_widget=bbtn, left_widget=bbtn
156                )
157
158        self._scroll_width = self._width - scroll_buffer_h
159        self._scroll_height = self._height - 180
160
161        # Not actually using a scroll widget anymore; just an image.
162        scroll_left = (self._width - self._scroll_width) * 0.5
163        scroll_bottom = self._height - self._scroll_height - 79 - 48
164        buffer_h = 10
165        buffer_v = 4
166        ba.imagewidget(
167            parent=self._root_widget,
168            position=(scroll_left - buffer_h, scroll_bottom - buffer_v),
169            size=(
170                self._scroll_width + 2 * buffer_h,
171                self._scroll_height + 2 * buffer_v,
172            ),
173            texture=ba.gettexture('scrollWidget'),
174            model_transparent=ba.getmodel('softEdgeOutside'),
175        )
176        self._tab_container: ba.Widget | None = None
177
178        self._restore_state()
179
180    def _set_tab(self, tab_id: TabID) -> None:
181        # pylint: disable=too-many-locals
182
183        if self._current_tab == tab_id:
184            return
185        self._current_tab = tab_id
186
187        # Preserve our current tab between runs.
188        cfg = ba.app.config
189        cfg['Watch Tab'] = tab_id.value
190        cfg.commit()
191
192        # Update tab colors based on which is selected.
193        # tabs.update_tab_button_colors(self._tab_buttons, tab)
194        self._tab_row.update_appearance(tab_id)
195
196        if self._tab_container:
197            self._tab_container.delete()
198        scroll_left = (self._width - self._scroll_width) * 0.5
199        scroll_bottom = self._height - self._scroll_height - 79 - 48
200
201        # A place where tabs can store data to get cleared when
202        # switching to a different tab
203        self._tab_data = {}
204
205        uiscale = ba.app.ui.uiscale
206        if tab_id is self.TabID.MY_REPLAYS:
207            c_width = self._scroll_width
208            c_height = self._scroll_height - 20
209            sub_scroll_height = c_height - 63
210            self._my_replays_scroll_width = sub_scroll_width = (
211                680 if uiscale is ba.UIScale.SMALL else 640
212            )
213
214            self._tab_container = cnt = ba.containerwidget(
215                parent=self._root_widget,
216                position=(
217                    scroll_left,
218                    scroll_bottom + (self._scroll_height - c_height) * 0.5,
219                ),
220                size=(c_width, c_height),
221                background=False,
222                selection_loops_to_parent=True,
223            )
224
225            v = c_height - 30
226            ba.textwidget(
227                parent=cnt,
228                position=(c_width * 0.5, v),
229                color=(0.6, 1.0, 0.6),
230                scale=0.7,
231                size=(0, 0),
232                maxwidth=c_width * 0.9,
233                h_align='center',
234                v_align='center',
235                text=ba.Lstr(
236                    resource='replayRenameWarningText',
237                    subs=[
238                        ('${REPLAY}', ba.Lstr(resource='replayNameDefaultText'))
239                    ],
240                ),
241            )
242
243            b_width = 140 if uiscale is ba.UIScale.SMALL else 178
244            b_height = (
245                107
246                if uiscale is ba.UIScale.SMALL
247                else 142
248                if uiscale is ba.UIScale.MEDIUM
249                else 190
250            )
251            b_space_extra = (
252                0
253                if uiscale is ba.UIScale.SMALL
254                else -2
255                if uiscale is ba.UIScale.MEDIUM
256                else -5
257            )
258
259            b_color = (0.6, 0.53, 0.63)
260            b_textcolor = (0.75, 0.7, 0.8)
261            btnv = (
262                c_height
263                - (
264                    48
265                    if uiscale is ba.UIScale.SMALL
266                    else 45
267                    if uiscale is ba.UIScale.MEDIUM
268                    else 40
269                )
270                - b_height
271            )
272            btnh = 40 if uiscale is ba.UIScale.SMALL else 40
273            smlh = 190 if uiscale is ba.UIScale.SMALL else 225
274            tscl = 1.0 if uiscale is ba.UIScale.SMALL else 1.2
275            self._my_replays_watch_replay_button = btn1 = ba.buttonwidget(
276                parent=cnt,
277                size=(b_width, b_height),
278                position=(btnh, btnv),
279                button_type='square',
280                color=b_color,
281                textcolor=b_textcolor,
282                on_activate_call=self._on_my_replay_play_press,
283                text_scale=tscl,
284                label=ba.Lstr(resource=self._r + '.watchReplayButtonText'),
285                autoselect=True,
286            )
287            ba.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button)
288            if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
289                ba.widget(
290                    edit=btn1,
291                    left_widget=ba.internal.get_special_widget('back_button'),
292                )
293            btnv -= b_height + b_space_extra
294            ba.buttonwidget(
295                parent=cnt,
296                size=(b_width, b_height),
297                position=(btnh, btnv),
298                button_type='square',
299                color=b_color,
300                textcolor=b_textcolor,
301                on_activate_call=self._on_my_replay_rename_press,
302                text_scale=tscl,
303                label=ba.Lstr(resource=self._r + '.renameReplayButtonText'),
304                autoselect=True,
305            )
306            btnv -= b_height + b_space_extra
307            ba.buttonwidget(
308                parent=cnt,
309                size=(b_width, b_height),
310                position=(btnh, btnv),
311                button_type='square',
312                color=b_color,
313                textcolor=b_textcolor,
314                on_activate_call=self._on_my_replay_delete_press,
315                text_scale=tscl,
316                label=ba.Lstr(resource=self._r + '.deleteReplayButtonText'),
317                autoselect=True,
318            )
319
320            v -= sub_scroll_height + 23
321            self._scrollwidget = scrlw = ba.scrollwidget(
322                parent=cnt,
323                position=(smlh, v),
324                size=(sub_scroll_width, sub_scroll_height),
325            )
326            ba.containerwidget(edit=cnt, selected_child=scrlw)
327            self._columnwidget = ba.columnwidget(
328                parent=scrlw, left_border=10, border=2, margin=0
329            )
330
331            ba.widget(
332                edit=scrlw,
333                autoselect=True,
334                left_widget=btn1,
335                up_widget=self._tab_row.tabs[tab_id].button,
336            )
337            ba.widget(edit=self._tab_row.tabs[tab_id].button, down_widget=scrlw)
338
339            self._my_replay_selected = None
340            self._refresh_my_replays()
341
342    def _no_replay_selected_error(self) -> None:
343        ba.screenmessage(
344            ba.Lstr(resource=self._r + '.noReplaySelectedErrorText'),
345            color=(1, 0, 0),
346        )
347        ba.playsound(ba.getsound('error'))
348
349    def _on_my_replay_play_press(self) -> None:
350        if self._my_replay_selected is None:
351            self._no_replay_selected_error()
352            return
353        ba.internal.increment_analytics_count('Replay watch')
354
355        def do_it() -> None:
356            try:
357                # Reset to normal speed.
358                ba.internal.set_replay_speed_exponent(0)
359                ba.internal.fade_screen(True)
360                assert self._my_replay_selected is not None
361                ba.internal.new_replay_session(
362                    ba.internal.get_replays_dir()
363                    + '/'
364                    + self._my_replay_selected
365                )
366            except Exception:
367                ba.print_exception('Error running replay session.')
368
369                # Drop back into a fresh main menu session
370                # in case we half-launched or something.
371                from bastd import mainmenu
372
373                ba.internal.new_host_session(mainmenu.MainMenuSession)
374
375        ba.internal.fade_screen(False, endcall=ba.Call(ba.pushcall, do_it))
376        ba.containerwidget(edit=self._root_widget, transition='out_left')
377
378    def _on_my_replay_rename_press(self) -> None:
379        if self._my_replay_selected is None:
380            self._no_replay_selected_error()
381            return
382        c_width = 600
383        c_height = 250
384        uiscale = ba.app.ui.uiscale
385        self._my_replays_rename_window = cnt = ba.containerwidget(
386            scale=(
387                1.8
388                if uiscale is ba.UIScale.SMALL
389                else 1.55
390                if uiscale is ba.UIScale.MEDIUM
391                else 1.0
392            ),
393            size=(c_width, c_height),
394            transition='in_scale',
395        )
396        dname = self._get_replay_display_name(self._my_replay_selected)
397        ba.textwidget(
398            parent=cnt,
399            size=(0, 0),
400            h_align='center',
401            v_align='center',
402            text=ba.Lstr(
403                resource=self._r + '.renameReplayText',
404                subs=[('${REPLAY}', dname)],
405            ),
406            maxwidth=c_width * 0.8,
407            position=(c_width * 0.5, c_height - 60),
408        )
409        self._my_replay_rename_text = txt = ba.textwidget(
410            parent=cnt,
411            size=(c_width * 0.8, 40),
412            h_align='left',
413            v_align='center',
414            text=dname,
415            editable=True,
416            description=ba.Lstr(resource=self._r + '.replayNameText'),
417            position=(c_width * 0.1, c_height - 140),
418            autoselect=True,
419            maxwidth=c_width * 0.7,
420            max_chars=200,
421        )
422        cbtn = ba.buttonwidget(
423            parent=cnt,
424            label=ba.Lstr(resource='cancelText'),
425            on_activate_call=ba.Call(
426                lambda c: ba.containerwidget(edit=c, transition='out_scale'),
427                cnt,
428            ),
429            size=(180, 60),
430            position=(30, 30),
431            autoselect=True,
432        )
433        okb = ba.buttonwidget(
434            parent=cnt,
435            label=ba.Lstr(resource=self._r + '.renameText'),
436            size=(180, 60),
437            position=(c_width - 230, 30),
438            on_activate_call=ba.Call(
439                self._rename_my_replay, self._my_replay_selected
440            ),
441            autoselect=True,
442        )
443        ba.widget(edit=cbtn, right_widget=okb)
444        ba.widget(edit=okb, left_widget=cbtn)
445        ba.textwidget(edit=txt, on_return_press_call=okb.activate)
446        ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb)
447
448    def _rename_my_replay(self, replay: str) -> None:
449        new_name = None
450        try:
451            if not self._my_replay_rename_text:
452                return
453            new_name_raw = cast(
454                str, ba.textwidget(query=self._my_replay_rename_text)
455            )
456            new_name = new_name_raw + '.brp'
457
458            # Ignore attempts to change it to what it already is
459            # (or what it looks like to the user).
460            if (
461                replay != new_name
462                and self._get_replay_display_name(replay) != new_name_raw
463            ):
464                old_name_full = (
465                    ba.internal.get_replays_dir() + '/' + replay
466                ).encode('utf-8')
467                new_name_full = (
468                    ba.internal.get_replays_dir() + '/' + new_name
469                ).encode('utf-8')
470                # False alarm; ba.textwidget can return non-None val.
471                # pylint: disable=unsupported-membership-test
472                if os.path.exists(new_name_full):
473                    ba.playsound(ba.getsound('error'))
474                    ba.screenmessage(
475                        ba.Lstr(
476                            resource=self._r
477                            + '.replayRenameErrorAlreadyExistsText'
478                        ),
479                        color=(1, 0, 0),
480                    )
481                elif any(char in new_name_raw for char in ['/', '\\', ':']):
482                    ba.playsound(ba.getsound('error'))
483                    ba.screenmessage(
484                        ba.Lstr(
485                            resource=self._r + '.replayRenameErrorInvalidName'
486                        ),
487                        color=(1, 0, 0),
488                    )
489                else:
490                    ba.internal.increment_analytics_count('Replay rename')
491                    os.rename(old_name_full, new_name_full)
492                    self._refresh_my_replays()
493                    ba.playsound(ba.getsound('gunCocking'))
494        except Exception:
495            ba.print_exception(
496                f"Error renaming replay '{replay}' to '{new_name}'."
497            )
498            ba.playsound(ba.getsound('error'))
499            ba.screenmessage(
500                ba.Lstr(resource=self._r + '.replayRenameErrorText'),
501                color=(1, 0, 0),
502            )
503
504        ba.containerwidget(
505            edit=self._my_replays_rename_window, transition='out_scale'
506        )
507
508    def _on_my_replay_delete_press(self) -> None:
509        from bastd.ui import confirm
510
511        if self._my_replay_selected is None:
512            self._no_replay_selected_error()
513            return
514        confirm.ConfirmWindow(
515            ba.Lstr(
516                resource=self._r + '.deleteConfirmText',
517                subs=[
518                    (
519                        '${REPLAY}',
520                        self._get_replay_display_name(self._my_replay_selected),
521                    )
522                ],
523            ),
524            ba.Call(self._delete_replay, self._my_replay_selected),
525            450,
526            150,
527        )
528
529    def _get_replay_display_name(self, replay: str) -> str:
530        if replay.endswith('.brp'):
531            replay = replay[:-4]
532        if replay == '__lastReplay':
533            return ba.Lstr(resource='replayNameDefaultText').evaluate()
534        return replay
535
536    def _delete_replay(self, replay: str) -> None:
537        try:
538            ba.internal.increment_analytics_count('Replay delete')
539            os.remove(
540                (ba.internal.get_replays_dir() + '/' + replay).encode('utf-8')
541            )
542            self._refresh_my_replays()
543            ba.playsound(ba.getsound('shieldDown'))
544            if replay == self._my_replay_selected:
545                self._my_replay_selected = None
546        except Exception:
547            ba.print_exception(f"Error deleting replay '{replay}'.")
548            ba.playsound(ba.getsound('error'))
549            ba.screenmessage(
550                ba.Lstr(resource=self._r + '.replayDeleteErrorText'),
551                color=(1, 0, 0),
552            )
553
554    def _on_my_replay_select(self, replay: str) -> None:
555        self._my_replay_selected = replay
556
557    def _refresh_my_replays(self) -> None:
558        assert self._columnwidget is not None
559        for child in self._columnwidget.get_children():
560            child.delete()
561        t_scale = 1.6
562        try:
563            names = os.listdir(ba.internal.get_replays_dir())
564
565            # Ignore random other files in there.
566            names = [n for n in names if n.endswith('.brp')]
567            names.sort(key=lambda x: x.lower())
568        except Exception:
569            ba.print_exception('Error listing replays dir.')
570            names = []
571
572        assert self._my_replays_scroll_width is not None
573        assert self._my_replays_watch_replay_button is not None
574        for i, name in enumerate(names):
575            txt = ba.textwidget(
576                parent=self._columnwidget,
577                size=(self._my_replays_scroll_width / t_scale, 30),
578                selectable=True,
579                color=(1.0, 1, 0.4)
580                if name == '__lastReplay.brp'
581                else (1, 1, 1),
582                always_highlight=True,
583                on_select_call=ba.Call(self._on_my_replay_select, name),
584                on_activate_call=self._my_replays_watch_replay_button.activate,
585                text=self._get_replay_display_name(name),
586                h_align='left',
587                v_align='center',
588                corner_scale=t_scale,
589                maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93,
590            )
591            if i == 0:
592                ba.widget(
593                    edit=txt,
594                    up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button,
595                )
596
597    def _save_state(self) -> None:
598        try:
599            sel = self._root_widget.get_selected_child()
600            selected_tab_ids = [
601                tab_id
602                for tab_id, tab in self._tab_row.tabs.items()
603                if sel == tab.button
604            ]
605            if sel == self._back_button:
606                sel_name = 'Back'
607            elif selected_tab_ids:
608                assert len(selected_tab_ids) == 1
609                sel_name = f'Tab:{selected_tab_ids[0].value}'
610            elif sel == self._tab_container:
611                sel_name = 'TabContainer'
612            else:
613                raise ValueError(f'unrecognized selection {sel}')
614            ba.app.ui.window_states[type(self)] = {'sel_name': sel_name}
615        except Exception:
616            ba.print_exception(f'Error saving state for {self}.')
617
618    def _restore_state(self) -> None:
619        from efro.util import enum_by_value
620
621        try:
622            sel: ba.Widget | None
623            sel_name = ba.app.ui.window_states.get(type(self), {}).get(
624                'sel_name'
625            )
626            assert isinstance(sel_name, (str, type(None)))
627            try:
628                current_tab = enum_by_value(
629                    self.TabID, ba.app.config.get('Watch Tab')
630                )
631            except ValueError:
632                current_tab = self.TabID.MY_REPLAYS
633            self._set_tab(current_tab)
634
635            if sel_name == 'Back':
636                sel = self._back_button
637            elif sel_name == 'TabContainer':
638                sel = self._tab_container
639            elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
640                try:
641                    sel_tab_id = enum_by_value(
642                        self.TabID, sel_name.split(':')[-1]
643                    )
644                except ValueError:
645                    sel_tab_id = self.TabID.MY_REPLAYS
646                sel = self._tab_row.tabs[sel_tab_id].button
647            else:
648                if self._tab_container is not None:
649                    sel = self._tab_container
650                else:
651                    sel = self._tab_row.tabs[current_tab].button
652            ba.containerwidget(edit=self._root_widget, selected_child=sel)
653        except Exception:
654            ba.print_exception(f'Error restoring state for {self}.')
655
656    def _back(self) -> None:
657        from bastd.ui.mainmenu import MainMenuWindow
658
659        self._save_state()
660        ba.containerwidget(
661            edit=self._root_widget, transition=self._transition_out
662        )
663        ba.app.ui.set_main_menu_window(
664            MainMenuWindow(transition='in_left').get_root_widget()
665        )

Window for watching replays.

WatchWindow( transition: str | None = 'in_right', origin_widget: _ba.Widget | None = None)
 28    def __init__(
 29        self,
 30        transition: str | None = 'in_right',
 31        origin_widget: ba.Widget | None = None,
 32    ):
 33        # pylint: disable=too-many-locals
 34        # pylint: disable=too-many-statements
 35        from bastd.ui.tabs import TabRow
 36
 37        ba.set_analytics_screen('Watch Window')
 38        scale_origin: tuple[float, float] | None
 39        if origin_widget is not None:
 40            self._transition_out = 'out_scale'
 41            scale_origin = origin_widget.get_screen_space_center()
 42            transition = 'in_scale'
 43        else:
 44            self._transition_out = 'out_right'
 45            scale_origin = None
 46        ba.app.ui.set_main_menu_location('Watch')
 47        self._tab_data: dict[str, Any] = {}
 48        self._my_replays_scroll_width: float | None = None
 49        self._my_replays_watch_replay_button: ba.Widget | None = None
 50        self._scrollwidget: ba.Widget | None = None
 51        self._columnwidget: ba.Widget | None = None
 52        self._my_replay_selected: str | None = None
 53        self._my_replays_rename_window: ba.Widget | None = None
 54        self._my_replay_rename_text: ba.Widget | None = None
 55        self._r = 'watchWindow'
 56        uiscale = ba.app.ui.uiscale
 57        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
 58        x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
 59        self._height = (
 60            578
 61            if uiscale is ba.UIScale.SMALL
 62            else 670
 63            if uiscale is ba.UIScale.MEDIUM
 64            else 800
 65        )
 66        self._current_tab: WatchWindow.TabID | None = None
 67        extra_top = 20 if uiscale is ba.UIScale.SMALL else 0
 68
 69        super().__init__(
 70            root_widget=ba.containerwidget(
 71                size=(self._width, self._height + extra_top),
 72                transition=transition,
 73                toolbar_visibility='menu_minimal',
 74                scale_origin_stack_offset=scale_origin,
 75                scale=(
 76                    1.3
 77                    if uiscale is ba.UIScale.SMALL
 78                    else 0.97
 79                    if uiscale is ba.UIScale.MEDIUM
 80                    else 0.8
 81                ),
 82                stack_offset=(0, -10)
 83                if uiscale is ba.UIScale.SMALL
 84                else (0, 15)
 85                if uiscale is ba.UIScale.MEDIUM
 86                else (0, 0),
 87            )
 88        )
 89
 90        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
 91            ba.containerwidget(
 92                edit=self._root_widget, on_cancel_call=self._back
 93            )
 94            self._back_button = None
 95        else:
 96            self._back_button = btn = ba.buttonwidget(
 97                parent=self._root_widget,
 98                autoselect=True,
 99                position=(70 + x_inset, self._height - 74),
100                size=(140, 60),
101                scale=1.1,
102                label=ba.Lstr(resource='backText'),
103                button_type='back',
104                on_activate_call=self._back,
105            )
106            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
107            ba.buttonwidget(
108                edit=btn,
109                button_type='backSmall',
110                size=(60, 60),
111                label=ba.charstr(ba.SpecialChar.BACK),
112            )
113
114        ba.textwidget(
115            parent=self._root_widget,
116            position=(self._width * 0.5, self._height - 38),
117            size=(0, 0),
118            color=ba.app.ui.title_color,
119            scale=1.5,
120            h_align='center',
121            v_align='center',
122            text=ba.Lstr(resource=self._r + '.titleText'),
123            maxwidth=400,
124        )
125
126        tabdefs = [
127            (
128                self.TabID.MY_REPLAYS,
129                ba.Lstr(resource=self._r + '.myReplaysText'),
130            ),
131            # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')),
132        ]
133
134        scroll_buffer_h = 130 + 2 * x_inset
135        tab_buffer_h = 750 + 2 * x_inset
136
137        self._tab_row = TabRow(
138            self._root_widget,
139            tabdefs,
140            pos=(tab_buffer_h * 0.5, self._height - 130),
141            size=(self._width - tab_buffer_h, 50),
142            on_select_call=self._set_tab,
143        )
144
145        if ba.app.ui.use_toolbars:
146            first_tab = self._tab_row.tabs[tabdefs[0][0]]
147            last_tab = self._tab_row.tabs[tabdefs[-1][0]]
148            ba.widget(
149                edit=last_tab.button,
150                right_widget=ba.internal.get_special_widget('party_button'),
151            )
152            if uiscale is ba.UIScale.SMALL:
153                bbtn = ba.internal.get_special_widget('back_button')
154                ba.widget(
155                    edit=first_tab.button, up_widget=bbtn, left_widget=bbtn
156                )
157
158        self._scroll_width = self._width - scroll_buffer_h
159        self._scroll_height = self._height - 180
160
161        # Not actually using a scroll widget anymore; just an image.
162        scroll_left = (self._width - self._scroll_width) * 0.5
163        scroll_bottom = self._height - self._scroll_height - 79 - 48
164        buffer_h = 10
165        buffer_v = 4
166        ba.imagewidget(
167            parent=self._root_widget,
168            position=(scroll_left - buffer_h, scroll_bottom - buffer_v),
169            size=(
170                self._scroll_width + 2 * buffer_h,
171                self._scroll_height + 2 * buffer_v,
172            ),
173            texture=ba.gettexture('scrollWidget'),
174            model_transparent=ba.getmodel('softEdgeOutside'),
175        )
176        self._tab_container: ba.Widget | None = None
177
178        self._restore_state()
Inherited Members
ba.ui.Window
get_root_widget
class WatchWindow.TabID(enum.Enum):
22    class TabID(Enum):
23        """Our available tab types."""
24
25        MY_REPLAYS = 'my_replays'
26        TEST_TAB = 'test_tab'

Our available tab types.

MY_REPLAYS = <TabID.MY_REPLAYS: 'my_replays'>
TEST_TAB = <TabID.TEST_TAB: 'test_tab'>
Inherited Members
enum.Enum
name
value