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

Window for starting teams games.

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