bauiv1lib.playlist.browser

Provides a window for browsing and launching game playlists.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides a window for browsing and launching game playlists."""
  4
  5from __future__ import annotations
  6
  7import copy
  8import math
  9import logging
 10from typing import override, TYPE_CHECKING
 11
 12import bascenev1 as bs
 13import bauiv1 as bui
 14
 15if TYPE_CHECKING:
 16    from bauiv1lib.play import PlaylistSelectContext
 17
 18
 19class PlaylistBrowserWindow(bui.MainWindow):
 20    """Window for starting teams games."""
 21
 22    def __init__(
 23        self,
 24        sessiontype: type[bs.Session],
 25        transition: str | None = 'in_right',
 26        origin_widget: bui.Widget | None = None,
 27        playlist_select_context: PlaylistSelectContext | None = None,
 28    ):
 29        # pylint: disable=cyclic-import
 30        from bauiv1lib.playlist import PlaylistTypeVars
 31
 32        # Store state for when we exit the next game.
 33        if issubclass(sessiontype, bs.DualTeamSession):
 34            bui.set_analytics_screen('Teams Window')
 35        elif issubclass(sessiontype, bs.FreeForAllSession):
 36            bui.set_analytics_screen('FreeForAll Window')
 37        else:
 38            raise TypeError(f'Invalid sessiontype: {sessiontype}.')
 39        self._pvars = PlaylistTypeVars(sessiontype)
 40
 41        self._sessiontype = sessiontype
 42
 43        self._customize_button: bui.Widget | None = None
 44        self._sub_width: float | None = None
 45        self._sub_height: float | None = None
 46        self._playlist_select_context = playlist_select_context
 47
 48        self._ensure_standard_playlists_exist()
 49
 50        # Get the current selection (if any).
 51        self._selected_playlist = bui.app.config.get(
 52            self._pvars.config_name + ' Playlist Selection'
 53        )
 54
 55        uiscale = bui.app.ui_v1.uiscale
 56        self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0
 57        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 58        self._height = (
 59            440
 60            if uiscale is bui.UIScale.SMALL
 61            else 510 if uiscale is bui.UIScale.MEDIUM else 580
 62        )
 63
 64        top_extra = 30 if uiscale is bui.UIScale.SMALL else 0
 65
 66        super().__init__(
 67            root_widget=bui.containerwidget(
 68                size=(self._width, self._height + top_extra),
 69                toolbar_visibility=(
 70                    'menu_minimal'
 71                    if (
 72                        uiscale is bui.UIScale.SMALL
 73                        or playlist_select_context is not None
 74                    )
 75                    else 'menu_full'
 76                ),
 77                scale=(
 78                    1.83
 79                    if uiscale is bui.UIScale.SMALL
 80                    else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9
 81                ),
 82                stack_offset=(
 83                    (0, -46) if uiscale is bui.UIScale.SMALL else (0, 0)
 84                ),
 85            ),
 86            transition=transition,
 87            origin_widget=origin_widget,
 88        )
 89
 90        self._back_button: bui.Widget | None = bui.buttonwidget(
 91            parent=self._root_widget,
 92            position=(59 + x_inset, self._height - 70),
 93            size=(120, 60),
 94            scale=1.0,
 95            on_activate_call=self._on_back_press,
 96            autoselect=True,
 97            label=bui.Lstr(resource='backText'),
 98            button_type='back',
 99        )
100        bui.containerwidget(
101            edit=self._root_widget, cancel_button=self._back_button
102        )
103        self._title_text = bui.textwidget(
104            parent=self._root_widget,
105            position=(
106                self._width * 0.5,
107                self._height - (32 if uiscale is bui.UIScale.SMALL else 41),
108            ),
109            size=(0, 0),
110            text=self._pvars.window_title_name,
111            scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3),
112            res_scale=1.5,
113            color=bui.app.ui_v1.heading_color,
114            h_align='center',
115            v_align='center',
116        )
117
118        bui.buttonwidget(
119            edit=self._back_button,
120            button_type='backSmall',
121            size=(60, 54),
122            position=(59 + x_inset, self._height - 67),
123            label=bui.charstr(bui.SpecialChar.BACK),
124        )
125
126        if uiscale is bui.UIScale.SMALL:
127            self._back_button.delete()
128            self._back_button = None
129            bui.containerwidget(
130                edit=self._root_widget, on_cancel_call=self._on_back_press
131            )
132            scroll_offs = 33
133        else:
134            scroll_offs = 0
135        self._scroll_width = self._width - (100 + 2 * x_inset)
136        self._scroll_height = self._height - (
137            146 if uiscale is bui.UIScale.SMALL else 136
138        )
139        self._scrollwidget = bui.scrollwidget(
140            parent=self._root_widget,
141            highlight=False,
142            size=(self._scroll_width, self._scroll_height),
143            position=(
144                (self._width - self._scroll_width) * 0.5,
145                65 + scroll_offs,
146            ),
147        )
148        bui.containerwidget(edit=self._scrollwidget, claims_left_right=True)
149        self._subcontainer: bui.Widget | None = None
150        self._config_name_full = self._pvars.config_name + ' Playlists'
151        self._last_config = None
152
153        # Update now and once per second (this should do our initial
154        # refresh).
155        self._update()
156        self._update_timer = bui.AppTimer(
157            1.0, bui.WeakCall(self._update), repeat=True
158        )
159
160    @override
161    def get_main_window_state(self) -> bui.MainWindowState:
162        # Support recreating our window for back/refresh purposes.
163        cls = type(self)
164
165        # Pull things out of self here; if we do it below in the lambda
166        # then we keep self alive.
167        sessiontype = self._sessiontype
168
169        # Pull anything out of self here; if we do it in the lambda
170        # we'll inadvertanly keep self alive.
171        playlist_select_context = self._playlist_select_context
172
173        return bui.BasicMainWindowState(
174            create_call=lambda transition, origin_widget: cls(
175                transition=transition,
176                origin_widget=origin_widget,
177                sessiontype=sessiontype,
178                playlist_select_context=playlist_select_context,
179            )
180        )
181
182    @override
183    def on_main_window_close(self) -> None:
184        self._save_state()
185
186    def _ensure_standard_playlists_exist(self) -> None:
187        plus = bui.app.plus
188        assert plus is not None
189
190        # On new installations, go ahead and create a few playlists
191        # besides the hard-coded default one:
192        if not plus.get_v1_account_misc_val('madeStandardPlaylists', False):
193            plus.add_v1_account_transaction(
194                {
195                    'type': 'ADD_PLAYLIST',
196                    'playlistType': 'Free-for-All',
197                    'playlistName': bui.Lstr(
198                        resource='singleGamePlaylistNameText'
199                    )
200                    .evaluate()
201                    .replace(
202                        '${GAME}',
203                        bui.Lstr(
204                            translate=('gameNames', 'Death Match')
205                        ).evaluate(),
206                    ),
207                    'playlist': [
208                        {
209                            'type': 'bs_death_match.DeathMatchGame',
210                            'settings': {
211                                'Epic Mode': False,
212                                'Kills to Win Per Player': 10,
213                                'Respawn Times': 1.0,
214                                'Time Limit': 300,
215                                'map': 'Doom Shroom',
216                            },
217                        },
218                        {
219                            'type': 'bs_death_match.DeathMatchGame',
220                            'settings': {
221                                'Epic Mode': False,
222                                'Kills to Win Per Player': 10,
223                                'Respawn Times': 1.0,
224                                'Time Limit': 300,
225                                'map': 'Crag Castle',
226                            },
227                        },
228                    ],
229                }
230            )
231            plus.add_v1_account_transaction(
232                {
233                    'type': 'ADD_PLAYLIST',
234                    'playlistType': 'Team Tournament',
235                    'playlistName': bui.Lstr(
236                        resource='singleGamePlaylistNameText'
237                    )
238                    .evaluate()
239                    .replace(
240                        '${GAME}',
241                        bui.Lstr(
242                            translate=('gameNames', 'Capture the Flag')
243                        ).evaluate(),
244                    ),
245                    'playlist': [
246                        {
247                            'type': 'bs_capture_the_flag.CTFGame',
248                            'settings': {
249                                'map': 'Bridgit',
250                                'Score to Win': 3,
251                                'Flag Idle Return Time': 30,
252                                'Flag Touch Return Time': 0,
253                                'Respawn Times': 1.0,
254                                'Time Limit': 600,
255                                'Epic Mode': False,
256                            },
257                        },
258                        {
259                            'type': 'bs_capture_the_flag.CTFGame',
260                            'settings': {
261                                'map': 'Roundabout',
262                                'Score to Win': 2,
263                                'Flag Idle Return Time': 30,
264                                'Flag Touch Return Time': 0,
265                                'Respawn Times': 1.0,
266                                'Time Limit': 600,
267                                'Epic Mode': False,
268                            },
269                        },
270                        {
271                            'type': 'bs_capture_the_flag.CTFGame',
272                            'settings': {
273                                'map': 'Tip Top',
274                                'Score to Win': 2,
275                                'Flag Idle Return Time': 30,
276                                'Flag Touch Return Time': 3,
277                                'Respawn Times': 1.0,
278                                'Time Limit': 300,
279                                'Epic Mode': False,
280                            },
281                        },
282                    ],
283                }
284            )
285            plus.add_v1_account_transaction(
286                {
287                    'type': 'ADD_PLAYLIST',
288                    'playlistType': 'Team Tournament',
289                    'playlistName': bui.Lstr(
290                        translate=('playlistNames', 'Just Sports')
291                    ).evaluate(),
292                    'playlist': [
293                        {
294                            'type': 'bs_hockey.HockeyGame',
295                            'settings': {
296                                'Time Limit': 0,
297                                'map': 'Hockey Stadium',
298                                'Score to Win': 1,
299                                'Respawn Times': 1.0,
300                            },
301                        },
302                        {
303                            'type': 'bs_football.FootballTeamGame',
304                            'settings': {
305                                'Time Limit': 0,
306                                'map': 'Football Stadium',
307                                'Score to Win': 21,
308                                'Respawn Times': 1.0,
309                            },
310                        },
311                    ],
312                }
313            )
314            plus.add_v1_account_transaction(
315                {
316                    'type': 'ADD_PLAYLIST',
317                    'playlistType': 'Free-for-All',
318                    'playlistName': bui.Lstr(
319                        translate=('playlistNames', 'Just Epic')
320                    ).evaluate(),
321                    'playlist': [
322                        {
323                            'type': 'bs_elimination.EliminationGame',
324                            'settings': {
325                                'Time Limit': 120,
326                                'map': 'Tip Top',
327                                'Respawn Times': 1.0,
328                                'Lives Per Player': 1,
329                                'Epic Mode': 1,
330                            },
331                        }
332                    ],
333                }
334            )
335            plus.add_v1_account_transaction(
336                {
337                    'type': 'SET_MISC_VAL',
338                    'name': 'madeStandardPlaylists',
339                    'value': True,
340                }
341            )
342            plus.run_v1_account_transactions()
343
344    def _refresh(self) -> None:
345        # FIXME: Should tidy this up.
346        # pylint: disable=too-many-statements
347        # pylint: disable=too-many-branches
348        # pylint: disable=too-many-locals
349        # pylint: disable=too-many-nested-blocks
350        from efro.util import asserttype
351        from bascenev1 import get_map_class, filter_playlist
352
353        if not self._root_widget:
354            return
355        if self._subcontainer is not None:
356            self._save_state()
357            self._subcontainer.delete()
358
359        # Make sure config exists.
360        if self._config_name_full not in bui.app.config:
361            bui.app.config[self._config_name_full] = {}
362
363        items = list(bui.app.config[self._config_name_full].items())
364
365        # Make sure everything is unicode.
366        items = [
367            (i[0].decode(), i[1]) if not isinstance(i[0], str) else i
368            for i in items
369        ]
370
371        items.sort(key=lambda x2: asserttype(x2[0], str).lower())
372        items = [['__default__', None]] + items  # default is always first
373
374        count = len(items)
375        columns = 3
376        rows = int(math.ceil(float(count) / columns))
377        button_width = 230
378        button_height = 230
379        button_buffer_h = -3
380        button_buffer_v = 0
381
382        self._sub_width = self._scroll_width
383        self._sub_height = (
384            40.0 + rows * (button_height + 2 * button_buffer_v) + 90
385        )
386        assert self._sub_width is not None
387        assert self._sub_height is not None
388        self._subcontainer = bui.containerwidget(
389            parent=self._scrollwidget,
390            size=(self._sub_width, self._sub_height),
391            background=False,
392        )
393
394        children = self._subcontainer.get_children()
395        for child in children:
396            child.delete()
397
398        assert bui.app.classic is not None
399        bui.textwidget(
400            parent=self._subcontainer,
401            text=bui.Lstr(resource='playlistsText'),
402            position=(40, self._sub_height - 26),
403            size=(0, 0),
404            scale=1.0,
405            maxwidth=400,
406            color=bui.app.ui_v1.title_color,
407            h_align='left',
408            v_align='center',
409        )
410
411        index = 0
412        appconfig = bui.app.config
413
414        mesh_opaque = bui.getmesh('level_select_button_opaque')
415        mesh_transparent = bui.getmesh('level_select_button_transparent')
416        mask_tex = bui.gettexture('mapPreviewMask')
417
418        h_offs = 225 if count == 1 else 115 if count == 2 else 0
419        h_offs_bottom = 0
420
421        uiscale = bui.app.ui_v1.uiscale
422        for y in range(rows):
423            for x in range(columns):
424                name = items[index][0]
425                assert name is not None
426                pos = (
427                    x * (button_width + 2 * button_buffer_h)
428                    + button_buffer_h
429                    + 8
430                    + h_offs,
431                    self._sub_height
432                    - 47
433                    - (y + 1) * (button_height + 2 * button_buffer_v),
434                )
435                btn = bui.buttonwidget(
436                    parent=self._subcontainer,
437                    button_type='square',
438                    size=(button_width, button_height),
439                    autoselect=True,
440                    label='',
441                    position=pos,
442                )
443
444                if x == 0 and uiscale is bui.UIScale.SMALL:
445                    bui.widget(
446                        edit=btn,
447                        left_widget=bui.get_special_widget('back_button'),
448                    )
449                if x == columns - 1 and uiscale is bui.UIScale.SMALL:
450                    bui.widget(
451                        edit=btn,
452                        right_widget=bui.get_special_widget('squad_button'),
453                    )
454                bui.buttonwidget(
455                    edit=btn,
456                    on_activate_call=bui.Call(
457                        self._on_playlist_press, btn, name
458                    ),
459                    on_select_call=bui.Call(self._on_playlist_select, name),
460                )
461                bui.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50)
462
463                if self._selected_playlist == name:
464                    bui.containerwidget(
465                        edit=self._subcontainer,
466                        selected_child=btn,
467                        visible_child=btn,
468                    )
469
470                if self._back_button is not None:
471                    if y == 0:
472                        bui.widget(edit=btn, up_widget=self._back_button)
473                    if x == 0:
474                        bui.widget(edit=btn, left_widget=self._back_button)
475
476                print_name: str | bui.Lstr | None
477                if name == '__default__':
478                    print_name = self._pvars.default_list_name
479                else:
480                    print_name = name
481                bui.textwidget(
482                    parent=self._subcontainer,
483                    text=print_name,
484                    position=(
485                        pos[0] + button_width * 0.5,
486                        pos[1] + button_height * 0.79,
487                    ),
488                    size=(0, 0),
489                    scale=button_width * 0.003,
490                    maxwidth=button_width * 0.7,
491                    draw_controller=btn,
492                    h_align='center',
493                    v_align='center',
494                )
495
496                # Poke into this playlist and see if we can display some of
497                # its maps.
498                map_images = []
499                try:
500                    map_textures = []
501                    map_texture_entries = []
502                    if name == '__default__':
503                        playlist = self._pvars.get_default_list_call()
504                    else:
505                        if (
506                            name
507                            not in appconfig[
508                                self._pvars.config_name + ' Playlists'
509                            ]
510                        ):
511                            print(
512                                'NOT FOUND ERR',
513                                appconfig[
514                                    self._pvars.config_name + ' Playlists'
515                                ],
516                            )
517                        playlist = appconfig[
518                            self._pvars.config_name + ' Playlists'
519                        ][name]
520                    playlist = filter_playlist(
521                        playlist,
522                        self._sessiontype,
523                        remove_unowned=False,
524                        mark_unowned=True,
525                        name=name,
526                    )
527                    for entry in playlist:
528                        mapname = entry['settings']['map']
529                        maptype: type[bs.Map] | None
530                        try:
531                            maptype = get_map_class(mapname)
532                        except bui.NotFoundError:
533                            maptype = None
534                        if maptype is not None:
535                            tex_name = maptype.get_preview_texture_name()
536                            if tex_name is not None:
537                                map_textures.append(tex_name)
538                                map_texture_entries.append(entry)
539                        if len(map_textures) >= 6:
540                            break
541
542                    if len(map_textures) > 4:
543                        img_rows = 3
544                        img_columns = 2
545                        scl = 0.33
546                        h_offs_img = 30
547                        v_offs_img = 126
548                    elif len(map_textures) > 2:
549                        img_rows = 2
550                        img_columns = 2
551                        scl = 0.35
552                        h_offs_img = 24
553                        v_offs_img = 110
554                    elif len(map_textures) > 1:
555                        img_rows = 2
556                        img_columns = 1
557                        scl = 0.5
558                        h_offs_img = 47
559                        v_offs_img = 105
560                    else:
561                        img_rows = 1
562                        img_columns = 1
563                        scl = 0.75
564                        h_offs_img = 20
565                        v_offs_img = 65
566
567                    v = None
568                    for row in range(img_rows):
569                        for col in range(img_columns):
570                            tex_index = row * img_columns + col
571                            if tex_index < len(map_textures):
572                                entry = map_texture_entries[tex_index]
573
574                                owned = not (
575                                    (
576                                        'is_unowned_map' in entry
577                                        and entry['is_unowned_map']
578                                    )
579                                    or (
580                                        'is_unowned_game' in entry
581                                        and entry['is_unowned_game']
582                                    )
583                                )
584
585                                tex_name = map_textures[tex_index]
586                                h = pos[0] + h_offs_img + scl * 250 * col
587                                v = pos[1] + v_offs_img - scl * 130 * row
588                                map_images.append(
589                                    bui.imagewidget(
590                                        parent=self._subcontainer,
591                                        size=(scl * 250.0, scl * 125.0),
592                                        position=(h, v),
593                                        texture=bui.gettexture(tex_name),
594                                        opacity=1.0 if owned else 0.25,
595                                        draw_controller=btn,
596                                        mesh_opaque=mesh_opaque,
597                                        mesh_transparent=mesh_transparent,
598                                        mask_texture=mask_tex,
599                                    )
600                                )
601                                if not owned:
602                                    bui.imagewidget(
603                                        parent=self._subcontainer,
604                                        size=(scl * 100.0, scl * 100.0),
605                                        position=(h + scl * 75, v + scl * 10),
606                                        texture=bui.gettexture('lock'),
607                                        draw_controller=btn,
608                                    )
609                        if v is not None:
610                            v -= scl * 130.0
611
612                except Exception:
613                    logging.exception('Error listing playlist maps.')
614
615                if not map_images:
616                    bui.textwidget(
617                        parent=self._subcontainer,
618                        text='???',
619                        scale=1.5,
620                        size=(0, 0),
621                        color=(1, 1, 1, 0.5),
622                        h_align='center',
623                        v_align='center',
624                        draw_controller=btn,
625                        position=(
626                            pos[0] + button_width * 0.5,
627                            pos[1] + button_height * 0.5,
628                        ),
629                    )
630
631                index += 1
632
633                if index >= count:
634                    break
635            if index >= count:
636                break
637        self._customize_button = btn = bui.buttonwidget(
638            parent=self._subcontainer,
639            size=(100, 30),
640            position=(34 + h_offs_bottom, 50),
641            text_scale=0.6,
642            label=bui.Lstr(resource='customizeText'),
643            on_activate_call=self._on_customize_press,
644            color=(0.54, 0.52, 0.67),
645            textcolor=(0.7, 0.65, 0.7),
646            autoselect=True,
647        )
648        bui.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28)
649        self._restore_state()
650
651    def on_play_options_window_run_game(self) -> None:
652        """(internal)"""
653
654        # No-op if we're not in control.
655        if not self.main_window_has_control():
656            # if not self._root_widget:
657            return
658
659        if self._playlist_select_context is not None:
660            # Done doing a playlist selection; now back all the way out
661            # of our selection windows to our stored starting point.
662            if self._playlist_select_context.back_state is None:
663                logging.error(
664                    'No back state found'
665                    ' after playlist select context completion.'
666                )
667            else:
668                self.main_window_back_state = (
669                    self._playlist_select_context.back_state
670                )
671            self.main_window_back()
672        else:
673            # Launching a regular game session; simply get our window
674            # transitioning out.
675            self.main_window_close(transition='out_left')
676
677    def _on_playlist_select(self, playlist_name: str) -> None:
678        self._selected_playlist = playlist_name
679
680    def _update(self) -> None:
681        # Make sure config exists.
682        if self._config_name_full not in bui.app.config:
683            bui.app.config[self._config_name_full] = {}
684
685        cfg = bui.app.config[self._config_name_full]
686        if cfg != self._last_config:
687            self._last_config = copy.deepcopy(cfg)
688            self._refresh()
689
690    def _on_playlist_press(
691        self, button: bui.Widget, playlist_name: str
692    ) -> None:
693        # pylint: disable=cyclic-import
694        from bauiv1lib.playoptions import PlayOptionsWindow
695
696        # Make sure the target playlist still exists.
697        exists = (
698            playlist_name == '__default__'
699            or playlist_name in bui.app.config.get(self._config_name_full, {})
700        )
701        if not exists:
702            return
703
704        self._save_state()
705        PlayOptionsWindow(
706            sessiontype=self._sessiontype,
707            scale_origin=button.get_screen_space_center(),
708            playlist=playlist_name,
709            delegate=self,
710            playlist_select_context=self._playlist_select_context,
711        )
712
713    def _on_customize_press(self) -> None:
714        # pylint: disable=cyclic-import
715        from bauiv1lib.playlist.customizebrowser import (
716            PlaylistCustomizeBrowserWindow,
717        )
718
719        # no-op if we're not in control.
720        if not self.main_window_has_control():
721            return
722
723        self.main_window_replace(
724            PlaylistCustomizeBrowserWindow(
725                origin_widget=self._customize_button,
726                sessiontype=self._sessiontype,
727            )
728        )
729
730    def _on_back_press(self) -> None:
731        # pylint: disable=cyclic-import
732        # from bauiv1lib.play import PlayWindow
733
734        # no-op if we're not in control.
735        if not self.main_window_has_control():
736            return
737
738        # Store our selected playlist if that's changed.
739        if self._selected_playlist is not None:
740            prev_sel = bui.app.config.get(
741                self._pvars.config_name + ' Playlist Selection'
742            )
743            if self._selected_playlist != prev_sel:
744                cfg = bui.app.config
745                cfg[self._pvars.config_name + ' Playlist Selection'] = (
746                    self._selected_playlist
747                )
748                cfg.commit()
749
750        self.main_window_back()
751
752    def _save_state(self) -> None:
753        try:
754            sel = self._root_widget.get_selected_child()
755            if sel == self._back_button:
756                sel_name = 'Back'
757            elif sel == self._scrollwidget:
758                assert self._subcontainer is not None
759                subsel = self._subcontainer.get_selected_child()
760                if subsel == self._customize_button:
761                    sel_name = 'Customize'
762                else:
763                    sel_name = 'Scroll'
764            else:
765                raise RuntimeError('Unrecognized selected widget.')
766            assert bui.app.classic is not None
767            bui.app.ui_v1.window_states[type(self)] = sel_name
768        except Exception:
769            logging.exception('Error saving state for %s.', self)
770
771    def _restore_state(self) -> None:
772        try:
773            assert bui.app.classic is not None
774            sel_name = bui.app.ui_v1.window_states.get(type(self))
775            if sel_name == 'Back':
776                sel = self._back_button
777            elif sel_name == 'Scroll':
778                sel = self._scrollwidget
779            elif sel_name == 'Customize':
780                sel = self._scrollwidget
781                bui.containerwidget(
782                    edit=self._subcontainer,
783                    selected_child=self._customize_button,
784                    visible_child=self._customize_button,
785                )
786            else:
787                sel = self._scrollwidget
788            bui.containerwidget(edit=self._root_widget, selected_child=sel)
789        except Exception:
790            logging.exception('Error restoring state for %s.', self)
class PlaylistBrowserWindow(bauiv1._uitypes.MainWindow):
 20class PlaylistBrowserWindow(bui.MainWindow):
 21    """Window for starting teams games."""
 22
 23    def __init__(
 24        self,
 25        sessiontype: type[bs.Session],
 26        transition: str | None = 'in_right',
 27        origin_widget: bui.Widget | None = None,
 28        playlist_select_context: PlaylistSelectContext | None = None,
 29    ):
 30        # pylint: disable=cyclic-import
 31        from bauiv1lib.playlist import PlaylistTypeVars
 32
 33        # Store state for when we exit the next game.
 34        if issubclass(sessiontype, bs.DualTeamSession):
 35            bui.set_analytics_screen('Teams Window')
 36        elif issubclass(sessiontype, bs.FreeForAllSession):
 37            bui.set_analytics_screen('FreeForAll Window')
 38        else:
 39            raise TypeError(f'Invalid sessiontype: {sessiontype}.')
 40        self._pvars = PlaylistTypeVars(sessiontype)
 41
 42        self._sessiontype = sessiontype
 43
 44        self._customize_button: bui.Widget | None = None
 45        self._sub_width: float | None = None
 46        self._sub_height: float | None = None
 47        self._playlist_select_context = playlist_select_context
 48
 49        self._ensure_standard_playlists_exist()
 50
 51        # Get the current selection (if any).
 52        self._selected_playlist = bui.app.config.get(
 53            self._pvars.config_name + ' Playlist Selection'
 54        )
 55
 56        uiscale = bui.app.ui_v1.uiscale
 57        self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0
 58        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 59        self._height = (
 60            440
 61            if uiscale is bui.UIScale.SMALL
 62            else 510 if uiscale is bui.UIScale.MEDIUM else 580
 63        )
 64
 65        top_extra = 30 if uiscale is bui.UIScale.SMALL else 0
 66
 67        super().__init__(
 68            root_widget=bui.containerwidget(
 69                size=(self._width, self._height + top_extra),
 70                toolbar_visibility=(
 71                    'menu_minimal'
 72                    if (
 73                        uiscale is bui.UIScale.SMALL
 74                        or playlist_select_context is not None
 75                    )
 76                    else 'menu_full'
 77                ),
 78                scale=(
 79                    1.83
 80                    if uiscale is bui.UIScale.SMALL
 81                    else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9
 82                ),
 83                stack_offset=(
 84                    (0, -46) if uiscale is bui.UIScale.SMALL else (0, 0)
 85                ),
 86            ),
 87            transition=transition,
 88            origin_widget=origin_widget,
 89        )
 90
 91        self._back_button: bui.Widget | None = bui.buttonwidget(
 92            parent=self._root_widget,
 93            position=(59 + x_inset, self._height - 70),
 94            size=(120, 60),
 95            scale=1.0,
 96            on_activate_call=self._on_back_press,
 97            autoselect=True,
 98            label=bui.Lstr(resource='backText'),
 99            button_type='back',
100        )
101        bui.containerwidget(
102            edit=self._root_widget, cancel_button=self._back_button
103        )
104        self._title_text = bui.textwidget(
105            parent=self._root_widget,
106            position=(
107                self._width * 0.5,
108                self._height - (32 if uiscale is bui.UIScale.SMALL else 41),
109            ),
110            size=(0, 0),
111            text=self._pvars.window_title_name,
112            scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3),
113            res_scale=1.5,
114            color=bui.app.ui_v1.heading_color,
115            h_align='center',
116            v_align='center',
117        )
118
119        bui.buttonwidget(
120            edit=self._back_button,
121            button_type='backSmall',
122            size=(60, 54),
123            position=(59 + x_inset, self._height - 67),
124            label=bui.charstr(bui.SpecialChar.BACK),
125        )
126
127        if uiscale is bui.UIScale.SMALL:
128            self._back_button.delete()
129            self._back_button = None
130            bui.containerwidget(
131                edit=self._root_widget, on_cancel_call=self._on_back_press
132            )
133            scroll_offs = 33
134        else:
135            scroll_offs = 0
136        self._scroll_width = self._width - (100 + 2 * x_inset)
137        self._scroll_height = self._height - (
138            146 if uiscale is bui.UIScale.SMALL else 136
139        )
140        self._scrollwidget = bui.scrollwidget(
141            parent=self._root_widget,
142            highlight=False,
143            size=(self._scroll_width, self._scroll_height),
144            position=(
145                (self._width - self._scroll_width) * 0.5,
146                65 + scroll_offs,
147            ),
148        )
149        bui.containerwidget(edit=self._scrollwidget, claims_left_right=True)
150        self._subcontainer: bui.Widget | None = None
151        self._config_name_full = self._pvars.config_name + ' Playlists'
152        self._last_config = None
153
154        # Update now and once per second (this should do our initial
155        # refresh).
156        self._update()
157        self._update_timer = bui.AppTimer(
158            1.0, bui.WeakCall(self._update), repeat=True
159        )
160
161    @override
162    def get_main_window_state(self) -> bui.MainWindowState:
163        # Support recreating our window for back/refresh purposes.
164        cls = type(self)
165
166        # Pull things out of self here; if we do it below in the lambda
167        # then we keep self alive.
168        sessiontype = self._sessiontype
169
170        # Pull anything out of self here; if we do it in the lambda
171        # we'll inadvertanly keep self alive.
172        playlist_select_context = self._playlist_select_context
173
174        return bui.BasicMainWindowState(
175            create_call=lambda transition, origin_widget: cls(
176                transition=transition,
177                origin_widget=origin_widget,
178                sessiontype=sessiontype,
179                playlist_select_context=playlist_select_context,
180            )
181        )
182
183    @override
184    def on_main_window_close(self) -> None:
185        self._save_state()
186
187    def _ensure_standard_playlists_exist(self) -> None:
188        plus = bui.app.plus
189        assert plus is not None
190
191        # On new installations, go ahead and create a few playlists
192        # besides the hard-coded default one:
193        if not plus.get_v1_account_misc_val('madeStandardPlaylists', False):
194            plus.add_v1_account_transaction(
195                {
196                    'type': 'ADD_PLAYLIST',
197                    'playlistType': 'Free-for-All',
198                    'playlistName': bui.Lstr(
199                        resource='singleGamePlaylistNameText'
200                    )
201                    .evaluate()
202                    .replace(
203                        '${GAME}',
204                        bui.Lstr(
205                            translate=('gameNames', 'Death Match')
206                        ).evaluate(),
207                    ),
208                    'playlist': [
209                        {
210                            'type': 'bs_death_match.DeathMatchGame',
211                            'settings': {
212                                'Epic Mode': False,
213                                'Kills to Win Per Player': 10,
214                                'Respawn Times': 1.0,
215                                'Time Limit': 300,
216                                'map': 'Doom Shroom',
217                            },
218                        },
219                        {
220                            'type': 'bs_death_match.DeathMatchGame',
221                            'settings': {
222                                'Epic Mode': False,
223                                'Kills to Win Per Player': 10,
224                                'Respawn Times': 1.0,
225                                'Time Limit': 300,
226                                'map': 'Crag Castle',
227                            },
228                        },
229                    ],
230                }
231            )
232            plus.add_v1_account_transaction(
233                {
234                    'type': 'ADD_PLAYLIST',
235                    'playlistType': 'Team Tournament',
236                    'playlistName': bui.Lstr(
237                        resource='singleGamePlaylistNameText'
238                    )
239                    .evaluate()
240                    .replace(
241                        '${GAME}',
242                        bui.Lstr(
243                            translate=('gameNames', 'Capture the Flag')
244                        ).evaluate(),
245                    ),
246                    'playlist': [
247                        {
248                            'type': 'bs_capture_the_flag.CTFGame',
249                            'settings': {
250                                'map': 'Bridgit',
251                                'Score to Win': 3,
252                                'Flag Idle Return Time': 30,
253                                'Flag Touch Return Time': 0,
254                                'Respawn Times': 1.0,
255                                'Time Limit': 600,
256                                'Epic Mode': False,
257                            },
258                        },
259                        {
260                            'type': 'bs_capture_the_flag.CTFGame',
261                            'settings': {
262                                'map': 'Roundabout',
263                                'Score to Win': 2,
264                                'Flag Idle Return Time': 30,
265                                'Flag Touch Return Time': 0,
266                                'Respawn Times': 1.0,
267                                'Time Limit': 600,
268                                'Epic Mode': False,
269                            },
270                        },
271                        {
272                            'type': 'bs_capture_the_flag.CTFGame',
273                            'settings': {
274                                'map': 'Tip Top',
275                                'Score to Win': 2,
276                                'Flag Idle Return Time': 30,
277                                'Flag Touch Return Time': 3,
278                                'Respawn Times': 1.0,
279                                'Time Limit': 300,
280                                'Epic Mode': False,
281                            },
282                        },
283                    ],
284                }
285            )
286            plus.add_v1_account_transaction(
287                {
288                    'type': 'ADD_PLAYLIST',
289                    'playlistType': 'Team Tournament',
290                    'playlistName': bui.Lstr(
291                        translate=('playlistNames', 'Just Sports')
292                    ).evaluate(),
293                    'playlist': [
294                        {
295                            'type': 'bs_hockey.HockeyGame',
296                            'settings': {
297                                'Time Limit': 0,
298                                'map': 'Hockey Stadium',
299                                'Score to Win': 1,
300                                'Respawn Times': 1.0,
301                            },
302                        },
303                        {
304                            'type': 'bs_football.FootballTeamGame',
305                            'settings': {
306                                'Time Limit': 0,
307                                'map': 'Football Stadium',
308                                'Score to Win': 21,
309                                'Respawn Times': 1.0,
310                            },
311                        },
312                    ],
313                }
314            )
315            plus.add_v1_account_transaction(
316                {
317                    'type': 'ADD_PLAYLIST',
318                    'playlistType': 'Free-for-All',
319                    'playlistName': bui.Lstr(
320                        translate=('playlistNames', 'Just Epic')
321                    ).evaluate(),
322                    'playlist': [
323                        {
324                            'type': 'bs_elimination.EliminationGame',
325                            'settings': {
326                                'Time Limit': 120,
327                                'map': 'Tip Top',
328                                'Respawn Times': 1.0,
329                                'Lives Per Player': 1,
330                                'Epic Mode': 1,
331                            },
332                        }
333                    ],
334                }
335            )
336            plus.add_v1_account_transaction(
337                {
338                    'type': 'SET_MISC_VAL',
339                    'name': 'madeStandardPlaylists',
340                    'value': True,
341                }
342            )
343            plus.run_v1_account_transactions()
344
345    def _refresh(self) -> None:
346        # FIXME: Should tidy this up.
347        # pylint: disable=too-many-statements
348        # pylint: disable=too-many-branches
349        # pylint: disable=too-many-locals
350        # pylint: disable=too-many-nested-blocks
351        from efro.util import asserttype
352        from bascenev1 import get_map_class, filter_playlist
353
354        if not self._root_widget:
355            return
356        if self._subcontainer is not None:
357            self._save_state()
358            self._subcontainer.delete()
359
360        # Make sure config exists.
361        if self._config_name_full not in bui.app.config:
362            bui.app.config[self._config_name_full] = {}
363
364        items = list(bui.app.config[self._config_name_full].items())
365
366        # Make sure everything is unicode.
367        items = [
368            (i[0].decode(), i[1]) if not isinstance(i[0], str) else i
369            for i in items
370        ]
371
372        items.sort(key=lambda x2: asserttype(x2[0], str).lower())
373        items = [['__default__', None]] + items  # default is always first
374
375        count = len(items)
376        columns = 3
377        rows = int(math.ceil(float(count) / columns))
378        button_width = 230
379        button_height = 230
380        button_buffer_h = -3
381        button_buffer_v = 0
382
383        self._sub_width = self._scroll_width
384        self._sub_height = (
385            40.0 + rows * (button_height + 2 * button_buffer_v) + 90
386        )
387        assert self._sub_width is not None
388        assert self._sub_height is not None
389        self._subcontainer = bui.containerwidget(
390            parent=self._scrollwidget,
391            size=(self._sub_width, self._sub_height),
392            background=False,
393        )
394
395        children = self._subcontainer.get_children()
396        for child in children:
397            child.delete()
398
399        assert bui.app.classic is not None
400        bui.textwidget(
401            parent=self._subcontainer,
402            text=bui.Lstr(resource='playlistsText'),
403            position=(40, self._sub_height - 26),
404            size=(0, 0),
405            scale=1.0,
406            maxwidth=400,
407            color=bui.app.ui_v1.title_color,
408            h_align='left',
409            v_align='center',
410        )
411
412        index = 0
413        appconfig = bui.app.config
414
415        mesh_opaque = bui.getmesh('level_select_button_opaque')
416        mesh_transparent = bui.getmesh('level_select_button_transparent')
417        mask_tex = bui.gettexture('mapPreviewMask')
418
419        h_offs = 225 if count == 1 else 115 if count == 2 else 0
420        h_offs_bottom = 0
421
422        uiscale = bui.app.ui_v1.uiscale
423        for y in range(rows):
424            for x in range(columns):
425                name = items[index][0]
426                assert name is not None
427                pos = (
428                    x * (button_width + 2 * button_buffer_h)
429                    + button_buffer_h
430                    + 8
431                    + h_offs,
432                    self._sub_height
433                    - 47
434                    - (y + 1) * (button_height + 2 * button_buffer_v),
435                )
436                btn = bui.buttonwidget(
437                    parent=self._subcontainer,
438                    button_type='square',
439                    size=(button_width, button_height),
440                    autoselect=True,
441                    label='',
442                    position=pos,
443                )
444
445                if x == 0 and uiscale is bui.UIScale.SMALL:
446                    bui.widget(
447                        edit=btn,
448                        left_widget=bui.get_special_widget('back_button'),
449                    )
450                if x == columns - 1 and uiscale is bui.UIScale.SMALL:
451                    bui.widget(
452                        edit=btn,
453                        right_widget=bui.get_special_widget('squad_button'),
454                    )
455                bui.buttonwidget(
456                    edit=btn,
457                    on_activate_call=bui.Call(
458                        self._on_playlist_press, btn, name
459                    ),
460                    on_select_call=bui.Call(self._on_playlist_select, name),
461                )
462                bui.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50)
463
464                if self._selected_playlist == name:
465                    bui.containerwidget(
466                        edit=self._subcontainer,
467                        selected_child=btn,
468                        visible_child=btn,
469                    )
470
471                if self._back_button is not None:
472                    if y == 0:
473                        bui.widget(edit=btn, up_widget=self._back_button)
474                    if x == 0:
475                        bui.widget(edit=btn, left_widget=self._back_button)
476
477                print_name: str | bui.Lstr | None
478                if name == '__default__':
479                    print_name = self._pvars.default_list_name
480                else:
481                    print_name = name
482                bui.textwidget(
483                    parent=self._subcontainer,
484                    text=print_name,
485                    position=(
486                        pos[0] + button_width * 0.5,
487                        pos[1] + button_height * 0.79,
488                    ),
489                    size=(0, 0),
490                    scale=button_width * 0.003,
491                    maxwidth=button_width * 0.7,
492                    draw_controller=btn,
493                    h_align='center',
494                    v_align='center',
495                )
496
497                # Poke into this playlist and see if we can display some of
498                # its maps.
499                map_images = []
500                try:
501                    map_textures = []
502                    map_texture_entries = []
503                    if name == '__default__':
504                        playlist = self._pvars.get_default_list_call()
505                    else:
506                        if (
507                            name
508                            not in appconfig[
509                                self._pvars.config_name + ' Playlists'
510                            ]
511                        ):
512                            print(
513                                'NOT FOUND ERR',
514                                appconfig[
515                                    self._pvars.config_name + ' Playlists'
516                                ],
517                            )
518                        playlist = appconfig[
519                            self._pvars.config_name + ' Playlists'
520                        ][name]
521                    playlist = filter_playlist(
522                        playlist,
523                        self._sessiontype,
524                        remove_unowned=False,
525                        mark_unowned=True,
526                        name=name,
527                    )
528                    for entry in playlist:
529                        mapname = entry['settings']['map']
530                        maptype: type[bs.Map] | None
531                        try:
532                            maptype = get_map_class(mapname)
533                        except bui.NotFoundError:
534                            maptype = None
535                        if maptype is not None:
536                            tex_name = maptype.get_preview_texture_name()
537                            if tex_name is not None:
538                                map_textures.append(tex_name)
539                                map_texture_entries.append(entry)
540                        if len(map_textures) >= 6:
541                            break
542
543                    if len(map_textures) > 4:
544                        img_rows = 3
545                        img_columns = 2
546                        scl = 0.33
547                        h_offs_img = 30
548                        v_offs_img = 126
549                    elif len(map_textures) > 2:
550                        img_rows = 2
551                        img_columns = 2
552                        scl = 0.35
553                        h_offs_img = 24
554                        v_offs_img = 110
555                    elif len(map_textures) > 1:
556                        img_rows = 2
557                        img_columns = 1
558                        scl = 0.5
559                        h_offs_img = 47
560                        v_offs_img = 105
561                    else:
562                        img_rows = 1
563                        img_columns = 1
564                        scl = 0.75
565                        h_offs_img = 20
566                        v_offs_img = 65
567
568                    v = None
569                    for row in range(img_rows):
570                        for col in range(img_columns):
571                            tex_index = row * img_columns + col
572                            if tex_index < len(map_textures):
573                                entry = map_texture_entries[tex_index]
574
575                                owned = not (
576                                    (
577                                        'is_unowned_map' in entry
578                                        and entry['is_unowned_map']
579                                    )
580                                    or (
581                                        'is_unowned_game' in entry
582                                        and entry['is_unowned_game']
583                                    )
584                                )
585
586                                tex_name = map_textures[tex_index]
587                                h = pos[0] + h_offs_img + scl * 250 * col
588                                v = pos[1] + v_offs_img - scl * 130 * row
589                                map_images.append(
590                                    bui.imagewidget(
591                                        parent=self._subcontainer,
592                                        size=(scl * 250.0, scl * 125.0),
593                                        position=(h, v),
594                                        texture=bui.gettexture(tex_name),
595                                        opacity=1.0 if owned else 0.25,
596                                        draw_controller=btn,
597                                        mesh_opaque=mesh_opaque,
598                                        mesh_transparent=mesh_transparent,
599                                        mask_texture=mask_tex,
600                                    )
601                                )
602                                if not owned:
603                                    bui.imagewidget(
604                                        parent=self._subcontainer,
605                                        size=(scl * 100.0, scl * 100.0),
606                                        position=(h + scl * 75, v + scl * 10),
607                                        texture=bui.gettexture('lock'),
608                                        draw_controller=btn,
609                                    )
610                        if v is not None:
611                            v -= scl * 130.0
612
613                except Exception:
614                    logging.exception('Error listing playlist maps.')
615
616                if not map_images:
617                    bui.textwidget(
618                        parent=self._subcontainer,
619                        text='???',
620                        scale=1.5,
621                        size=(0, 0),
622                        color=(1, 1, 1, 0.5),
623                        h_align='center',
624                        v_align='center',
625                        draw_controller=btn,
626                        position=(
627                            pos[0] + button_width * 0.5,
628                            pos[1] + button_height * 0.5,
629                        ),
630                    )
631
632                index += 1
633
634                if index >= count:
635                    break
636            if index >= count:
637                break
638        self._customize_button = btn = bui.buttonwidget(
639            parent=self._subcontainer,
640            size=(100, 30),
641            position=(34 + h_offs_bottom, 50),
642            text_scale=0.6,
643            label=bui.Lstr(resource='customizeText'),
644            on_activate_call=self._on_customize_press,
645            color=(0.54, 0.52, 0.67),
646            textcolor=(0.7, 0.65, 0.7),
647            autoselect=True,
648        )
649        bui.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28)
650        self._restore_state()
651
652    def on_play_options_window_run_game(self) -> None:
653        """(internal)"""
654
655        # No-op if we're not in control.
656        if not self.main_window_has_control():
657            # if not self._root_widget:
658            return
659
660        if self._playlist_select_context is not None:
661            # Done doing a playlist selection; now back all the way out
662            # of our selection windows to our stored starting point.
663            if self._playlist_select_context.back_state is None:
664                logging.error(
665                    'No back state found'
666                    ' after playlist select context completion.'
667                )
668            else:
669                self.main_window_back_state = (
670                    self._playlist_select_context.back_state
671                )
672            self.main_window_back()
673        else:
674            # Launching a regular game session; simply get our window
675            # transitioning out.
676            self.main_window_close(transition='out_left')
677
678    def _on_playlist_select(self, playlist_name: str) -> None:
679        self._selected_playlist = playlist_name
680
681    def _update(self) -> None:
682        # Make sure config exists.
683        if self._config_name_full not in bui.app.config:
684            bui.app.config[self._config_name_full] = {}
685
686        cfg = bui.app.config[self._config_name_full]
687        if cfg != self._last_config:
688            self._last_config = copy.deepcopy(cfg)
689            self._refresh()
690
691    def _on_playlist_press(
692        self, button: bui.Widget, playlist_name: str
693    ) -> None:
694        # pylint: disable=cyclic-import
695        from bauiv1lib.playoptions import PlayOptionsWindow
696
697        # Make sure the target playlist still exists.
698        exists = (
699            playlist_name == '__default__'
700            or playlist_name in bui.app.config.get(self._config_name_full, {})
701        )
702        if not exists:
703            return
704
705        self._save_state()
706        PlayOptionsWindow(
707            sessiontype=self._sessiontype,
708            scale_origin=button.get_screen_space_center(),
709            playlist=playlist_name,
710            delegate=self,
711            playlist_select_context=self._playlist_select_context,
712        )
713
714    def _on_customize_press(self) -> None:
715        # pylint: disable=cyclic-import
716        from bauiv1lib.playlist.customizebrowser import (
717            PlaylistCustomizeBrowserWindow,
718        )
719
720        # no-op if we're not in control.
721        if not self.main_window_has_control():
722            return
723
724        self.main_window_replace(
725            PlaylistCustomizeBrowserWindow(
726                origin_widget=self._customize_button,
727                sessiontype=self._sessiontype,
728            )
729        )
730
731    def _on_back_press(self) -> None:
732        # pylint: disable=cyclic-import
733        # from bauiv1lib.play import PlayWindow
734
735        # no-op if we're not in control.
736        if not self.main_window_has_control():
737            return
738
739        # Store our selected playlist if that's changed.
740        if self._selected_playlist is not None:
741            prev_sel = bui.app.config.get(
742                self._pvars.config_name + ' Playlist Selection'
743            )
744            if self._selected_playlist != prev_sel:
745                cfg = bui.app.config
746                cfg[self._pvars.config_name + ' Playlist Selection'] = (
747                    self._selected_playlist
748                )
749                cfg.commit()
750
751        self.main_window_back()
752
753    def _save_state(self) -> None:
754        try:
755            sel = self._root_widget.get_selected_child()
756            if sel == self._back_button:
757                sel_name = 'Back'
758            elif sel == self._scrollwidget:
759                assert self._subcontainer is not None
760                subsel = self._subcontainer.get_selected_child()
761                if subsel == self._customize_button:
762                    sel_name = 'Customize'
763                else:
764                    sel_name = 'Scroll'
765            else:
766                raise RuntimeError('Unrecognized selected widget.')
767            assert bui.app.classic is not None
768            bui.app.ui_v1.window_states[type(self)] = sel_name
769        except Exception:
770            logging.exception('Error saving state for %s.', self)
771
772    def _restore_state(self) -> None:
773        try:
774            assert bui.app.classic is not None
775            sel_name = bui.app.ui_v1.window_states.get(type(self))
776            if sel_name == 'Back':
777                sel = self._back_button
778            elif sel_name == 'Scroll':
779                sel = self._scrollwidget
780            elif sel_name == 'Customize':
781                sel = self._scrollwidget
782                bui.containerwidget(
783                    edit=self._subcontainer,
784                    selected_child=self._customize_button,
785                    visible_child=self._customize_button,
786                )
787            else:
788                sel = self._scrollwidget
789            bui.containerwidget(edit=self._root_widget, selected_child=sel)
790        except Exception:
791            logging.exception('Error restoring state for %s.', self)

Window for starting teams games.

PlaylistBrowserWindow( sessiontype: type[bascenev1.Session], transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None, playlist_select_context: bauiv1lib.play.PlaylistSelectContext | None = None)
 23    def __init__(
 24        self,
 25        sessiontype: type[bs.Session],
 26        transition: str | None = 'in_right',
 27        origin_widget: bui.Widget | None = None,
 28        playlist_select_context: PlaylistSelectContext | None = None,
 29    ):
 30        # pylint: disable=cyclic-import
 31        from bauiv1lib.playlist import PlaylistTypeVars
 32
 33        # Store state for when we exit the next game.
 34        if issubclass(sessiontype, bs.DualTeamSession):
 35            bui.set_analytics_screen('Teams Window')
 36        elif issubclass(sessiontype, bs.FreeForAllSession):
 37            bui.set_analytics_screen('FreeForAll Window')
 38        else:
 39            raise TypeError(f'Invalid sessiontype: {sessiontype}.')
 40        self._pvars = PlaylistTypeVars(sessiontype)
 41
 42        self._sessiontype = sessiontype
 43
 44        self._customize_button: bui.Widget | None = None
 45        self._sub_width: float | None = None
 46        self._sub_height: float | None = None
 47        self._playlist_select_context = playlist_select_context
 48
 49        self._ensure_standard_playlists_exist()
 50
 51        # Get the current selection (if any).
 52        self._selected_playlist = bui.app.config.get(
 53            self._pvars.config_name + ' Playlist Selection'
 54        )
 55
 56        uiscale = bui.app.ui_v1.uiscale
 57        self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0
 58        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 59        self._height = (
 60            440
 61            if uiscale is bui.UIScale.SMALL
 62            else 510 if uiscale is bui.UIScale.MEDIUM else 580
 63        )
 64
 65        top_extra = 30 if uiscale is bui.UIScale.SMALL else 0
 66
 67        super().__init__(
 68            root_widget=bui.containerwidget(
 69                size=(self._width, self._height + top_extra),
 70                toolbar_visibility=(
 71                    'menu_minimal'
 72                    if (
 73                        uiscale is bui.UIScale.SMALL
 74                        or playlist_select_context is not None
 75                    )
 76                    else 'menu_full'
 77                ),
 78                scale=(
 79                    1.83
 80                    if uiscale is bui.UIScale.SMALL
 81                    else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9
 82                ),
 83                stack_offset=(
 84                    (0, -46) if uiscale is bui.UIScale.SMALL else (0, 0)
 85                ),
 86            ),
 87            transition=transition,
 88            origin_widget=origin_widget,
 89        )
 90
 91        self._back_button: bui.Widget | None = bui.buttonwidget(
 92            parent=self._root_widget,
 93            position=(59 + x_inset, self._height - 70),
 94            size=(120, 60),
 95            scale=1.0,
 96            on_activate_call=self._on_back_press,
 97            autoselect=True,
 98            label=bui.Lstr(resource='backText'),
 99            button_type='back',
100        )
101        bui.containerwidget(
102            edit=self._root_widget, cancel_button=self._back_button
103        )
104        self._title_text = bui.textwidget(
105            parent=self._root_widget,
106            position=(
107                self._width * 0.5,
108                self._height - (32 if uiscale is bui.UIScale.SMALL else 41),
109            ),
110            size=(0, 0),
111            text=self._pvars.window_title_name,
112            scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3),
113            res_scale=1.5,
114            color=bui.app.ui_v1.heading_color,
115            h_align='center',
116            v_align='center',
117        )
118
119        bui.buttonwidget(
120            edit=self._back_button,
121            button_type='backSmall',
122            size=(60, 54),
123            position=(59 + x_inset, self._height - 67),
124            label=bui.charstr(bui.SpecialChar.BACK),
125        )
126
127        if uiscale is bui.UIScale.SMALL:
128            self._back_button.delete()
129            self._back_button = None
130            bui.containerwidget(
131                edit=self._root_widget, on_cancel_call=self._on_back_press
132            )
133            scroll_offs = 33
134        else:
135            scroll_offs = 0
136        self._scroll_width = self._width - (100 + 2 * x_inset)
137        self._scroll_height = self._height - (
138            146 if uiscale is bui.UIScale.SMALL else 136
139        )
140        self._scrollwidget = bui.scrollwidget(
141            parent=self._root_widget,
142            highlight=False,
143            size=(self._scroll_width, self._scroll_height),
144            position=(
145                (self._width - self._scroll_width) * 0.5,
146                65 + scroll_offs,
147            ),
148        )
149        bui.containerwidget(edit=self._scrollwidget, claims_left_right=True)
150        self._subcontainer: bui.Widget | None = None
151        self._config_name_full = self._pvars.config_name + ' Playlists'
152        self._last_config = None
153
154        # Update now and once per second (this should do our initial
155        # refresh).
156        self._update()
157        self._update_timer = bui.AppTimer(
158            1.0, bui.WeakCall(self._update), repeat=True
159        )

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:
161    @override
162    def get_main_window_state(self) -> bui.MainWindowState:
163        # Support recreating our window for back/refresh purposes.
164        cls = type(self)
165
166        # Pull things out of self here; if we do it below in the lambda
167        # then we keep self alive.
168        sessiontype = self._sessiontype
169
170        # Pull anything out of self here; if we do it in the lambda
171        # we'll inadvertanly keep self alive.
172        playlist_select_context = self._playlist_select_context
173
174        return bui.BasicMainWindowState(
175            create_call=lambda transition, origin_widget: cls(
176                transition=transition,
177                origin_widget=origin_widget,
178                sessiontype=sessiontype,
179                playlist_select_context=playlist_select_context,
180            )
181        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
183    @override
184    def on_main_window_close(self) -> None:
185        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.