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

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
340    @override
341    def on_main_window_close(self) -> None:
342        if self._selected_playlist_name is not None:
343            cfg = bui.app.config
344            cfg[f'{self._pvars.config_name} Playlist Selection'] = (
345                self._selected_playlist_name
346            )
347            cfg.commit()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_is_top_level
main_window_close
main_window_has_control
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget