bauiv1lib.watch

Provides UI functionality for watching replays.

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

Window for watching replays.

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

Create a MainWindow given a root widget and transition info.

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

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

Return a WindowState to recreate this window, if supported.

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

Called before transitioning out a main window.

A good opportunity to save window state/etc.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_is_top_level
main_window_close
main_window_has_control
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