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

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
180    @override
181    def on_main_window_close(self) -> None:
182        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'>