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

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 if uiscale is bui.UIScale.MEDIUM else 800
 65        )
 66        self._current_tab: WatchWindow.TabID | None = None
 67        extra_top = 20 if uiscale is bui.UIScale.SMALL else 0
 68
 69        super().__init__(
 70            root_widget=bui.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 bui.UIScale.SMALL
 78                    else 0.97 if uiscale is bui.UIScale.MEDIUM else 0.8
 79                ),
 80                stack_offset=(
 81                    (0, -10)
 82                    if uiscale is bui.UIScale.SMALL
 83                    else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0)
 84                ),
 85            )
 86        )
 87
 88        if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars:
 89            bui.containerwidget(
 90                edit=self._root_widget, on_cancel_call=self._back
 91            )
 92            self._back_button = None
 93        else:
 94            self._back_button = btn = bui.buttonwidget(
 95                parent=self._root_widget,
 96                autoselect=True,
 97                position=(70 + x_inset, self._height - 74),
 98                size=(140, 60),
 99                scale=1.1,
100                label=bui.Lstr(resource='backText'),
101                button_type='back',
102                on_activate_call=self._back,
103            )
104            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
105            bui.buttonwidget(
106                edit=btn,
107                button_type='backSmall',
108                size=(60, 60),
109                label=bui.charstr(bui.SpecialChar.BACK),
110            )
111
112        bui.textwidget(
113            parent=self._root_widget,
114            position=(self._width * 0.5, self._height - 38),
115            size=(0, 0),
116            color=bui.app.ui_v1.title_color,
117            scale=1.5,
118            h_align='center',
119            v_align='center',
120            text=bui.Lstr(resource=self._r + '.titleText'),
121            maxwidth=400,
122        )
123
124        tabdefs = [
125            (
126                self.TabID.MY_REPLAYS,
127                bui.Lstr(resource=self._r + '.myReplaysText'),
128            ),
129            # (self.TabID.TEST_TAB, bui.Lstr(value='Testing')),
130        ]
131
132        scroll_buffer_h = 130 + 2 * x_inset
133        tab_buffer_h = 750 + 2 * x_inset
134
135        self._tab_row = TabRow(
136            self._root_widget,
137            tabdefs,
138            pos=(tab_buffer_h * 0.5, self._height - 130),
139            size=(self._width - tab_buffer_h, 50),
140            on_select_call=self._set_tab,
141        )
142
143        if bui.app.ui_v1.use_toolbars:
144            first_tab = self._tab_row.tabs[tabdefs[0][0]]
145            last_tab = self._tab_row.tabs[tabdefs[-1][0]]
146            bui.widget(
147                edit=last_tab.button,
148                right_widget=bui.get_special_widget('party_button'),
149            )
150            if uiscale is bui.UIScale.SMALL:
151                bbtn = bui.get_special_widget('back_button')
152                bui.widget(
153                    edit=first_tab.button, up_widget=bbtn, left_widget=bbtn
154                )
155
156        self._scroll_width = self._width - scroll_buffer_h
157        self._scroll_height = self._height - 180
158
159        # Not actually using a scroll widget anymore; just an image.
160        scroll_left = (self._width - self._scroll_width) * 0.5
161        scroll_bottom = self._height - self._scroll_height - 79 - 48
162        buffer_h = 10
163        buffer_v = 4
164        bui.imagewidget(
165            parent=self._root_widget,
166            position=(scroll_left - buffer_h, scroll_bottom - buffer_v),
167            size=(
168                self._scroll_width + 2 * buffer_h,
169                self._scroll_height + 2 * buffer_v,
170            ),
171            texture=bui.gettexture('scrollWidget'),
172            mesh_transparent=bui.getmesh('softEdgeOutside'),
173        )
174        self._tab_container: bui.Widget | None = None
175
176        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