bauiv1lib.playlist.browser

Provides a window for browsing and launching game playlists.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides a window for browsing and launching game playlists."""
  4
  5from __future__ import annotations
  6
  7import copy
  8import math
  9import logging
 10from typing import override
 11
 12import bascenev1 as bs
 13import bauiv1 as bui
 14
 15
 16class PlaylistBrowserWindow(bui.MainWindow):
 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=cyclic-import
 26        from bauiv1lib.playlist import PlaylistTypeVars
 27
 28        # Store state for when we exit the next game.
 29        if issubclass(sessiontype, bs.DualTeamSession):
 30            bui.set_analytics_screen('Teams Window')
 31        elif issubclass(sessiontype, bs.FreeForAllSession):
 32            bui.set_analytics_screen('FreeForAll Window')
 33        else:
 34            raise TypeError(f'Invalid sessiontype: {sessiontype}.')
 35        self._pvars = PlaylistTypeVars(sessiontype)
 36
 37        self._sessiontype = sessiontype
 38
 39        self._customize_button: bui.Widget | None = None
 40        self._sub_width: float | None = None
 41        self._sub_height: float | None = None
 42
 43        self._ensure_standard_playlists_exist()
 44
 45        # Get the current selection (if any).
 46        self._selected_playlist = bui.app.config.get(
 47            self._pvars.config_name + ' Playlist Selection'
 48        )
 49
 50        uiscale = bui.app.ui_v1.uiscale
 51        self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0
 52        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 53        self._height = (
 54            440
 55            if uiscale is bui.UIScale.SMALL
 56            else 510 if uiscale is bui.UIScale.MEDIUM else 580
 57        )
 58
 59        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
 60
 61        super().__init__(
 62            root_widget=bui.containerwidget(
 63                size=(self._width, self._height + top_extra),
 64                toolbar_visibility=(
 65                    'menu_minimal'
 66                    if uiscale is bui.UIScale.SMALL
 67                    else 'menu_full'
 68                ),
 69                scale=(
 70                    1.83
 71                    if uiscale is bui.UIScale.SMALL
 72                    else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9
 73                ),
 74                stack_offset=(
 75                    (0, -56) if uiscale is bui.UIScale.SMALL else (0, 0)
 76                ),
 77            ),
 78            transition=transition,
 79            origin_widget=origin_widget,
 80        )
 81
 82        self._back_button: bui.Widget | None = bui.buttonwidget(
 83            parent=self._root_widget,
 84            position=(59 + x_inset, self._height - 70),
 85            size=(120, 60),
 86            scale=1.0,
 87            on_activate_call=self._on_back_press,
 88            autoselect=True,
 89            label=bui.Lstr(resource='backText'),
 90            button_type='back',
 91        )
 92        bui.containerwidget(
 93            edit=self._root_widget, cancel_button=self._back_button
 94        )
 95        self._title_text = bui.textwidget(
 96            parent=self._root_widget,
 97            position=(
 98                self._width * 0.5,
 99                self._height - (32 if uiscale is bui.UIScale.SMALL else 41),
100            ),
101            size=(0, 0),
102            text=self._pvars.window_title_name,
103            scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3),
104            res_scale=1.5,
105            color=bui.app.ui_v1.heading_color,
106            h_align='center',
107            v_align='center',
108        )
109        # if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars:
110        #     bui.textwidget(edit=txt, text='')
111
112        bui.buttonwidget(
113            edit=self._back_button,
114            button_type='backSmall',
115            size=(60, 54),
116            position=(59 + x_inset, self._height - 67),
117            label=bui.charstr(bui.SpecialChar.BACK),
118        )
119
120        if uiscale is bui.UIScale.SMALL:
121            self._back_button.delete()
122            self._back_button = None
123            bui.containerwidget(
124                edit=self._root_widget, on_cancel_call=self._on_back_press
125            )
126            scroll_offs = 33
127        else:
128            scroll_offs = 0
129        self._scroll_width = self._width - (100 + 2 * x_inset)
130        self._scroll_height = self._height - (
131            146 if uiscale is bui.UIScale.SMALL else 136
132        )
133        self._scrollwidget = bui.scrollwidget(
134            parent=self._root_widget,
135            highlight=False,
136            size=(self._scroll_width, self._scroll_height),
137            position=(
138                (self._width - self._scroll_width) * 0.5,
139                65 + scroll_offs,
140            ),
141        )
142        bui.containerwidget(edit=self._scrollwidget, claims_left_right=True)
143        self._subcontainer: bui.Widget | None = None
144        self._config_name_full = self._pvars.config_name + ' Playlists'
145        self._last_config = None
146
147        # Update now and once per second.
148        # (this should do our initial refresh)
149        self._update()
150        self._update_timer = bui.AppTimer(
151            1.0, bui.WeakCall(self._update), repeat=True
152        )
153
154    @override
155    def get_main_window_state(self) -> bui.MainWindowState:
156        # Support recreating our window for back/refresh purposes.
157        cls = type(self)
158
159        # Pull things out of self here; if we do it below in the lambda
160        # then we keep self alive.
161        sessiontype = self._sessiontype
162
163        return bui.BasicMainWindowState(
164            create_call=lambda transition, origin_widget: cls(
165                transition=transition,
166                origin_widget=origin_widget,
167                sessiontype=sessiontype,
168            )
169        )
170
171    @override
172    def on_main_window_close(self) -> None:
173        self._save_state()
174
175    def _ensure_standard_playlists_exist(self) -> None:
176        plus = bui.app.plus
177        assert plus is not None
178
179        # On new installations, go ahead and create a few playlists
180        # besides the hard-coded default one:
181        if not plus.get_v1_account_misc_val('madeStandardPlaylists', False):
182            plus.add_v1_account_transaction(
183                {
184                    'type': 'ADD_PLAYLIST',
185                    'playlistType': 'Free-for-All',
186                    'playlistName': bui.Lstr(
187                        resource='singleGamePlaylistNameText'
188                    )
189                    .evaluate()
190                    .replace(
191                        '${GAME}',
192                        bui.Lstr(
193                            translate=('gameNames', 'Death Match')
194                        ).evaluate(),
195                    ),
196                    'playlist': [
197                        {
198                            'type': 'bs_death_match.DeathMatchGame',
199                            'settings': {
200                                'Epic Mode': False,
201                                'Kills to Win Per Player': 10,
202                                'Respawn Times': 1.0,
203                                'Time Limit': 300,
204                                'map': 'Doom Shroom',
205                            },
206                        },
207                        {
208                            'type': 'bs_death_match.DeathMatchGame',
209                            'settings': {
210                                'Epic Mode': False,
211                                'Kills to Win Per Player': 10,
212                                'Respawn Times': 1.0,
213                                'Time Limit': 300,
214                                'map': 'Crag Castle',
215                            },
216                        },
217                    ],
218                }
219            )
220            plus.add_v1_account_transaction(
221                {
222                    'type': 'ADD_PLAYLIST',
223                    'playlistType': 'Team Tournament',
224                    'playlistName': bui.Lstr(
225                        resource='singleGamePlaylistNameText'
226                    )
227                    .evaluate()
228                    .replace(
229                        '${GAME}',
230                        bui.Lstr(
231                            translate=('gameNames', 'Capture the Flag')
232                        ).evaluate(),
233                    ),
234                    'playlist': [
235                        {
236                            'type': 'bs_capture_the_flag.CTFGame',
237                            'settings': {
238                                'map': 'Bridgit',
239                                'Score to Win': 3,
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': 'Roundabout',
251                                'Score to Win': 2,
252                                'Flag Idle Return Time': 30,
253                                'Flag Touch Return Time': 0,
254                                'Respawn Times': 1.0,
255                                'Time Limit': 600,
256                                'Epic Mode': False,
257                            },
258                        },
259                        {
260                            'type': 'bs_capture_the_flag.CTFGame',
261                            'settings': {
262                                'map': 'Tip Top',
263                                'Score to Win': 2,
264                                'Flag Idle Return Time': 30,
265                                'Flag Touch Return Time': 3,
266                                'Respawn Times': 1.0,
267                                'Time Limit': 300,
268                                'Epic Mode': False,
269                            },
270                        },
271                    ],
272                }
273            )
274            plus.add_v1_account_transaction(
275                {
276                    'type': 'ADD_PLAYLIST',
277                    'playlistType': 'Team Tournament',
278                    'playlistName': bui.Lstr(
279                        translate=('playlistNames', 'Just Sports')
280                    ).evaluate(),
281                    'playlist': [
282                        {
283                            'type': 'bs_hockey.HockeyGame',
284                            'settings': {
285                                'Time Limit': 0,
286                                'map': 'Hockey Stadium',
287                                'Score to Win': 1,
288                                'Respawn Times': 1.0,
289                            },
290                        },
291                        {
292                            'type': 'bs_football.FootballTeamGame',
293                            'settings': {
294                                'Time Limit': 0,
295                                'map': 'Football Stadium',
296                                'Score to Win': 21,
297                                'Respawn Times': 1.0,
298                            },
299                        },
300                    ],
301                }
302            )
303            plus.add_v1_account_transaction(
304                {
305                    'type': 'ADD_PLAYLIST',
306                    'playlistType': 'Free-for-All',
307                    'playlistName': bui.Lstr(
308                        translate=('playlistNames', 'Just Epic')
309                    ).evaluate(),
310                    'playlist': [
311                        {
312                            'type': 'bs_elimination.EliminationGame',
313                            'settings': {
314                                'Time Limit': 120,
315                                'map': 'Tip Top',
316                                'Respawn Times': 1.0,
317                                'Lives Per Player': 1,
318                                'Epic Mode': 1,
319                            },
320                        }
321                    ],
322                }
323            )
324            plus.add_v1_account_transaction(
325                {
326                    'type': 'SET_MISC_VAL',
327                    'name': 'madeStandardPlaylists',
328                    'value': True,
329                }
330            )
331            plus.run_v1_account_transactions()
332
333    def _refresh(self) -> None:
334        # FIXME: Should tidy this up.
335        # pylint: disable=too-many-statements
336        # pylint: disable=too-many-branches
337        # pylint: disable=too-many-locals
338        # pylint: disable=too-many-nested-blocks
339        from efro.util import asserttype
340        from bascenev1 import get_map_class, filter_playlist
341
342        if not self._root_widget:
343            return
344        if self._subcontainer is not None:
345            self._save_state()
346            self._subcontainer.delete()
347
348        # Make sure config exists.
349        if self._config_name_full not in bui.app.config:
350            bui.app.config[self._config_name_full] = {}
351
352        items = list(bui.app.config[self._config_name_full].items())
353
354        # Make sure everything is unicode.
355        items = [
356            (i[0].decode(), i[1]) if not isinstance(i[0], str) else i
357            for i in items
358        ]
359
360        items.sort(key=lambda x2: asserttype(x2[0], str).lower())
361        items = [['__default__', None]] + items  # default is always first
362
363        count = len(items)
364        columns = 3
365        rows = int(math.ceil(float(count) / columns))
366        button_width = 230
367        button_height = 230
368        button_buffer_h = -3
369        button_buffer_v = 0
370
371        self._sub_width = self._scroll_width
372        self._sub_height = (
373            40.0 + rows * (button_height + 2 * button_buffer_v) + 90
374        )
375        assert self._sub_width is not None
376        assert self._sub_height is not None
377        self._subcontainer = bui.containerwidget(
378            parent=self._scrollwidget,
379            size=(self._sub_width, self._sub_height),
380            background=False,
381        )
382
383        children = self._subcontainer.get_children()
384        for child in children:
385            child.delete()
386
387        assert bui.app.classic is not None
388        bui.textwidget(
389            parent=self._subcontainer,
390            text=bui.Lstr(resource='playlistsText'),
391            position=(40, self._sub_height - 26),
392            size=(0, 0),
393            scale=1.0,
394            maxwidth=400,
395            color=bui.app.ui_v1.title_color,
396            h_align='left',
397            v_align='center',
398        )
399
400        index = 0
401        appconfig = bui.app.config
402
403        mesh_opaque = bui.getmesh('level_select_button_opaque')
404        mesh_transparent = bui.getmesh('level_select_button_transparent')
405        mask_tex = bui.gettexture('mapPreviewMask')
406
407        h_offs = 225 if count == 1 else 115 if count == 2 else 0
408        h_offs_bottom = 0
409
410        uiscale = bui.app.ui_v1.uiscale
411        for y in range(rows):
412            for x in range(columns):
413                name = items[index][0]
414                assert name is not None
415                pos = (
416                    x * (button_width + 2 * button_buffer_h)
417                    + button_buffer_h
418                    + 8
419                    + h_offs,
420                    self._sub_height
421                    - 47
422                    - (y + 1) * (button_height + 2 * button_buffer_v),
423                )
424                btn = bui.buttonwidget(
425                    parent=self._subcontainer,
426                    button_type='square',
427                    size=(button_width, button_height),
428                    autoselect=True,
429                    label='',
430                    position=pos,
431                )
432
433                if x == 0 and uiscale is bui.UIScale.SMALL:
434                    bui.widget(
435                        edit=btn,
436                        left_widget=bui.get_special_widget('back_button'),
437                    )
438                if x == columns - 1 and uiscale is bui.UIScale.SMALL:
439                    bui.widget(
440                        edit=btn,
441                        right_widget=bui.get_special_widget('squad_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_window(
695            PlaylistCustomizeBrowserWindow(
696                origin_widget=self._customize_button,
697                sessiontype=self._sessiontype,
698            ),
699            from_window=self,
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[self._pvars.config_name + ' Playlist Selection'] = (
718                    self._selected_playlist
719                )
720                cfg.commit()
721
722        self.main_window_back()
723
724        # self._save_state()
725        # bui.containerwidget(
726        #     edit=self._root_widget, transition=self._transition_out
727        # )
728        # assert bui.app.classic is not None
729        # bui.app.ui_v1.set_main_window(
730        #     PlayWindow(transition='in_left'), from_window=self, is_back=True
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)
class PlaylistBrowserWindow(bauiv1._uitypes.MainWindow):
 17class PlaylistBrowserWindow(bui.MainWindow):
 18    """Window for starting teams games."""
 19
 20    def __init__(
 21        self,
 22        sessiontype: type[bs.Session],
 23        transition: str | None = 'in_right',
 24        origin_widget: bui.Widget | None = None,
 25    ):
 26        # pylint: disable=cyclic-import
 27        from bauiv1lib.playlist import PlaylistTypeVars
 28
 29        # Store state for when we exit the next game.
 30        if issubclass(sessiontype, bs.DualTeamSession):
 31            bui.set_analytics_screen('Teams Window')
 32        elif issubclass(sessiontype, bs.FreeForAllSession):
 33            bui.set_analytics_screen('FreeForAll Window')
 34        else:
 35            raise TypeError(f'Invalid sessiontype: {sessiontype}.')
 36        self._pvars = PlaylistTypeVars(sessiontype)
 37
 38        self._sessiontype = sessiontype
 39
 40        self._customize_button: bui.Widget | None = None
 41        self._sub_width: float | None = None
 42        self._sub_height: float | None = None
 43
 44        self._ensure_standard_playlists_exist()
 45
 46        # Get the current selection (if any).
 47        self._selected_playlist = bui.app.config.get(
 48            self._pvars.config_name + ' Playlist Selection'
 49        )
 50
 51        uiscale = bui.app.ui_v1.uiscale
 52        self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0
 53        x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
 54        self._height = (
 55            440
 56            if uiscale is bui.UIScale.SMALL
 57            else 510 if uiscale is bui.UIScale.MEDIUM else 580
 58        )
 59
 60        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
 61
 62        super().__init__(
 63            root_widget=bui.containerwidget(
 64                size=(self._width, self._height + top_extra),
 65                toolbar_visibility=(
 66                    'menu_minimal'
 67                    if uiscale is bui.UIScale.SMALL
 68                    else 'menu_full'
 69                ),
 70                scale=(
 71                    1.83
 72                    if uiscale is bui.UIScale.SMALL
 73                    else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9
 74                ),
 75                stack_offset=(
 76                    (0, -56) if uiscale is bui.UIScale.SMALL else (0, 0)
 77                ),
 78            ),
 79            transition=transition,
 80            origin_widget=origin_widget,
 81        )
 82
 83        self._back_button: bui.Widget | None = bui.buttonwidget(
 84            parent=self._root_widget,
 85            position=(59 + x_inset, self._height - 70),
 86            size=(120, 60),
 87            scale=1.0,
 88            on_activate_call=self._on_back_press,
 89            autoselect=True,
 90            label=bui.Lstr(resource='backText'),
 91            button_type='back',
 92        )
 93        bui.containerwidget(
 94            edit=self._root_widget, cancel_button=self._back_button
 95        )
 96        self._title_text = bui.textwidget(
 97            parent=self._root_widget,
 98            position=(
 99                self._width * 0.5,
100                self._height - (32 if uiscale is bui.UIScale.SMALL else 41),
101            ),
102            size=(0, 0),
103            text=self._pvars.window_title_name,
104            scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3),
105            res_scale=1.5,
106            color=bui.app.ui_v1.heading_color,
107            h_align='center',
108            v_align='center',
109        )
110        # if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars:
111        #     bui.textwidget(edit=txt, text='')
112
113        bui.buttonwidget(
114            edit=self._back_button,
115            button_type='backSmall',
116            size=(60, 54),
117            position=(59 + x_inset, self._height - 67),
118            label=bui.charstr(bui.SpecialChar.BACK),
119        )
120
121        if uiscale is bui.UIScale.SMALL:
122            self._back_button.delete()
123            self._back_button = None
124            bui.containerwidget(
125                edit=self._root_widget, on_cancel_call=self._on_back_press
126            )
127            scroll_offs = 33
128        else:
129            scroll_offs = 0
130        self._scroll_width = self._width - (100 + 2 * x_inset)
131        self._scroll_height = self._height - (
132            146 if uiscale is bui.UIScale.SMALL else 136
133        )
134        self._scrollwidget = bui.scrollwidget(
135            parent=self._root_widget,
136            highlight=False,
137            size=(self._scroll_width, self._scroll_height),
138            position=(
139                (self._width - self._scroll_width) * 0.5,
140                65 + scroll_offs,
141            ),
142        )
143        bui.containerwidget(edit=self._scrollwidget, claims_left_right=True)
144        self._subcontainer: bui.Widget | None = None
145        self._config_name_full = self._pvars.config_name + ' Playlists'
146        self._last_config = None
147
148        # Update now and once per second.
149        # (this should do our initial refresh)
150        self._update()
151        self._update_timer = bui.AppTimer(
152            1.0, bui.WeakCall(self._update), repeat=True
153        )
154
155    @override
156    def get_main_window_state(self) -> bui.MainWindowState:
157        # Support recreating our window for back/refresh purposes.
158        cls = type(self)
159
160        # Pull things out of self here; if we do it below in the lambda
161        # then we keep self alive.
162        sessiontype = self._sessiontype
163
164        return bui.BasicMainWindowState(
165            create_call=lambda transition, origin_widget: cls(
166                transition=transition,
167                origin_widget=origin_widget,
168                sessiontype=sessiontype,
169            )
170        )
171
172    @override
173    def on_main_window_close(self) -> None:
174        self._save_state()
175
176    def _ensure_standard_playlists_exist(self) -> None:
177        plus = bui.app.plus
178        assert plus is not None
179
180        # On new installations, go ahead and create a few playlists
181        # besides the hard-coded default one:
182        if not plus.get_v1_account_misc_val('madeStandardPlaylists', False):
183            plus.add_v1_account_transaction(
184                {
185                    'type': 'ADD_PLAYLIST',
186                    'playlistType': 'Free-for-All',
187                    'playlistName': bui.Lstr(
188                        resource='singleGamePlaylistNameText'
189                    )
190                    .evaluate()
191                    .replace(
192                        '${GAME}',
193                        bui.Lstr(
194                            translate=('gameNames', 'Death Match')
195                        ).evaluate(),
196                    ),
197                    'playlist': [
198                        {
199                            'type': 'bs_death_match.DeathMatchGame',
200                            'settings': {
201                                'Epic Mode': False,
202                                'Kills to Win Per Player': 10,
203                                'Respawn Times': 1.0,
204                                'Time Limit': 300,
205                                'map': 'Doom Shroom',
206                            },
207                        },
208                        {
209                            'type': 'bs_death_match.DeathMatchGame',
210                            'settings': {
211                                'Epic Mode': False,
212                                'Kills to Win Per Player': 10,
213                                'Respawn Times': 1.0,
214                                'Time Limit': 300,
215                                'map': 'Crag Castle',
216                            },
217                        },
218                    ],
219                }
220            )
221            plus.add_v1_account_transaction(
222                {
223                    'type': 'ADD_PLAYLIST',
224                    'playlistType': 'Team Tournament',
225                    'playlistName': bui.Lstr(
226                        resource='singleGamePlaylistNameText'
227                    )
228                    .evaluate()
229                    .replace(
230                        '${GAME}',
231                        bui.Lstr(
232                            translate=('gameNames', 'Capture the Flag')
233                        ).evaluate(),
234                    ),
235                    'playlist': [
236                        {
237                            'type': 'bs_capture_the_flag.CTFGame',
238                            'settings': {
239                                'map': 'Bridgit',
240                                'Score to Win': 3,
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': 'Roundabout',
252                                'Score to Win': 2,
253                                'Flag Idle Return Time': 30,
254                                'Flag Touch Return Time': 0,
255                                'Respawn Times': 1.0,
256                                'Time Limit': 600,
257                                'Epic Mode': False,
258                            },
259                        },
260                        {
261                            'type': 'bs_capture_the_flag.CTFGame',
262                            'settings': {
263                                'map': 'Tip Top',
264                                'Score to Win': 2,
265                                'Flag Idle Return Time': 30,
266                                'Flag Touch Return Time': 3,
267                                'Respawn Times': 1.0,
268                                'Time Limit': 300,
269                                'Epic Mode': False,
270                            },
271                        },
272                    ],
273                }
274            )
275            plus.add_v1_account_transaction(
276                {
277                    'type': 'ADD_PLAYLIST',
278                    'playlistType': 'Team Tournament',
279                    'playlistName': bui.Lstr(
280                        translate=('playlistNames', 'Just Sports')
281                    ).evaluate(),
282                    'playlist': [
283                        {
284                            'type': 'bs_hockey.HockeyGame',
285                            'settings': {
286                                'Time Limit': 0,
287                                'map': 'Hockey Stadium',
288                                'Score to Win': 1,
289                                'Respawn Times': 1.0,
290                            },
291                        },
292                        {
293                            'type': 'bs_football.FootballTeamGame',
294                            'settings': {
295                                'Time Limit': 0,
296                                'map': 'Football Stadium',
297                                'Score to Win': 21,
298                                'Respawn Times': 1.0,
299                            },
300                        },
301                    ],
302                }
303            )
304            plus.add_v1_account_transaction(
305                {
306                    'type': 'ADD_PLAYLIST',
307                    'playlistType': 'Free-for-All',
308                    'playlistName': bui.Lstr(
309                        translate=('playlistNames', 'Just Epic')
310                    ).evaluate(),
311                    'playlist': [
312                        {
313                            'type': 'bs_elimination.EliminationGame',
314                            'settings': {
315                                'Time Limit': 120,
316                                'map': 'Tip Top',
317                                'Respawn Times': 1.0,
318                                'Lives Per Player': 1,
319                                'Epic Mode': 1,
320                            },
321                        }
322                    ],
323                }
324            )
325            plus.add_v1_account_transaction(
326                {
327                    'type': 'SET_MISC_VAL',
328                    'name': 'madeStandardPlaylists',
329                    'value': True,
330                }
331            )
332            plus.run_v1_account_transactions()
333
334    def _refresh(self) -> None:
335        # FIXME: Should tidy this up.
336        # pylint: disable=too-many-statements
337        # pylint: disable=too-many-branches
338        # pylint: disable=too-many-locals
339        # pylint: disable=too-many-nested-blocks
340        from efro.util import asserttype
341        from bascenev1 import get_map_class, filter_playlist
342
343        if not self._root_widget:
344            return
345        if self._subcontainer is not None:
346            self._save_state()
347            self._subcontainer.delete()
348
349        # Make sure config exists.
350        if self._config_name_full not in bui.app.config:
351            bui.app.config[self._config_name_full] = {}
352
353        items = list(bui.app.config[self._config_name_full].items())
354
355        # Make sure everything is unicode.
356        items = [
357            (i[0].decode(), i[1]) if not isinstance(i[0], str) else i
358            for i in items
359        ]
360
361        items.sort(key=lambda x2: asserttype(x2[0], str).lower())
362        items = [['__default__', None]] + items  # default is always first
363
364        count = len(items)
365        columns = 3
366        rows = int(math.ceil(float(count) / columns))
367        button_width = 230
368        button_height = 230
369        button_buffer_h = -3
370        button_buffer_v = 0
371
372        self._sub_width = self._scroll_width
373        self._sub_height = (
374            40.0 + rows * (button_height + 2 * button_buffer_v) + 90
375        )
376        assert self._sub_width is not None
377        assert self._sub_height is not None
378        self._subcontainer = bui.containerwidget(
379            parent=self._scrollwidget,
380            size=(self._sub_width, self._sub_height),
381            background=False,
382        )
383
384        children = self._subcontainer.get_children()
385        for child in children:
386            child.delete()
387
388        assert bui.app.classic is not None
389        bui.textwidget(
390            parent=self._subcontainer,
391            text=bui.Lstr(resource='playlistsText'),
392            position=(40, self._sub_height - 26),
393            size=(0, 0),
394            scale=1.0,
395            maxwidth=400,
396            color=bui.app.ui_v1.title_color,
397            h_align='left',
398            v_align='center',
399        )
400
401        index = 0
402        appconfig = bui.app.config
403
404        mesh_opaque = bui.getmesh('level_select_button_opaque')
405        mesh_transparent = bui.getmesh('level_select_button_transparent')
406        mask_tex = bui.gettexture('mapPreviewMask')
407
408        h_offs = 225 if count == 1 else 115 if count == 2 else 0
409        h_offs_bottom = 0
410
411        uiscale = bui.app.ui_v1.uiscale
412        for y in range(rows):
413            for x in range(columns):
414                name = items[index][0]
415                assert name is not None
416                pos = (
417                    x * (button_width + 2 * button_buffer_h)
418                    + button_buffer_h
419                    + 8
420                    + h_offs,
421                    self._sub_height
422                    - 47
423                    - (y + 1) * (button_height + 2 * button_buffer_v),
424                )
425                btn = bui.buttonwidget(
426                    parent=self._subcontainer,
427                    button_type='square',
428                    size=(button_width, button_height),
429                    autoselect=True,
430                    label='',
431                    position=pos,
432                )
433
434                if x == 0 and uiscale is bui.UIScale.SMALL:
435                    bui.widget(
436                        edit=btn,
437                        left_widget=bui.get_special_widget('back_button'),
438                    )
439                if x == columns - 1 and uiscale is bui.UIScale.SMALL:
440                    bui.widget(
441                        edit=btn,
442                        right_widget=bui.get_special_widget('squad_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_window(
696            PlaylistCustomizeBrowserWindow(
697                origin_widget=self._customize_button,
698                sessiontype=self._sessiontype,
699            ),
700            from_window=self,
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[self._pvars.config_name + ' Playlist Selection'] = (
719                    self._selected_playlist
720                )
721                cfg.commit()
722
723        self.main_window_back()
724
725        # self._save_state()
726        # bui.containerwidget(
727        #     edit=self._root_widget, transition=self._transition_out
728        # )
729        # assert bui.app.classic is not None
730        # bui.app.ui_v1.set_main_window(
731        #     PlayWindow(transition='in_left'), from_window=self, is_back=True
732        # )
733
734    def _save_state(self) -> None:
735        try:
736            sel = self._root_widget.get_selected_child()
737            if sel == self._back_button:
738                sel_name = 'Back'
739            elif sel == self._scrollwidget:
740                assert self._subcontainer is not None
741                subsel = self._subcontainer.get_selected_child()
742                if subsel == self._customize_button:
743                    sel_name = 'Customize'
744                else:
745                    sel_name = 'Scroll'
746            else:
747                raise RuntimeError('Unrecognized selected widget.')
748            assert bui.app.classic is not None
749            bui.app.ui_v1.window_states[type(self)] = sel_name
750        except Exception:
751            logging.exception('Error saving state for %s.', self)
752
753    def _restore_state(self) -> None:
754        try:
755            assert bui.app.classic is not None
756            sel_name = bui.app.ui_v1.window_states.get(type(self))
757            if sel_name == 'Back':
758                sel = self._back_button
759            elif sel_name == 'Scroll':
760                sel = self._scrollwidget
761            elif sel_name == 'Customize':
762                sel = self._scrollwidget
763                bui.containerwidget(
764                    edit=self._subcontainer,
765                    selected_child=self._customize_button,
766                    visible_child=self._customize_button,
767                )
768            else:
769                sel = self._scrollwidget
770            bui.containerwidget(edit=self._root_widget, selected_child=sel)
771        except Exception:
772            logging.exception('Error restoring state for %s.', self)

Window for starting teams games.

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

Create a MainWindow given a root widget and transition info.

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

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
155    @override
156    def get_main_window_state(self) -> bui.MainWindowState:
157        # Support recreating our window for back/refresh purposes.
158        cls = type(self)
159
160        # Pull things out of self here; if we do it below in the lambda
161        # then we keep self alive.
162        sessiontype = self._sessiontype
163
164        return bui.BasicMainWindowState(
165            create_call=lambda transition, origin_widget: cls(
166                transition=transition,
167                origin_widget=origin_widget,
168                sessiontype=sessiontype,
169            )
170        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
172    @override
173    def on_main_window_close(self) -> None:
174        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_close
can_change_main_window
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget