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

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
185    @override
186    def on_main_window_close(self) -> None:
187        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_close
can_change_main_window
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget
class WatchWindow.TabID(enum.Enum):
23    class TabID(Enum):
24        """Our available tab types."""
25
26        MY_REPLAYS = 'my_replays'
27        TEST_TAB = 'test_tab'

Our available tab types.

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