bastd.ui.playlist.customizebrowser

Provides UI for viewing/creating/editing playlists.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides UI for viewing/creating/editing playlists."""
  4
  5from __future__ import annotations
  6
  7import copy
  8import time
  9from typing import TYPE_CHECKING
 10
 11import ba
 12import ba.internal
 13
 14if TYPE_CHECKING:
 15    from typing import Any
 16
 17
 18class PlaylistCustomizeBrowserWindow(ba.Window):
 19    """Window for viewing a playlist."""
 20
 21    def __init__(
 22        self,
 23        sessiontype: type[ba.Session],
 24        transition: str = 'in_right',
 25        select_playlist: str | None = None,
 26        origin_widget: ba.Widget | None = None,
 27    ):
 28        # Yes this needs tidying.
 29        # pylint: disable=too-many-locals
 30        # pylint: disable=too-many-statements
 31        # pylint: disable=cyclic-import
 32        from bastd.ui import playlist
 33
 34        scale_origin: tuple[float, float] | None
 35        if origin_widget is not None:
 36            self._transition_out = 'out_scale'
 37            scale_origin = origin_widget.get_screen_space_center()
 38            transition = 'in_scale'
 39        else:
 40            self._transition_out = 'out_right'
 41            scale_origin = None
 42
 43        self._sessiontype = sessiontype
 44        self._pvars = playlist.PlaylistTypeVars(sessiontype)
 45        self._max_playlists = 30
 46        self._r = 'gameListWindow'
 47        uiscale = ba.app.ui.uiscale
 48        self._width = 750.0 if uiscale is ba.UIScale.SMALL else 650.0
 49        x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0
 50        self._height = (
 51            380.0
 52            if uiscale is ba.UIScale.SMALL
 53            else 420.0
 54            if uiscale is ba.UIScale.MEDIUM
 55            else 500.0
 56        )
 57        top_extra = 20.0 if uiscale is ba.UIScale.SMALL else 0.0
 58
 59        super().__init__(
 60            root_widget=ba.containerwidget(
 61                size=(self._width, self._height + top_extra),
 62                transition=transition,
 63                scale_origin_stack_offset=scale_origin,
 64                scale=(
 65                    2.05
 66                    if uiscale is ba.UIScale.SMALL
 67                    else 1.5
 68                    if uiscale is ba.UIScale.MEDIUM
 69                    else 1.0
 70                ),
 71                stack_offset=(0, -10)
 72                if uiscale is ba.UIScale.SMALL
 73                else (0, 0),
 74            )
 75        )
 76
 77        self._back_button = back_button = btn = ba.buttonwidget(
 78            parent=self._root_widget,
 79            position=(43 + x_inset, self._height - 60),
 80            size=(160, 68),
 81            scale=0.77,
 82            autoselect=True,
 83            text_scale=1.3,
 84            label=ba.Lstr(resource='backText'),
 85            button_type='back',
 86        )
 87
 88        ba.textwidget(
 89            parent=self._root_widget,
 90            position=(0, self._height - 47),
 91            size=(self._width, 25),
 92            text=ba.Lstr(
 93                resource=self._r + '.titleText',
 94                subs=[('${TYPE}', self._pvars.window_title_name)],
 95            ),
 96            color=ba.app.ui.heading_color,
 97            maxwidth=290,
 98            h_align='center',
 99            v_align='center',
100        )
101
102        ba.buttonwidget(
103            edit=btn,
104            button_type='backSmall',
105            size=(60, 60),
106            label=ba.charstr(ba.SpecialChar.BACK),
107        )
108
109        v = self._height - 59.0
110        h = 41 + x_inset
111        b_color = (0.6, 0.53, 0.63)
112        b_textcolor = (0.75, 0.7, 0.8)
113        self._lock_images: list[ba.Widget] = []
114        lock_tex = ba.gettexture('lock')
115
116        scl = (
117            1.1
118            if uiscale is ba.UIScale.SMALL
119            else 1.27
120            if uiscale is ba.UIScale.MEDIUM
121            else 1.57
122        )
123        scl *= 0.63
124        v -= 65.0 * scl
125        new_button = btn = ba.buttonwidget(
126            parent=self._root_widget,
127            position=(h, v),
128            size=(90, 58.0 * scl),
129            on_activate_call=self._new_playlist,
130            color=b_color,
131            autoselect=True,
132            button_type='square',
133            textcolor=b_textcolor,
134            text_scale=0.7,
135            label=ba.Lstr(
136                resource='newText', fallback_resource=self._r + '.newText'
137            ),
138        )
139        self._lock_images.append(
140            ba.imagewidget(
141                parent=self._root_widget,
142                size=(30, 30),
143                draw_controller=btn,
144                position=(h - 10, v + 58.0 * scl - 28),
145                texture=lock_tex,
146            )
147        )
148
149        v -= 65.0 * scl
150        self._edit_button = edit_button = btn = ba.buttonwidget(
151            parent=self._root_widget,
152            position=(h, v),
153            size=(90, 58.0 * scl),
154            on_activate_call=self._edit_playlist,
155            color=b_color,
156            autoselect=True,
157            textcolor=b_textcolor,
158            button_type='square',
159            text_scale=0.7,
160            label=ba.Lstr(
161                resource='editText', fallback_resource=self._r + '.editText'
162            ),
163        )
164        self._lock_images.append(
165            ba.imagewidget(
166                parent=self._root_widget,
167                size=(30, 30),
168                draw_controller=btn,
169                position=(h - 10, v + 58.0 * scl - 28),
170                texture=lock_tex,
171            )
172        )
173
174        v -= 65.0 * scl
175        duplicate_button = btn = ba.buttonwidget(
176            parent=self._root_widget,
177            position=(h, v),
178            size=(90, 58.0 * scl),
179            on_activate_call=self._duplicate_playlist,
180            color=b_color,
181            autoselect=True,
182            textcolor=b_textcolor,
183            button_type='square',
184            text_scale=0.7,
185            label=ba.Lstr(
186                resource='duplicateText',
187                fallback_resource=self._r + '.duplicateText',
188            ),
189        )
190        self._lock_images.append(
191            ba.imagewidget(
192                parent=self._root_widget,
193                size=(30, 30),
194                draw_controller=btn,
195                position=(h - 10, v + 58.0 * scl - 28),
196                texture=lock_tex,
197            )
198        )
199
200        v -= 65.0 * scl
201        delete_button = btn = ba.buttonwidget(
202            parent=self._root_widget,
203            position=(h, v),
204            size=(90, 58.0 * scl),
205            on_activate_call=self._delete_playlist,
206            color=b_color,
207            autoselect=True,
208            textcolor=b_textcolor,
209            button_type='square',
210            text_scale=0.7,
211            label=ba.Lstr(
212                resource='deleteText', fallback_resource=self._r + '.deleteText'
213            ),
214        )
215        self._lock_images.append(
216            ba.imagewidget(
217                parent=self._root_widget,
218                size=(30, 30),
219                draw_controller=btn,
220                position=(h - 10, v + 58.0 * scl - 28),
221                texture=lock_tex,
222            )
223        )
224        v -= 65.0 * scl
225        self._import_button = ba.buttonwidget(
226            parent=self._root_widget,
227            position=(h, v),
228            size=(90, 58.0 * scl),
229            on_activate_call=self._import_playlist,
230            color=b_color,
231            autoselect=True,
232            textcolor=b_textcolor,
233            button_type='square',
234            text_scale=0.7,
235            label=ba.Lstr(resource='importText'),
236        )
237        v -= 65.0 * scl
238        btn = ba.buttonwidget(
239            parent=self._root_widget,
240            position=(h, v),
241            size=(90, 58.0 * scl),
242            on_activate_call=self._share_playlist,
243            color=b_color,
244            autoselect=True,
245            textcolor=b_textcolor,
246            button_type='square',
247            text_scale=0.7,
248            label=ba.Lstr(resource='shareText'),
249        )
250        self._lock_images.append(
251            ba.imagewidget(
252                parent=self._root_widget,
253                size=(30, 30),
254                draw_controller=btn,
255                position=(h - 10, v + 58.0 * scl - 28),
256                texture=lock_tex,
257            )
258        )
259
260        v = self._height - 75
261        self._scroll_height = self._height - 119
262        scrollwidget = ba.scrollwidget(
263            parent=self._root_widget,
264            position=(140 + x_inset, v - self._scroll_height),
265            size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10),
266            highlight=False,
267        )
268        ba.widget(edit=back_button, right_widget=scrollwidget)
269        self._columnwidget = ba.columnwidget(
270            parent=scrollwidget, border=2, margin=0
271        )
272
273        h = 145
274
275        self._do_randomize_val = ba.app.config.get(
276            self._pvars.config_name + ' Playlist Randomize', 0
277        )
278
279        h += 210
280
281        for btn in [new_button, delete_button, edit_button, duplicate_button]:
282            ba.widget(edit=btn, right_widget=scrollwidget)
283        ba.widget(
284            edit=scrollwidget,
285            left_widget=new_button,
286            right_widget=ba.internal.get_special_widget('party_button')
287            if ba.app.ui.use_toolbars
288            else None,
289        )
290
291        # make sure config exists
292        self._config_name_full = self._pvars.config_name + ' Playlists'
293
294        if self._config_name_full not in ba.app.config:
295            ba.app.config[self._config_name_full] = {}
296
297        self._selected_playlist_name: str | None = None
298        self._selected_playlist_index: int | None = None
299        self._playlist_widgets: list[ba.Widget] = []
300
301        self._refresh(select_playlist=select_playlist)
302
303        ba.buttonwidget(edit=back_button, on_activate_call=self._back)
304        ba.containerwidget(edit=self._root_widget, cancel_button=back_button)
305
306        ba.containerwidget(edit=self._root_widget, selected_child=scrollwidget)
307
308        # Keep our lock images up to date/etc.
309        self._update_timer = ba.Timer(
310            1.0,
311            ba.WeakCall(self._update),
312            timetype=ba.TimeType.REAL,
313            repeat=True,
314        )
315        self._update()
316
317    def _update(self) -> None:
318        have = ba.app.accounts_v1.have_pro_options()
319        for lock in self._lock_images:
320            ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0)
321
322    def _back(self) -> None:
323        # pylint: disable=cyclic-import
324        from bastd.ui.playlist import browser
325
326        if self._selected_playlist_name is not None:
327            cfg = ba.app.config
328            cfg[
329                self._pvars.config_name + ' Playlist Selection'
330            ] = self._selected_playlist_name
331            cfg.commit()
332
333        ba.containerwidget(
334            edit=self._root_widget, transition=self._transition_out
335        )
336        ba.app.ui.set_main_menu_window(
337            browser.PlaylistBrowserWindow(
338                transition='in_left', sessiontype=self._sessiontype
339            ).get_root_widget()
340        )
341
342    def _select(self, name: str, index: int) -> None:
343        self._selected_playlist_name = name
344        self._selected_playlist_index = index
345
346    def _run_selected_playlist(self) -> None:
347        # pylint: disable=cyclic-import
348        ba.internal.unlock_all_input()
349        try:
350            ba.internal.new_host_session(self._sessiontype)
351        except Exception:
352            from bastd import mainmenu
353
354            ba.print_exception(f'Error running session {self._sessiontype}.')
355
356            # Drop back into a main menu session.
357            ba.internal.new_host_session(mainmenu.MainMenuSession)
358
359    def _choose_playlist(self) -> None:
360        if self._selected_playlist_name is None:
361            return
362        self._save_playlist_selection()
363        ba.containerwidget(edit=self._root_widget, transition='out_left')
364        ba.internal.fade_screen(False, endcall=self._run_selected_playlist)
365        ba.internal.lock_all_input()
366
367    def _refresh(self, select_playlist: str | None = None) -> None:
368        from efro.util import asserttype
369
370        old_selection = self._selected_playlist_name
371
372        # If there was no prev selection, look in prefs.
373        if old_selection is None:
374            old_selection = ba.app.config.get(
375                self._pvars.config_name + ' Playlist Selection'
376            )
377
378        old_selection_index = self._selected_playlist_index
379
380        # Delete old.
381        while self._playlist_widgets:
382            self._playlist_widgets.pop().delete()
383
384        items = list(ba.app.config[self._config_name_full].items())
385
386        # Make sure everything is unicode now.
387        items = [
388            (i[0].decode(), i[1]) if not isinstance(i[0], str) else i
389            for i in items
390        ]
391
392        items.sort(key=lambda x: asserttype(x[0], str).lower())
393
394        items = [['__default__', None]] + items  # Default is always first.
395        index = 0
396        for pname, _ in items:
397            assert pname is not None
398            txtw = ba.textwidget(
399                parent=self._columnwidget,
400                size=(self._width - 40, 30),
401                maxwidth=self._width - 110,
402                text=self._get_playlist_display_name(pname),
403                h_align='left',
404                v_align='center',
405                color=(0.6, 0.6, 0.7, 1.0)
406                if pname == '__default__'
407                else (0.85, 0.85, 0.85, 1),
408                always_highlight=True,
409                on_select_call=ba.Call(self._select, pname, index),
410                on_activate_call=ba.Call(self._edit_button.activate),
411                selectable=True,
412            )
413            ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50)
414
415            # Hitting up from top widget should jump to 'back'
416            if index == 0:
417                ba.widget(edit=txtw, up_widget=self._back_button)
418
419            self._playlist_widgets.append(txtw)
420
421            # Select this one if the user requested it.
422            if select_playlist is not None:
423                if pname == select_playlist:
424                    ba.columnwidget(
425                        edit=self._columnwidget,
426                        selected_child=txtw,
427                        visible_child=txtw,
428                    )
429            else:
430                # Select this one if it was previously selected.
431                # Go by index if there's one.
432                if old_selection_index is not None:
433                    if index == old_selection_index:
434                        ba.columnwidget(
435                            edit=self._columnwidget,
436                            selected_child=txtw,
437                            visible_child=txtw,
438                        )
439                else:  # Otherwise look by name.
440                    if pname == old_selection:
441                        ba.columnwidget(
442                            edit=self._columnwidget,
443                            selected_child=txtw,
444                            visible_child=txtw,
445                        )
446
447            index += 1
448
449    def _save_playlist_selection(self) -> None:
450        # Store the selected playlist in prefs.
451        # This serves dual purposes of letting us re-select it next time
452        # if we want and also lets us pass it to the game (since we reset
453        # the whole python environment that's not actually easy).
454        cfg = ba.app.config
455        cfg[
456            self._pvars.config_name + ' Playlist Selection'
457        ] = self._selected_playlist_name
458        cfg[
459            self._pvars.config_name + ' Playlist Randomize'
460        ] = self._do_randomize_val
461        cfg.commit()
462
463    def _new_playlist(self) -> None:
464        # pylint: disable=cyclic-import
465        from bastd.ui.playlist.editcontroller import PlaylistEditController
466        from bastd.ui.purchase import PurchaseWindow
467
468        if not ba.app.accounts_v1.have_pro_options():
469            PurchaseWindow(items=['pro'])
470            return
471
472        # Clamp at our max playlist number.
473        if len(ba.app.config[self._config_name_full]) > self._max_playlists:
474            ba.screenmessage(
475                ba.Lstr(
476                    translate=(
477                        'serverResponses',
478                        'Max number of playlists reached.',
479                    )
480                ),
481                color=(1, 0, 0),
482            )
483            ba.playsound(ba.getsound('error'))
484            return
485
486        # In case they cancel so we can return to this state.
487        self._save_playlist_selection()
488
489        # Kick off the edit UI.
490        PlaylistEditController(sessiontype=self._sessiontype)
491        ba.containerwidget(edit=self._root_widget, transition='out_left')
492
493    def _edit_playlist(self) -> None:
494        # pylint: disable=cyclic-import
495        from bastd.ui.playlist.editcontroller import PlaylistEditController
496        from bastd.ui.purchase import PurchaseWindow
497
498        if not ba.app.accounts_v1.have_pro_options():
499            PurchaseWindow(items=['pro'])
500            return
501        if self._selected_playlist_name is None:
502            return
503        if self._selected_playlist_name == '__default__':
504            ba.playsound(ba.getsound('error'))
505            ba.screenmessage(ba.Lstr(resource=self._r + '.cantEditDefaultText'))
506            return
507        self._save_playlist_selection()
508        PlaylistEditController(
509            existing_playlist_name=self._selected_playlist_name,
510            sessiontype=self._sessiontype,
511        )
512        ba.containerwidget(edit=self._root_widget, transition='out_left')
513
514    def _do_delete_playlist(self) -> None:
515        ba.internal.add_transaction(
516            {
517                'type': 'REMOVE_PLAYLIST',
518                'playlistType': self._pvars.config_name,
519                'playlistName': self._selected_playlist_name,
520            }
521        )
522        ba.internal.run_transactions()
523        ba.playsound(ba.getsound('shieldDown'))
524
525        # (we don't use len()-1 here because the default list adds one)
526        assert self._selected_playlist_index is not None
527        if self._selected_playlist_index > len(
528            ba.app.config[self._pvars.config_name + ' Playlists']
529        ):
530            self._selected_playlist_index = len(
531                ba.app.config[self._pvars.config_name + ' Playlists']
532            )
533        self._refresh()
534
535    def _import_playlist(self) -> None:
536        # pylint: disable=cyclic-import
537        from bastd.ui.playlist import share
538
539        # Gotta be signed in for this to work.
540        if ba.internal.get_v1_account_state() != 'signed_in':
541            ba.screenmessage(
542                ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
543            )
544            ba.playsound(ba.getsound('error'))
545            return
546
547        share.SharePlaylistImportWindow(
548            origin_widget=self._import_button,
549            on_success_callback=ba.WeakCall(self._on_playlist_import_success),
550        )
551
552    def _on_playlist_import_success(self) -> None:
553        self._refresh()
554
555    def _on_share_playlist_response(self, name: str, response: Any) -> None:
556        # pylint: disable=cyclic-import
557        from bastd.ui.playlist import share
558
559        if response is None:
560            ba.screenmessage(
561                ba.Lstr(resource='internal.unavailableNoConnectionText'),
562                color=(1, 0, 0),
563            )
564            ba.playsound(ba.getsound('error'))
565            return
566        share.SharePlaylistResultsWindow(name, response)
567
568    def _share_playlist(self) -> None:
569        # pylint: disable=cyclic-import
570        from bastd.ui.purchase import PurchaseWindow
571
572        if not ba.app.accounts_v1.have_pro_options():
573            PurchaseWindow(items=['pro'])
574            return
575
576        # Gotta be signed in for this to work.
577        if ba.internal.get_v1_account_state() != 'signed_in':
578            ba.screenmessage(
579                ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
580            )
581            ba.playsound(ba.getsound('error'))
582            return
583        if self._selected_playlist_name == '__default__':
584            ba.playsound(ba.getsound('error'))
585            ba.screenmessage(
586                ba.Lstr(resource=self._r + '.cantShareDefaultText'),
587                color=(1, 0, 0),
588            )
589            return
590
591        if self._selected_playlist_name is None:
592            return
593
594        ba.internal.add_transaction(
595            {
596                'type': 'SHARE_PLAYLIST',
597                'expire_time': time.time() + 5,
598                'playlistType': self._pvars.config_name,
599                'playlistName': self._selected_playlist_name,
600            },
601            callback=ba.WeakCall(
602                self._on_share_playlist_response, self._selected_playlist_name
603            ),
604        )
605        ba.internal.run_transactions()
606        ba.screenmessage(ba.Lstr(resource='sharingText'))
607
608    def _delete_playlist(self) -> None:
609        # pylint: disable=cyclic-import
610        from bastd.ui.purchase import PurchaseWindow
611        from bastd.ui.confirm import ConfirmWindow
612
613        if not ba.app.accounts_v1.have_pro_options():
614            PurchaseWindow(items=['pro'])
615            return
616
617        if self._selected_playlist_name is None:
618            return
619        if self._selected_playlist_name == '__default__':
620            ba.playsound(ba.getsound('error'))
621            ba.screenmessage(
622                ba.Lstr(resource=self._r + '.cantDeleteDefaultText')
623            )
624        else:
625            ConfirmWindow(
626                ba.Lstr(
627                    resource=self._r + '.deleteConfirmText',
628                    subs=[('${LIST}', self._selected_playlist_name)],
629                ),
630                self._do_delete_playlist,
631                450,
632                150,
633            )
634
635    def _get_playlist_display_name(self, playlist: str) -> ba.Lstr:
636        if playlist == '__default__':
637            return self._pvars.default_list_name
638        return (
639            playlist
640            if isinstance(playlist, ba.Lstr)
641            else ba.Lstr(value=playlist)
642        )
643
644    def _duplicate_playlist(self) -> None:
645        # pylint: disable=too-many-branches
646        # pylint: disable=cyclic-import
647        from bastd.ui.purchase import PurchaseWindow
648
649        if not ba.app.accounts_v1.have_pro_options():
650            PurchaseWindow(items=['pro'])
651            return
652        if self._selected_playlist_name is None:
653            return
654        plst: list[dict[str, Any]] | None
655        if self._selected_playlist_name == '__default__':
656            plst = self._pvars.get_default_list_call()
657        else:
658            plst = ba.app.config[self._config_name_full].get(
659                self._selected_playlist_name
660            )
661            if plst is None:
662                ba.playsound(ba.getsound('error'))
663                return
664
665        # clamp at our max playlist number
666        if len(ba.app.config[self._config_name_full]) > self._max_playlists:
667            ba.screenmessage(
668                ba.Lstr(
669                    translate=(
670                        'serverResponses',
671                        'Max number of playlists reached.',
672                    )
673                ),
674                color=(1, 0, 0),
675            )
676            ba.playsound(ba.getsound('error'))
677            return
678
679        copy_text = ba.Lstr(resource='copyOfText').evaluate()
680        # get just 'Copy' or whatnot
681        copy_word = copy_text.replace('${NAME}', '').strip()
682        # find a valid dup name that doesn't exist
683
684        test_index = 1
685        base_name = self._get_playlist_display_name(
686            self._selected_playlist_name
687        ).evaluate()
688
689        # If it looks like a copy, strip digits and spaces off the end.
690        if copy_word in base_name:
691            while base_name[-1].isdigit() or base_name[-1] == ' ':
692                base_name = base_name[:-1]
693        while True:
694            if copy_word in base_name:
695                test_name = base_name
696            else:
697                test_name = copy_text.replace('${NAME}', base_name)
698            if test_index > 1:
699                test_name += ' ' + str(test_index)
700            if test_name not in ba.app.config[self._config_name_full]:
701                break
702            test_index += 1
703
704        ba.internal.add_transaction(
705            {
706                'type': 'ADD_PLAYLIST',
707                'playlistType': self._pvars.config_name,
708                'playlistName': test_name,
709                'playlist': copy.deepcopy(plst),
710            }
711        )
712        ba.internal.run_transactions()
713
714        ba.playsound(ba.getsound('gunCocking'))
715        self._refresh(select_playlist=test_name)
class PlaylistCustomizeBrowserWindow(ba.ui.Window):
 19class PlaylistCustomizeBrowserWindow(ba.Window):
 20    """Window for viewing a playlist."""
 21
 22    def __init__(
 23        self,
 24        sessiontype: type[ba.Session],
 25        transition: str = 'in_right',
 26        select_playlist: str | None = None,
 27        origin_widget: ba.Widget | None = None,
 28    ):
 29        # Yes this needs tidying.
 30        # pylint: disable=too-many-locals
 31        # pylint: disable=too-many-statements
 32        # pylint: disable=cyclic-import
 33        from bastd.ui import playlist
 34
 35        scale_origin: tuple[float, float] | None
 36        if origin_widget is not None:
 37            self._transition_out = 'out_scale'
 38            scale_origin = origin_widget.get_screen_space_center()
 39            transition = 'in_scale'
 40        else:
 41            self._transition_out = 'out_right'
 42            scale_origin = None
 43
 44        self._sessiontype = sessiontype
 45        self._pvars = playlist.PlaylistTypeVars(sessiontype)
 46        self._max_playlists = 30
 47        self._r = 'gameListWindow'
 48        uiscale = ba.app.ui.uiscale
 49        self._width = 750.0 if uiscale is ba.UIScale.SMALL else 650.0
 50        x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0
 51        self._height = (
 52            380.0
 53            if uiscale is ba.UIScale.SMALL
 54            else 420.0
 55            if uiscale is ba.UIScale.MEDIUM
 56            else 500.0
 57        )
 58        top_extra = 20.0 if uiscale is ba.UIScale.SMALL else 0.0
 59
 60        super().__init__(
 61            root_widget=ba.containerwidget(
 62                size=(self._width, self._height + top_extra),
 63                transition=transition,
 64                scale_origin_stack_offset=scale_origin,
 65                scale=(
 66                    2.05
 67                    if uiscale is ba.UIScale.SMALL
 68                    else 1.5
 69                    if uiscale is ba.UIScale.MEDIUM
 70                    else 1.0
 71                ),
 72                stack_offset=(0, -10)
 73                if uiscale is ba.UIScale.SMALL
 74                else (0, 0),
 75            )
 76        )
 77
 78        self._back_button = back_button = btn = ba.buttonwidget(
 79            parent=self._root_widget,
 80            position=(43 + x_inset, self._height - 60),
 81            size=(160, 68),
 82            scale=0.77,
 83            autoselect=True,
 84            text_scale=1.3,
 85            label=ba.Lstr(resource='backText'),
 86            button_type='back',
 87        )
 88
 89        ba.textwidget(
 90            parent=self._root_widget,
 91            position=(0, self._height - 47),
 92            size=(self._width, 25),
 93            text=ba.Lstr(
 94                resource=self._r + '.titleText',
 95                subs=[('${TYPE}', self._pvars.window_title_name)],
 96            ),
 97            color=ba.app.ui.heading_color,
 98            maxwidth=290,
 99            h_align='center',
100            v_align='center',
101        )
102
103        ba.buttonwidget(
104            edit=btn,
105            button_type='backSmall',
106            size=(60, 60),
107            label=ba.charstr(ba.SpecialChar.BACK),
108        )
109
110        v = self._height - 59.0
111        h = 41 + x_inset
112        b_color = (0.6, 0.53, 0.63)
113        b_textcolor = (0.75, 0.7, 0.8)
114        self._lock_images: list[ba.Widget] = []
115        lock_tex = ba.gettexture('lock')
116
117        scl = (
118            1.1
119            if uiscale is ba.UIScale.SMALL
120            else 1.27
121            if uiscale is ba.UIScale.MEDIUM
122            else 1.57
123        )
124        scl *= 0.63
125        v -= 65.0 * scl
126        new_button = btn = ba.buttonwidget(
127            parent=self._root_widget,
128            position=(h, v),
129            size=(90, 58.0 * scl),
130            on_activate_call=self._new_playlist,
131            color=b_color,
132            autoselect=True,
133            button_type='square',
134            textcolor=b_textcolor,
135            text_scale=0.7,
136            label=ba.Lstr(
137                resource='newText', fallback_resource=self._r + '.newText'
138            ),
139        )
140        self._lock_images.append(
141            ba.imagewidget(
142                parent=self._root_widget,
143                size=(30, 30),
144                draw_controller=btn,
145                position=(h - 10, v + 58.0 * scl - 28),
146                texture=lock_tex,
147            )
148        )
149
150        v -= 65.0 * scl
151        self._edit_button = edit_button = btn = ba.buttonwidget(
152            parent=self._root_widget,
153            position=(h, v),
154            size=(90, 58.0 * scl),
155            on_activate_call=self._edit_playlist,
156            color=b_color,
157            autoselect=True,
158            textcolor=b_textcolor,
159            button_type='square',
160            text_scale=0.7,
161            label=ba.Lstr(
162                resource='editText', fallback_resource=self._r + '.editText'
163            ),
164        )
165        self._lock_images.append(
166            ba.imagewidget(
167                parent=self._root_widget,
168                size=(30, 30),
169                draw_controller=btn,
170                position=(h - 10, v + 58.0 * scl - 28),
171                texture=lock_tex,
172            )
173        )
174
175        v -= 65.0 * scl
176        duplicate_button = btn = ba.buttonwidget(
177            parent=self._root_widget,
178            position=(h, v),
179            size=(90, 58.0 * scl),
180            on_activate_call=self._duplicate_playlist,
181            color=b_color,
182            autoselect=True,
183            textcolor=b_textcolor,
184            button_type='square',
185            text_scale=0.7,
186            label=ba.Lstr(
187                resource='duplicateText',
188                fallback_resource=self._r + '.duplicateText',
189            ),
190        )
191        self._lock_images.append(
192            ba.imagewidget(
193                parent=self._root_widget,
194                size=(30, 30),
195                draw_controller=btn,
196                position=(h - 10, v + 58.0 * scl - 28),
197                texture=lock_tex,
198            )
199        )
200
201        v -= 65.0 * scl
202        delete_button = btn = ba.buttonwidget(
203            parent=self._root_widget,
204            position=(h, v),
205            size=(90, 58.0 * scl),
206            on_activate_call=self._delete_playlist,
207            color=b_color,
208            autoselect=True,
209            textcolor=b_textcolor,
210            button_type='square',
211            text_scale=0.7,
212            label=ba.Lstr(
213                resource='deleteText', fallback_resource=self._r + '.deleteText'
214            ),
215        )
216        self._lock_images.append(
217            ba.imagewidget(
218                parent=self._root_widget,
219                size=(30, 30),
220                draw_controller=btn,
221                position=(h - 10, v + 58.0 * scl - 28),
222                texture=lock_tex,
223            )
224        )
225        v -= 65.0 * scl
226        self._import_button = ba.buttonwidget(
227            parent=self._root_widget,
228            position=(h, v),
229            size=(90, 58.0 * scl),
230            on_activate_call=self._import_playlist,
231            color=b_color,
232            autoselect=True,
233            textcolor=b_textcolor,
234            button_type='square',
235            text_scale=0.7,
236            label=ba.Lstr(resource='importText'),
237        )
238        v -= 65.0 * scl
239        btn = ba.buttonwidget(
240            parent=self._root_widget,
241            position=(h, v),
242            size=(90, 58.0 * scl),
243            on_activate_call=self._share_playlist,
244            color=b_color,
245            autoselect=True,
246            textcolor=b_textcolor,
247            button_type='square',
248            text_scale=0.7,
249            label=ba.Lstr(resource='shareText'),
250        )
251        self._lock_images.append(
252            ba.imagewidget(
253                parent=self._root_widget,
254                size=(30, 30),
255                draw_controller=btn,
256                position=(h - 10, v + 58.0 * scl - 28),
257                texture=lock_tex,
258            )
259        )
260
261        v = self._height - 75
262        self._scroll_height = self._height - 119
263        scrollwidget = ba.scrollwidget(
264            parent=self._root_widget,
265            position=(140 + x_inset, v - self._scroll_height),
266            size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10),
267            highlight=False,
268        )
269        ba.widget(edit=back_button, right_widget=scrollwidget)
270        self._columnwidget = ba.columnwidget(
271            parent=scrollwidget, border=2, margin=0
272        )
273
274        h = 145
275
276        self._do_randomize_val = ba.app.config.get(
277            self._pvars.config_name + ' Playlist Randomize', 0
278        )
279
280        h += 210
281
282        for btn in [new_button, delete_button, edit_button, duplicate_button]:
283            ba.widget(edit=btn, right_widget=scrollwidget)
284        ba.widget(
285            edit=scrollwidget,
286            left_widget=new_button,
287            right_widget=ba.internal.get_special_widget('party_button')
288            if ba.app.ui.use_toolbars
289            else None,
290        )
291
292        # make sure config exists
293        self._config_name_full = self._pvars.config_name + ' Playlists'
294
295        if self._config_name_full not in ba.app.config:
296            ba.app.config[self._config_name_full] = {}
297
298        self._selected_playlist_name: str | None = None
299        self._selected_playlist_index: int | None = None
300        self._playlist_widgets: list[ba.Widget] = []
301
302        self._refresh(select_playlist=select_playlist)
303
304        ba.buttonwidget(edit=back_button, on_activate_call=self._back)
305        ba.containerwidget(edit=self._root_widget, cancel_button=back_button)
306
307        ba.containerwidget(edit=self._root_widget, selected_child=scrollwidget)
308
309        # Keep our lock images up to date/etc.
310        self._update_timer = ba.Timer(
311            1.0,
312            ba.WeakCall(self._update),
313            timetype=ba.TimeType.REAL,
314            repeat=True,
315        )
316        self._update()
317
318    def _update(self) -> None:
319        have = ba.app.accounts_v1.have_pro_options()
320        for lock in self._lock_images:
321            ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0)
322
323    def _back(self) -> None:
324        # pylint: disable=cyclic-import
325        from bastd.ui.playlist import browser
326
327        if self._selected_playlist_name is not None:
328            cfg = ba.app.config
329            cfg[
330                self._pvars.config_name + ' Playlist Selection'
331            ] = self._selected_playlist_name
332            cfg.commit()
333
334        ba.containerwidget(
335            edit=self._root_widget, transition=self._transition_out
336        )
337        ba.app.ui.set_main_menu_window(
338            browser.PlaylistBrowserWindow(
339                transition='in_left', sessiontype=self._sessiontype
340            ).get_root_widget()
341        )
342
343    def _select(self, name: str, index: int) -> None:
344        self._selected_playlist_name = name
345        self._selected_playlist_index = index
346
347    def _run_selected_playlist(self) -> None:
348        # pylint: disable=cyclic-import
349        ba.internal.unlock_all_input()
350        try:
351            ba.internal.new_host_session(self._sessiontype)
352        except Exception:
353            from bastd import mainmenu
354
355            ba.print_exception(f'Error running session {self._sessiontype}.')
356
357            # Drop back into a main menu session.
358            ba.internal.new_host_session(mainmenu.MainMenuSession)
359
360    def _choose_playlist(self) -> None:
361        if self._selected_playlist_name is None:
362            return
363        self._save_playlist_selection()
364        ba.containerwidget(edit=self._root_widget, transition='out_left')
365        ba.internal.fade_screen(False, endcall=self._run_selected_playlist)
366        ba.internal.lock_all_input()
367
368    def _refresh(self, select_playlist: str | None = None) -> None:
369        from efro.util import asserttype
370
371        old_selection = self._selected_playlist_name
372
373        # If there was no prev selection, look in prefs.
374        if old_selection is None:
375            old_selection = ba.app.config.get(
376                self._pvars.config_name + ' Playlist Selection'
377            )
378
379        old_selection_index = self._selected_playlist_index
380
381        # Delete old.
382        while self._playlist_widgets:
383            self._playlist_widgets.pop().delete()
384
385        items = list(ba.app.config[self._config_name_full].items())
386
387        # Make sure everything is unicode now.
388        items = [
389            (i[0].decode(), i[1]) if not isinstance(i[0], str) else i
390            for i in items
391        ]
392
393        items.sort(key=lambda x: asserttype(x[0], str).lower())
394
395        items = [['__default__', None]] + items  # Default is always first.
396        index = 0
397        for pname, _ in items:
398            assert pname is not None
399            txtw = ba.textwidget(
400                parent=self._columnwidget,
401                size=(self._width - 40, 30),
402                maxwidth=self._width - 110,
403                text=self._get_playlist_display_name(pname),
404                h_align='left',
405                v_align='center',
406                color=(0.6, 0.6, 0.7, 1.0)
407                if pname == '__default__'
408                else (0.85, 0.85, 0.85, 1),
409                always_highlight=True,
410                on_select_call=ba.Call(self._select, pname, index),
411                on_activate_call=ba.Call(self._edit_button.activate),
412                selectable=True,
413            )
414            ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50)
415
416            # Hitting up from top widget should jump to 'back'
417            if index == 0:
418                ba.widget(edit=txtw, up_widget=self._back_button)
419
420            self._playlist_widgets.append(txtw)
421
422            # Select this one if the user requested it.
423            if select_playlist is not None:
424                if pname == select_playlist:
425                    ba.columnwidget(
426                        edit=self._columnwidget,
427                        selected_child=txtw,
428                        visible_child=txtw,
429                    )
430            else:
431                # Select this one if it was previously selected.
432                # Go by index if there's one.
433                if old_selection_index is not None:
434                    if index == old_selection_index:
435                        ba.columnwidget(
436                            edit=self._columnwidget,
437                            selected_child=txtw,
438                            visible_child=txtw,
439                        )
440                else:  # Otherwise look by name.
441                    if pname == old_selection:
442                        ba.columnwidget(
443                            edit=self._columnwidget,
444                            selected_child=txtw,
445                            visible_child=txtw,
446                        )
447
448            index += 1
449
450    def _save_playlist_selection(self) -> None:
451        # Store the selected playlist in prefs.
452        # This serves dual purposes of letting us re-select it next time
453        # if we want and also lets us pass it to the game (since we reset
454        # the whole python environment that's not actually easy).
455        cfg = ba.app.config
456        cfg[
457            self._pvars.config_name + ' Playlist Selection'
458        ] = self._selected_playlist_name
459        cfg[
460            self._pvars.config_name + ' Playlist Randomize'
461        ] = self._do_randomize_val
462        cfg.commit()
463
464    def _new_playlist(self) -> None:
465        # pylint: disable=cyclic-import
466        from bastd.ui.playlist.editcontroller import PlaylistEditController
467        from bastd.ui.purchase import PurchaseWindow
468
469        if not ba.app.accounts_v1.have_pro_options():
470            PurchaseWindow(items=['pro'])
471            return
472
473        # Clamp at our max playlist number.
474        if len(ba.app.config[self._config_name_full]) > self._max_playlists:
475            ba.screenmessage(
476                ba.Lstr(
477                    translate=(
478                        'serverResponses',
479                        'Max number of playlists reached.',
480                    )
481                ),
482                color=(1, 0, 0),
483            )
484            ba.playsound(ba.getsound('error'))
485            return
486
487        # In case they cancel so we can return to this state.
488        self._save_playlist_selection()
489
490        # Kick off the edit UI.
491        PlaylistEditController(sessiontype=self._sessiontype)
492        ba.containerwidget(edit=self._root_widget, transition='out_left')
493
494    def _edit_playlist(self) -> None:
495        # pylint: disable=cyclic-import
496        from bastd.ui.playlist.editcontroller import PlaylistEditController
497        from bastd.ui.purchase import PurchaseWindow
498
499        if not ba.app.accounts_v1.have_pro_options():
500            PurchaseWindow(items=['pro'])
501            return
502        if self._selected_playlist_name is None:
503            return
504        if self._selected_playlist_name == '__default__':
505            ba.playsound(ba.getsound('error'))
506            ba.screenmessage(ba.Lstr(resource=self._r + '.cantEditDefaultText'))
507            return
508        self._save_playlist_selection()
509        PlaylistEditController(
510            existing_playlist_name=self._selected_playlist_name,
511            sessiontype=self._sessiontype,
512        )
513        ba.containerwidget(edit=self._root_widget, transition='out_left')
514
515    def _do_delete_playlist(self) -> None:
516        ba.internal.add_transaction(
517            {
518                'type': 'REMOVE_PLAYLIST',
519                'playlistType': self._pvars.config_name,
520                'playlistName': self._selected_playlist_name,
521            }
522        )
523        ba.internal.run_transactions()
524        ba.playsound(ba.getsound('shieldDown'))
525
526        # (we don't use len()-1 here because the default list adds one)
527        assert self._selected_playlist_index is not None
528        if self._selected_playlist_index > len(
529            ba.app.config[self._pvars.config_name + ' Playlists']
530        ):
531            self._selected_playlist_index = len(
532                ba.app.config[self._pvars.config_name + ' Playlists']
533            )
534        self._refresh()
535
536    def _import_playlist(self) -> None:
537        # pylint: disable=cyclic-import
538        from bastd.ui.playlist import share
539
540        # Gotta be signed in for this to work.
541        if ba.internal.get_v1_account_state() != 'signed_in':
542            ba.screenmessage(
543                ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
544            )
545            ba.playsound(ba.getsound('error'))
546            return
547
548        share.SharePlaylistImportWindow(
549            origin_widget=self._import_button,
550            on_success_callback=ba.WeakCall(self._on_playlist_import_success),
551        )
552
553    def _on_playlist_import_success(self) -> None:
554        self._refresh()
555
556    def _on_share_playlist_response(self, name: str, response: Any) -> None:
557        # pylint: disable=cyclic-import
558        from bastd.ui.playlist import share
559
560        if response is None:
561            ba.screenmessage(
562                ba.Lstr(resource='internal.unavailableNoConnectionText'),
563                color=(1, 0, 0),
564            )
565            ba.playsound(ba.getsound('error'))
566            return
567        share.SharePlaylistResultsWindow(name, response)
568
569    def _share_playlist(self) -> None:
570        # pylint: disable=cyclic-import
571        from bastd.ui.purchase import PurchaseWindow
572
573        if not ba.app.accounts_v1.have_pro_options():
574            PurchaseWindow(items=['pro'])
575            return
576
577        # Gotta be signed in for this to work.
578        if ba.internal.get_v1_account_state() != 'signed_in':
579            ba.screenmessage(
580                ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
581            )
582            ba.playsound(ba.getsound('error'))
583            return
584        if self._selected_playlist_name == '__default__':
585            ba.playsound(ba.getsound('error'))
586            ba.screenmessage(
587                ba.Lstr(resource=self._r + '.cantShareDefaultText'),
588                color=(1, 0, 0),
589            )
590            return
591
592        if self._selected_playlist_name is None:
593            return
594
595        ba.internal.add_transaction(
596            {
597                'type': 'SHARE_PLAYLIST',
598                'expire_time': time.time() + 5,
599                'playlistType': self._pvars.config_name,
600                'playlistName': self._selected_playlist_name,
601            },
602            callback=ba.WeakCall(
603                self._on_share_playlist_response, self._selected_playlist_name
604            ),
605        )
606        ba.internal.run_transactions()
607        ba.screenmessage(ba.Lstr(resource='sharingText'))
608
609    def _delete_playlist(self) -> None:
610        # pylint: disable=cyclic-import
611        from bastd.ui.purchase import PurchaseWindow
612        from bastd.ui.confirm import ConfirmWindow
613
614        if not ba.app.accounts_v1.have_pro_options():
615            PurchaseWindow(items=['pro'])
616            return
617
618        if self._selected_playlist_name is None:
619            return
620        if self._selected_playlist_name == '__default__':
621            ba.playsound(ba.getsound('error'))
622            ba.screenmessage(
623                ba.Lstr(resource=self._r + '.cantDeleteDefaultText')
624            )
625        else:
626            ConfirmWindow(
627                ba.Lstr(
628                    resource=self._r + '.deleteConfirmText',
629                    subs=[('${LIST}', self._selected_playlist_name)],
630                ),
631                self._do_delete_playlist,
632                450,
633                150,
634            )
635
636    def _get_playlist_display_name(self, playlist: str) -> ba.Lstr:
637        if playlist == '__default__':
638            return self._pvars.default_list_name
639        return (
640            playlist
641            if isinstance(playlist, ba.Lstr)
642            else ba.Lstr(value=playlist)
643        )
644
645    def _duplicate_playlist(self) -> None:
646        # pylint: disable=too-many-branches
647        # pylint: disable=cyclic-import
648        from bastd.ui.purchase import PurchaseWindow
649
650        if not ba.app.accounts_v1.have_pro_options():
651            PurchaseWindow(items=['pro'])
652            return
653        if self._selected_playlist_name is None:
654            return
655        plst: list[dict[str, Any]] | None
656        if self._selected_playlist_name == '__default__':
657            plst = self._pvars.get_default_list_call()
658        else:
659            plst = ba.app.config[self._config_name_full].get(
660                self._selected_playlist_name
661            )
662            if plst is None:
663                ba.playsound(ba.getsound('error'))
664                return
665
666        # clamp at our max playlist number
667        if len(ba.app.config[self._config_name_full]) > self._max_playlists:
668            ba.screenmessage(
669                ba.Lstr(
670                    translate=(
671                        'serverResponses',
672                        'Max number of playlists reached.',
673                    )
674                ),
675                color=(1, 0, 0),
676            )
677            ba.playsound(ba.getsound('error'))
678            return
679
680        copy_text = ba.Lstr(resource='copyOfText').evaluate()
681        # get just 'Copy' or whatnot
682        copy_word = copy_text.replace('${NAME}', '').strip()
683        # find a valid dup name that doesn't exist
684
685        test_index = 1
686        base_name = self._get_playlist_display_name(
687            self._selected_playlist_name
688        ).evaluate()
689
690        # If it looks like a copy, strip digits and spaces off the end.
691        if copy_word in base_name:
692            while base_name[-1].isdigit() or base_name[-1] == ' ':
693                base_name = base_name[:-1]
694        while True:
695            if copy_word in base_name:
696                test_name = base_name
697            else:
698                test_name = copy_text.replace('${NAME}', base_name)
699            if test_index > 1:
700                test_name += ' ' + str(test_index)
701            if test_name not in ba.app.config[self._config_name_full]:
702                break
703            test_index += 1
704
705        ba.internal.add_transaction(
706            {
707                'type': 'ADD_PLAYLIST',
708                'playlistType': self._pvars.config_name,
709                'playlistName': test_name,
710                'playlist': copy.deepcopy(plst),
711            }
712        )
713        ba.internal.run_transactions()
714
715        ba.playsound(ba.getsound('gunCocking'))
716        self._refresh(select_playlist=test_name)

Window for viewing a playlist.

PlaylistCustomizeBrowserWindow( sessiontype: type[ba._session.Session], transition: str = 'in_right', select_playlist: str | None = None, origin_widget: _ba.Widget | None = None)
 22    def __init__(
 23        self,
 24        sessiontype: type[ba.Session],
 25        transition: str = 'in_right',
 26        select_playlist: str | None = None,
 27        origin_widget: ba.Widget | None = None,
 28    ):
 29        # Yes this needs tidying.
 30        # pylint: disable=too-many-locals
 31        # pylint: disable=too-many-statements
 32        # pylint: disable=cyclic-import
 33        from bastd.ui import playlist
 34
 35        scale_origin: tuple[float, float] | None
 36        if origin_widget is not None:
 37            self._transition_out = 'out_scale'
 38            scale_origin = origin_widget.get_screen_space_center()
 39            transition = 'in_scale'
 40        else:
 41            self._transition_out = 'out_right'
 42            scale_origin = None
 43
 44        self._sessiontype = sessiontype
 45        self._pvars = playlist.PlaylistTypeVars(sessiontype)
 46        self._max_playlists = 30
 47        self._r = 'gameListWindow'
 48        uiscale = ba.app.ui.uiscale
 49        self._width = 750.0 if uiscale is ba.UIScale.SMALL else 650.0
 50        x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0
 51        self._height = (
 52            380.0
 53            if uiscale is ba.UIScale.SMALL
 54            else 420.0
 55            if uiscale is ba.UIScale.MEDIUM
 56            else 500.0
 57        )
 58        top_extra = 20.0 if uiscale is ba.UIScale.SMALL else 0.0
 59
 60        super().__init__(
 61            root_widget=ba.containerwidget(
 62                size=(self._width, self._height + top_extra),
 63                transition=transition,
 64                scale_origin_stack_offset=scale_origin,
 65                scale=(
 66                    2.05
 67                    if uiscale is ba.UIScale.SMALL
 68                    else 1.5
 69                    if uiscale is ba.UIScale.MEDIUM
 70                    else 1.0
 71                ),
 72                stack_offset=(0, -10)
 73                if uiscale is ba.UIScale.SMALL
 74                else (0, 0),
 75            )
 76        )
 77
 78        self._back_button = back_button = btn = ba.buttonwidget(
 79            parent=self._root_widget,
 80            position=(43 + x_inset, self._height - 60),
 81            size=(160, 68),
 82            scale=0.77,
 83            autoselect=True,
 84            text_scale=1.3,
 85            label=ba.Lstr(resource='backText'),
 86            button_type='back',
 87        )
 88
 89        ba.textwidget(
 90            parent=self._root_widget,
 91            position=(0, self._height - 47),
 92            size=(self._width, 25),
 93            text=ba.Lstr(
 94                resource=self._r + '.titleText',
 95                subs=[('${TYPE}', self._pvars.window_title_name)],
 96            ),
 97            color=ba.app.ui.heading_color,
 98            maxwidth=290,
 99            h_align='center',
100            v_align='center',
101        )
102
103        ba.buttonwidget(
104            edit=btn,
105            button_type='backSmall',
106            size=(60, 60),
107            label=ba.charstr(ba.SpecialChar.BACK),
108        )
109
110        v = self._height - 59.0
111        h = 41 + x_inset
112        b_color = (0.6, 0.53, 0.63)
113        b_textcolor = (0.75, 0.7, 0.8)
114        self._lock_images: list[ba.Widget] = []
115        lock_tex = ba.gettexture('lock')
116
117        scl = (
118            1.1
119            if uiscale is ba.UIScale.SMALL
120            else 1.27
121            if uiscale is ba.UIScale.MEDIUM
122            else 1.57
123        )
124        scl *= 0.63
125        v -= 65.0 * scl
126        new_button = btn = ba.buttonwidget(
127            parent=self._root_widget,
128            position=(h, v),
129            size=(90, 58.0 * scl),
130            on_activate_call=self._new_playlist,
131            color=b_color,
132            autoselect=True,
133            button_type='square',
134            textcolor=b_textcolor,
135            text_scale=0.7,
136            label=ba.Lstr(
137                resource='newText', fallback_resource=self._r + '.newText'
138            ),
139        )
140        self._lock_images.append(
141            ba.imagewidget(
142                parent=self._root_widget,
143                size=(30, 30),
144                draw_controller=btn,
145                position=(h - 10, v + 58.0 * scl - 28),
146                texture=lock_tex,
147            )
148        )
149
150        v -= 65.0 * scl
151        self._edit_button = edit_button = btn = ba.buttonwidget(
152            parent=self._root_widget,
153            position=(h, v),
154            size=(90, 58.0 * scl),
155            on_activate_call=self._edit_playlist,
156            color=b_color,
157            autoselect=True,
158            textcolor=b_textcolor,
159            button_type='square',
160            text_scale=0.7,
161            label=ba.Lstr(
162                resource='editText', fallback_resource=self._r + '.editText'
163            ),
164        )
165        self._lock_images.append(
166            ba.imagewidget(
167                parent=self._root_widget,
168                size=(30, 30),
169                draw_controller=btn,
170                position=(h - 10, v + 58.0 * scl - 28),
171                texture=lock_tex,
172            )
173        )
174
175        v -= 65.0 * scl
176        duplicate_button = btn = ba.buttonwidget(
177            parent=self._root_widget,
178            position=(h, v),
179            size=(90, 58.0 * scl),
180            on_activate_call=self._duplicate_playlist,
181            color=b_color,
182            autoselect=True,
183            textcolor=b_textcolor,
184            button_type='square',
185            text_scale=0.7,
186            label=ba.Lstr(
187                resource='duplicateText',
188                fallback_resource=self._r + '.duplicateText',
189            ),
190        )
191        self._lock_images.append(
192            ba.imagewidget(
193                parent=self._root_widget,
194                size=(30, 30),
195                draw_controller=btn,
196                position=(h - 10, v + 58.0 * scl - 28),
197                texture=lock_tex,
198            )
199        )
200
201        v -= 65.0 * scl
202        delete_button = btn = ba.buttonwidget(
203            parent=self._root_widget,
204            position=(h, v),
205            size=(90, 58.0 * scl),
206            on_activate_call=self._delete_playlist,
207            color=b_color,
208            autoselect=True,
209            textcolor=b_textcolor,
210            button_type='square',
211            text_scale=0.7,
212            label=ba.Lstr(
213                resource='deleteText', fallback_resource=self._r + '.deleteText'
214            ),
215        )
216        self._lock_images.append(
217            ba.imagewidget(
218                parent=self._root_widget,
219                size=(30, 30),
220                draw_controller=btn,
221                position=(h - 10, v + 58.0 * scl - 28),
222                texture=lock_tex,
223            )
224        )
225        v -= 65.0 * scl
226        self._import_button = ba.buttonwidget(
227            parent=self._root_widget,
228            position=(h, v),
229            size=(90, 58.0 * scl),
230            on_activate_call=self._import_playlist,
231            color=b_color,
232            autoselect=True,
233            textcolor=b_textcolor,
234            button_type='square',
235            text_scale=0.7,
236            label=ba.Lstr(resource='importText'),
237        )
238        v -= 65.0 * scl
239        btn = ba.buttonwidget(
240            parent=self._root_widget,
241            position=(h, v),
242            size=(90, 58.0 * scl),
243            on_activate_call=self._share_playlist,
244            color=b_color,
245            autoselect=True,
246            textcolor=b_textcolor,
247            button_type='square',
248            text_scale=0.7,
249            label=ba.Lstr(resource='shareText'),
250        )
251        self._lock_images.append(
252            ba.imagewidget(
253                parent=self._root_widget,
254                size=(30, 30),
255                draw_controller=btn,
256                position=(h - 10, v + 58.0 * scl - 28),
257                texture=lock_tex,
258            )
259        )
260
261        v = self._height - 75
262        self._scroll_height = self._height - 119
263        scrollwidget = ba.scrollwidget(
264            parent=self._root_widget,
265            position=(140 + x_inset, v - self._scroll_height),
266            size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10),
267            highlight=False,
268        )
269        ba.widget(edit=back_button, right_widget=scrollwidget)
270        self._columnwidget = ba.columnwidget(
271            parent=scrollwidget, border=2, margin=0
272        )
273
274        h = 145
275
276        self._do_randomize_val = ba.app.config.get(
277            self._pvars.config_name + ' Playlist Randomize', 0
278        )
279
280        h += 210
281
282        for btn in [new_button, delete_button, edit_button, duplicate_button]:
283            ba.widget(edit=btn, right_widget=scrollwidget)
284        ba.widget(
285            edit=scrollwidget,
286            left_widget=new_button,
287            right_widget=ba.internal.get_special_widget('party_button')
288            if ba.app.ui.use_toolbars
289            else None,
290        )
291
292        # make sure config exists
293        self._config_name_full = self._pvars.config_name + ' Playlists'
294
295        if self._config_name_full not in ba.app.config:
296            ba.app.config[self._config_name_full] = {}
297
298        self._selected_playlist_name: str | None = None
299        self._selected_playlist_index: int | None = None
300        self._playlist_widgets: list[ba.Widget] = []
301
302        self._refresh(select_playlist=select_playlist)
303
304        ba.buttonwidget(edit=back_button, on_activate_call=self._back)
305        ba.containerwidget(edit=self._root_widget, cancel_button=back_button)
306
307        ba.containerwidget(edit=self._root_widget, selected_child=scrollwidget)
308
309        # Keep our lock images up to date/etc.
310        self._update_timer = ba.Timer(
311            1.0,
312            ba.WeakCall(self._update),
313            timetype=ba.TimeType.REAL,
314            repeat=True,
315        )
316        self._update()
Inherited Members
ba.ui.Window
get_root_widget