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