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

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        from bauiv1lib.tabs import TabRow
 36
 37        bui.set_analytics_screen('Watch Window')
 38        self._tab_data: dict[str, Any] = {}
 39        self._my_replays_scroll_width: float | None = None
 40        self._my_replays_watch_replay_button: bui.Widget | None = None
 41        self._scrollwidget: bui.Widget | None = None
 42        self._columnwidget: bui.Widget | None = None
 43        self._my_replay_selected: str | None = None
 44        self._my_replays_rename_window: bui.Widget | None = None
 45        self._my_replay_rename_text: bui.Widget | None = None
 46        self._r = 'watchWindow'
 47        uiscale = bui.app.ui_v1.uiscale
 48        self._width = 1440 if uiscale is bui.UIScale.SMALL else 1040
 49        x_inset = 200 if uiscale is bui.UIScale.SMALL else 0
 50        self._height = (
 51            570
 52            if uiscale is bui.UIScale.SMALL
 53            else 670 if uiscale is bui.UIScale.MEDIUM else 800
 54        )
 55        self._current_tab: WatchWindow.TabID | None = None
 56        extra_top = 20 if uiscale is bui.UIScale.SMALL else 0
 57
 58        super().__init__(
 59            root_widget=bui.containerwidget(
 60                size=(self._width, self._height + extra_top),
 61                toolbar_visibility=(
 62                    'menu_minimal'
 63                    if uiscale is bui.UIScale.SMALL
 64                    else 'menu_full'
 65                ),
 66                scale=(
 67                    1.32
 68                    if uiscale is bui.UIScale.SMALL
 69                    else 0.85 if uiscale is bui.UIScale.MEDIUM else 0.65
 70                ),
 71                stack_offset=(
 72                    (0, 30)
 73                    if uiscale is bui.UIScale.SMALL
 74                    else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0)
 75                ),
 76            ),
 77            transition=transition,
 78            origin_widget=origin_widget,
 79        )
 80
 81        if uiscale is bui.UIScale.SMALL:
 82            bui.containerwidget(
 83                edit=self._root_widget, on_cancel_call=self.main_window_back
 84            )
 85            self._back_button = None
 86        else:
 87            self._back_button = btn = bui.buttonwidget(
 88                parent=self._root_widget,
 89                autoselect=True,
 90                position=(70 + x_inset, self._height - 74),
 91                size=(60, 60),
 92                scale=1.1,
 93                label=bui.charstr(bui.SpecialChar.BACK),
 94                button_type='backSmall',
 95                on_activate_call=self.main_window_back,
 96            )
 97            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 98
 99        bui.textwidget(
100            parent=self._root_widget,
101            position=(
102                self._width * 0.5,
103                self._height - (65 if uiscale is bui.UIScale.SMALL else 38),
104            ),
105            size=(0, 0),
106            color=bui.app.ui_v1.title_color,
107            scale=0.7 if uiscale is bui.UIScale.SMALL else 1.5,
108            h_align='center',
109            v_align='center',
110            text=(
111                ''
112                if uiscale is bui.UIScale.SMALL
113                else bui.Lstr(resource=f'{self._r}.titleText')
114            ),
115            maxwidth=400,
116        )
117
118        tabdefs = [
119            (
120                self.TabID.MY_REPLAYS,
121                bui.Lstr(resource=f'{self._r}.myReplaysText'),
122            ),
123            # (self.TabID.TEST_TAB, bui.Lstr(value='Testing')),
124        ]
125
126        scroll_buffer_h = 130 + 2 * x_inset
127        tab_buffer_h = 750 + 2 * x_inset
128
129        self._tab_row = TabRow(
130            self._root_widget,
131            tabdefs,
132            pos=(tab_buffer_h * 0.5, self._height - 130),
133            size=(self._width - tab_buffer_h, 50),
134            on_select_call=self._set_tab,
135        )
136
137        first_tab = self._tab_row.tabs[tabdefs[0][0]]
138        last_tab = self._tab_row.tabs[tabdefs[-1][0]]
139        bui.widget(
140            edit=last_tab.button,
141            right_widget=bui.get_special_widget('squad_button'),
142        )
143        if uiscale is bui.UIScale.SMALL:
144            bbtn = bui.get_special_widget('back_button')
145            bui.widget(edit=first_tab.button, up_widget=bbtn, left_widget=bbtn)
146
147        self._scroll_width = self._width - scroll_buffer_h
148        self._scroll_height = self._height - 180
149
150        # Not actually using a scroll widget anymore; just an image.
151        scroll_left = (self._width - self._scroll_width) * 0.5
152        scroll_bottom = self._height - self._scroll_height - 79 - 48
153        buffer_h = 10
154        buffer_v = 4
155        bui.imagewidget(
156            parent=self._root_widget,
157            position=(scroll_left - buffer_h, scroll_bottom - buffer_v),
158            size=(
159                self._scroll_width + 2 * buffer_h,
160                self._scroll_height + 2 * buffer_v,
161            ),
162            texture=bui.gettexture('scrollWidget'),
163            mesh_transparent=bui.getmesh('softEdgeOutside'),
164        )
165        self._tab_container: bui.Widget | None = None
166
167        self._restore_state()

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
169    @override
170    def get_main_window_state(self) -> bui.MainWindowState:
171        # Support recreating our window for back/refresh purposes.
172        cls = type(self)
173        return bui.BasicMainWindowState(
174            create_call=lambda transition, origin_widget: cls(
175                transition=transition, origin_widget=origin_widget
176            )
177        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
179    @override
180    def on_main_window_close(self) -> None:
181        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

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'>