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

Window for starting teams games.

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