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

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
348    @override
349    def on_main_window_close(self) -> None:
350        if self._selected_playlist_name is not None:
351            cfg = bui.app.config
352            cfg[f'{self._pvars.config_name} Playlist Selection'] = (
353                self._selected_playlist_name
354            )
355            cfg.commit()

Called before transitioning out a main window.

A good opportunity to save window state/etc.