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

Window for viewing a playlist.

PlaylistCustomizeBrowserWindow( sessiontype: type[bascenev1.Session], transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None, select_playlist: str | None = None)
 26    def __init__(
 27        self,
 28        sessiontype: type[bs.Session],
 29        transition: str | None = 'in_right',
 30        origin_widget: bui.Widget | None = None,
 31        select_playlist: str | None = None,
 32    ):
 33        # pylint: disable=too-many-locals
 34        # pylint: disable=too-many-statements
 35        # pylint: disable=cyclic-import
 36        from bauiv1lib import playlist
 37
 38        self._sessiontype = sessiontype
 39        self._pvars = playlist.PlaylistTypeVars(sessiontype)
 40        self._max_playlists = 30
 41        self._r = 'gameListWindow'
 42        assert bui.app.classic is not None
 43        uiscale = bui.app.ui_v1.uiscale
 44        self._width = 1200.0 if uiscale is bui.UIScale.SMALL else 650.0
 45        self._height = (
 46            800.0
 47            if uiscale is bui.UIScale.SMALL
 48            else 420.0 if uiscale is bui.UIScale.MEDIUM else 500.0
 49        )
 50
 51        # Do some fancy math to fill all available screen area up to the
 52        # size of our backing container. This lets us fit to the exact
 53        # screen shape at small ui scale.
 54        screensize = bui.get_virtual_screen_size()
 55        scale = (
 56            1.8
 57            if uiscale is bui.UIScale.SMALL
 58            else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 59        )
 60        # Calc screen size in our local container space and clamp to a
 61        # bit smaller than our container size.
 62        target_width = min(self._width - 70, screensize[0] / scale)
 63        target_height = min(self._height - 40, screensize[1] / scale)
 64
 65        # To get top/left coords, go to the center of our window and
 66        # offset by half the width/height of our target area.
 67        yoffs = (
 68            0.5 * self._height
 69            + 0.5 * target_height
 70            + (30.0 if uiscale is bui.UIScale.SMALL else 50)
 71        )
 72
 73        self._button_width = 90
 74        self._x_inset = 10
 75        self._scroll_width = (
 76            target_width - self._button_width - 2.0 * self._x_inset
 77        )
 78        self._scroll_height = target_height - 75
 79        self._scroll_bottom = yoffs - 98 - self._scroll_height
 80        self._button_height = self._scroll_height / 6.0
 81
 82        super().__init__(
 83            root_widget=bui.containerwidget(
 84                size=(self._width, self._height),
 85                scale=scale,
 86                toolbar_visibility=(
 87                    'menu_minimal'
 88                    if uiscale is bui.UIScale.SMALL
 89                    else 'menu_full'
 90                ),
 91            ),
 92            transition=transition,
 93            origin_widget=origin_widget,
 94            # We're affected by screen size only at small ui-scale.
 95            refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
 96        )
 97
 98        self._back_button: bui.Widget | None
 99        if uiscale is bui.UIScale.SMALL:
100            self._back_button = None
101            bui.containerwidget(
102                edit=self._root_widget, on_cancel_call=self.main_window_back
103            )
104        else:
105            self._back_button = bui.buttonwidget(
106                parent=self._root_widget,
107                position=(43, yoffs - 87),
108                size=(60, 60),
109                scale=0.77,
110                autoselect=True,
111                text_scale=1.3,
112                label=bui.charstr(bui.SpecialChar.BACK),
113                button_type='backSmall',
114            )
115
116        bui.textwidget(
117            parent=self._root_widget,
118            position=(0, yoffs - (77 if uiscale is bui.UIScale.SMALL else 77)),
119            size=(self._width, 25),
120            text=bui.Lstr(
121                resource=f'{self._r}.titleText',
122                subs=[('${TYPE}', self._pvars.window_title_name)],
123            ),
124            color=bui.app.ui_v1.heading_color,
125            maxwidth=290,
126            h_align='center',
127            v_align='center',
128        )
129
130        h = self._width * 0.5 - (self._scroll_width + self._button_width) * 0.5
131        b_color = (0.6, 0.53, 0.63)
132        b_textcolor = (0.75, 0.7, 0.8)
133        self._lock_images: list[bui.Widget] = []
134        xmargin = 0.06
135        ymargin = 0.05
136
137        def _make_button(
138            i: int, label: bui.Lstr, call: Callable[[], None]
139        ) -> bui.Widget:
140            v = self._scroll_bottom + self._button_height * i
141            return bui.buttonwidget(
142                parent=self._root_widget,
143                position=(
144                    h + xmargin * self._button_width,
145                    v + ymargin * self._button_height,
146                ),
147                size=(
148                    self._button_width * (1.0 - 2.0 * xmargin),
149                    self._button_height * (1.0 - 2.0 * ymargin),
150                ),
151                on_activate_call=call,
152                color=b_color,
153                autoselect=True,
154                button_type='square',
155                textcolor=b_textcolor,
156                text_scale=0.7,
157                label=label,
158            )
159
160        new_button = _make_button(
161            5,
162            bui.Lstr(
163                resource='newText', fallback_resource=f'{self._r}.newText'
164            ),
165            self._new_playlist,
166        )
167        self._edit_button = _make_button(
168            4,
169            bui.Lstr(
170                resource='editText',
171                fallback_resource=f'{self._r}.editText',
172            ),
173            self._edit_playlist,
174        )
175
176        duplicate_button = _make_button(
177            3,
178            bui.Lstr(
179                resource='duplicateText',
180                fallback_resource=f'{self._r}.duplicateText',
181            ),
182            self._duplicate_playlist,
183        )
184
185        delete_button = _make_button(
186            2,
187            bui.Lstr(
188                resource='deleteText', fallback_resource=f'{self._r}.deleteText'
189            ),
190            self._delete_playlist,
191        )
192
193        self._import_button = _make_button(
194            1, bui.Lstr(resource='importText'), self._import_playlist
195        )
196
197        share_button = _make_button(
198            0, bui.Lstr(resource='shareText'), self._share_playlist
199        )
200
201        scrollwidget = bui.scrollwidget(
202            parent=self._root_widget,
203            size=(self._scroll_width, self._scroll_height),
204            position=(
205                self._width * 0.5
206                - (self._scroll_width + self._button_width) * 0.5
207                + self._button_width,
208                self._scroll_bottom,
209            ),
210            highlight=False,
211            border_opacity=0.4,
212        )
213        if self._back_button is not None:
214            bui.widget(edit=self._back_button, right_widget=scrollwidget)
215
216        self._columnwidget = bui.columnwidget(
217            parent=scrollwidget, border=2, margin=0
218        )
219
220        h = 145
221
222        self._do_randomize_val = bui.app.config.get(
223            self._pvars.config_name + ' Playlist Randomize', 0
224        )
225
226        h += 210
227
228        for btn in [
229            new_button,
230            delete_button,
231            self._edit_button,
232            duplicate_button,
233            self._import_button,
234            share_button,
235        ]:
236            bui.widget(edit=btn, right_widget=scrollwidget)
237        bui.widget(
238            edit=scrollwidget,
239            left_widget=new_button,
240            right_widget=bui.get_special_widget('squad_button'),
241        )
242
243        # Make sure config exists.
244        self._config_name_full = f'{self._pvars.config_name} Playlists'
245
246        if self._config_name_full not in bui.app.config:
247            bui.app.config[self._config_name_full] = {}
248
249        self._selected_playlist_name: str | None = None
250        self._selected_playlist_index: int | None = None
251        self._playlist_widgets: list[bui.Widget] = []
252
253        self._refresh(select_playlist=select_playlist)
254
255        if self._back_button is not None:
256            bui.buttonwidget(
257                edit=self._back_button, on_activate_call=self.main_window_back
258            )
259            bui.containerwidget(
260                edit=self._root_widget, cancel_button=self._back_button
261            )
262
263        bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget)
264
265        # Keep our lock images up to date/etc.
266        self._update_timer = bui.AppTimer(
267            1.0, bui.WeakCall(self._update), repeat=True
268        )
269        self._update()

Create a MainWindow given a root widget and transition info.

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

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
271    @override
272    def get_main_window_state(self) -> bui.MainWindowState:
273        # Support recreating our window for back/refresh purposes.
274        cls = type(self)
275
276        # Avoid dereferencing self within the lambda or we'll keep
277        # ourself alive indefinitely.
278        stype = self._sessiontype
279
280        return bui.BasicMainWindowState(
281            create_call=lambda transition, origin_widget: cls(
282                transition=transition,
283                origin_widget=origin_widget,
284                sessiontype=stype,
285            )
286        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
288    @override
289    def on_main_window_close(self) -> None:
290        if self._selected_playlist_name is not None:
291            cfg = bui.app.config
292            cfg[f'{self._pvars.config_name} Playlist Selection'] = (
293                self._selected_playlist_name
294            )
295            cfg.commit()

Called before transitioning out a main window.

A good opportunity to save window state/etc.