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

Window for watching replays.

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