bauiv1lib.playlist.browser

Provides a window for browsing and launching game playlists.

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

Window for starting teams games.

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

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

Return a WindowState to recreate this window, if supported.

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

Called before transitioning out a main window.

A good opportunity to save window state/etc.