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