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        )
278        if self._back_button is not None:
279            bui.widget(edit=self._back_button, right_widget=scrollwidget)
280
281        self._columnwidget = bui.columnwidget(
282            parent=scrollwidget, border=2, margin=0
283        )
284
285        h = 145
286
287        self._do_randomize_val = bui.app.config.get(
288            self._pvars.config_name + ' Playlist Randomize', 0
289        )
290
291        h += 210
292
293        for btn in [new_button, delete_button, edit_button, duplicate_button]:
294            bui.widget(edit=btn, right_widget=scrollwidget)
295        bui.widget(
296            edit=scrollwidget,
297            left_widget=new_button,
298            right_widget=bui.get_special_widget('squad_button'),
299        )
300
301        # Make sure config exists.
302        self._config_name_full = f'{self._pvars.config_name} Playlists'
303
304        if self._config_name_full not in bui.app.config:
305            bui.app.config[self._config_name_full] = {}
306
307        self._selected_playlist_name: str | None = None
308        self._selected_playlist_index: int | None = None
309        self._playlist_widgets: list[bui.Widget] = []
310
311        self._refresh(select_playlist=select_playlist)
312
313        if self._back_button is not None:
314            bui.buttonwidget(
315                edit=self._back_button, on_activate_call=self.main_window_back
316            )
317            bui.containerwidget(
318                edit=self._root_widget, cancel_button=self._back_button
319            )
320
321        bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget)
322
323        # Keep our lock images up to date/etc.
324        self._update_timer = bui.AppTimer(
325            1.0, bui.WeakCall(self._update), repeat=True
326        )
327        self._update()
328
329    @override
330    def get_main_window_state(self) -> bui.MainWindowState:
331        # Support recreating our window for back/refresh purposes.
332        cls = type(self)
333
334        # Avoid dereferencing self within the lambda or we'll keep
335        # ourself alive indefinitely.
336        stype = self._sessiontype
337
338        return bui.BasicMainWindowState(
339            create_call=lambda transition, origin_widget: cls(
340                transition=transition,
341                origin_widget=origin_widget,
342                sessiontype=stype,
343            )
344        )
345
346    @override
347    def on_main_window_close(self) -> None:
348        if self._selected_playlist_name is not None:
349            cfg = bui.app.config
350            cfg[f'{self._pvars.config_name} Playlist Selection'] = (
351                self._selected_playlist_name
352            )
353            cfg.commit()
354
355    def _update(self) -> None:
356        assert bui.app.classic is not None
357        have = bui.app.classic.accounts.have_pro_options()
358        for lock in self._lock_images:
359            bui.imagewidget(
360                edit=lock, opacity=0.0 if (have or not REQUIRE_PRO) else 1.0
361            )
362
363    def _select(self, name: str, index: int) -> None:
364        self._selected_playlist_name = name
365        self._selected_playlist_index = index
366
367    def _refresh(self, select_playlist: str | None = None) -> None:
368        from efro.util import asserttype
369
370        old_selection = self._selected_playlist_name
371
372        # If there was no prev selection, look in prefs.
373        if old_selection is None:
374            old_selection = bui.app.config.get(
375                self._pvars.config_name + ' Playlist Selection'
376            )
377
378        old_selection_index = self._selected_playlist_index
379
380        # Delete old.
381        while self._playlist_widgets:
382            self._playlist_widgets.pop().delete()
383
384        items = list(bui.app.config[self._config_name_full].items())
385
386        # Make sure everything is unicode now.
387        items = [
388            (i[0].decode(), i[1]) if not isinstance(i[0], str) else i
389            for i in items
390        ]
391
392        items.sort(key=lambda x: asserttype(x[0], str).lower())
393
394        items = [['__default__', None]] + items  # Default is always first.
395        index = 0
396        for pname, _ in items:
397            assert pname is not None
398            txtw = bui.textwidget(
399                parent=self._columnwidget,
400                size=(self._width - 40, 30),
401                maxwidth=440,
402                text=self._get_playlist_display_name(pname),
403                h_align='left',
404                v_align='center',
405                color=(
406                    (0.6, 0.6, 0.7, 1.0)
407                    if pname == '__default__'
408                    else (0.85, 0.85, 0.85, 1)
409                ),
410                always_highlight=True,
411                on_select_call=bui.Call(self._select, pname, index),
412                on_activate_call=bui.Call(self._edit_button.activate),
413                selectable=True,
414            )
415            bui.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50)
416
417            # Hitting up from top widget should jump to 'back'.
418            if index == 0:
419                bui.widget(
420                    edit=txtw,
421                    up_widget=(
422                        self._back_button
423                        if self._back_button is not None
424                        else bui.get_special_widget('back_button')
425                    ),
426                )
427
428            self._playlist_widgets.append(txtw)
429
430            # Select this one if the user requested it.
431            if select_playlist is not None:
432                if pname == select_playlist:
433                    bui.columnwidget(
434                        edit=self._columnwidget,
435                        selected_child=txtw,
436                        visible_child=txtw,
437                    )
438            else:
439                # Select this one if it was previously selected. Go by
440                # index if there's one.
441                if old_selection_index is not None:
442                    if index == old_selection_index:
443                        bui.columnwidget(
444                            edit=self._columnwidget,
445                            selected_child=txtw,
446                            visible_child=txtw,
447                        )
448                else:  # Otherwise look by name.
449                    if pname == old_selection:
450                        bui.columnwidget(
451                            edit=self._columnwidget,
452                            selected_child=txtw,
453                            visible_child=txtw,
454                        )
455
456            index += 1
457
458    def _save_playlist_selection(self) -> None:
459        # Store the selected playlist in prefs. This serves dual
460        # purposes of letting us re-select it next time if we want and
461        # also lets us pass it to the game (since we reset the whole
462        # python environment that's not actually easy).
463        cfg = bui.app.config
464        cfg[self._pvars.config_name + ' Playlist Selection'] = (
465            self._selected_playlist_name
466        )
467        cfg[self._pvars.config_name + ' Playlist Randomize'] = (
468            self._do_randomize_val
469        )
470        cfg.commit()
471
472    def _new_playlist(self) -> None:
473        # pylint: disable=cyclic-import
474        from bauiv1lib.playlist.editcontroller import PlaylistEditController
475        from bauiv1lib.purchase import PurchaseWindow
476
477        # No-op if we're not in control.
478        if not self.main_window_has_control():
479            return
480
481        assert bui.app.classic is not None
482        if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options():
483            PurchaseWindow(items=['pro'])
484            return
485
486        # Clamp at our max playlist number.
487        if len(bui.app.config[self._config_name_full]) > self._max_playlists:
488            bui.screenmessage(
489                bui.Lstr(
490                    translate=(
491                        'serverResponses',
492                        'Max number of playlists reached.',
493                    )
494                ),
495                color=(1, 0, 0),
496            )
497            bui.getsound('error').play()
498            return
499
500        # In case they cancel so we can return to this state.
501        self._save_playlist_selection()
502
503        # Kick off the edit UI.
504        PlaylistEditController(sessiontype=self._sessiontype, from_window=self)
505
506    def _edit_playlist(self) -> None:
507        # pylint: disable=cyclic-import
508        from bauiv1lib.playlist.editcontroller import PlaylistEditController
509        from bauiv1lib.purchase import PurchaseWindow
510
511        assert bui.app.classic is not None
512        if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options():
513            PurchaseWindow(items=['pro'])
514            return
515        if self._selected_playlist_name is None:
516            return
517        if self._selected_playlist_name == '__default__':
518            bui.getsound('error').play()
519            bui.screenmessage(
520                bui.Lstr(resource=f'{self._r}.cantEditDefaultText')
521            )
522            return
523        self._save_playlist_selection()
524        PlaylistEditController(
525            existing_playlist_name=self._selected_playlist_name,
526            sessiontype=self._sessiontype,
527            from_window=self,
528        )
529
530    def _do_delete_playlist(self) -> None:
531        plus = bui.app.plus
532        assert plus is not None
533        plus.add_v1_account_transaction(
534            {
535                'type': 'REMOVE_PLAYLIST',
536                'playlistType': self._pvars.config_name,
537                'playlistName': self._selected_playlist_name,
538            }
539        )
540        plus.run_v1_account_transactions()
541        bui.getsound('shieldDown').play()
542
543        # (we don't use len()-1 here because the default list adds one)
544        assert self._selected_playlist_index is not None
545        self._selected_playlist_index = min(
546            self._selected_playlist_index,
547            len(bui.app.config[self._pvars.config_name + ' Playlists']),
548        )
549        self._refresh()
550
551    def _import_playlist(self) -> None:
552        # pylint: disable=cyclic-import
553        from bauiv1lib.playlist import share
554
555        plus = bui.app.plus
556        assert plus is not None
557
558        # Gotta be signed in for this to work.
559        if plus.get_v1_account_state() != 'signed_in':
560            bui.screenmessage(
561                bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
562            )
563            bui.getsound('error').play()
564            return
565
566        share.SharePlaylistImportWindow(
567            origin_widget=self._import_button,
568            on_success_callback=bui.WeakCall(self._on_playlist_import_success),
569        )
570
571    def _on_playlist_import_success(self) -> None:
572        self._refresh()
573
574    def _on_share_playlist_response(self, name: str, response: Any) -> None:
575        # pylint: disable=cyclic-import
576        from bauiv1lib.playlist import share
577
578        if response is None:
579            bui.screenmessage(
580                bui.Lstr(resource='internal.unavailableNoConnectionText'),
581                color=(1, 0, 0),
582            )
583            bui.getsound('error').play()
584            return
585        share.SharePlaylistResultsWindow(name, response)
586
587    def _share_playlist(self) -> None:
588        # pylint: disable=cyclic-import
589        from bauiv1lib.purchase import PurchaseWindow
590
591        plus = bui.app.plus
592        assert plus is not None
593
594        assert bui.app.classic is not None
595        if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options():
596            PurchaseWindow(items=['pro'])
597            return
598
599        # Gotta be signed in for this to work.
600        if plus.get_v1_account_state() != 'signed_in':
601            bui.screenmessage(
602                bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
603            )
604            bui.getsound('error').play()
605            return
606        if self._selected_playlist_name == '__default__':
607            bui.getsound('error').play()
608            bui.screenmessage(
609                bui.Lstr(resource=f'{self._r}.cantShareDefaultText'),
610                color=(1, 0, 0),
611            )
612            return
613
614        if self._selected_playlist_name is None:
615            return
616
617        plus.add_v1_account_transaction(
618            {
619                'type': 'SHARE_PLAYLIST',
620                'expire_time': time.time() + 5,
621                'playlistType': self._pvars.config_name,
622                'playlistName': self._selected_playlist_name,
623            },
624            callback=bui.WeakCall(
625                self._on_share_playlist_response, self._selected_playlist_name
626            ),
627        )
628        plus.run_v1_account_transactions()
629        bui.screenmessage(bui.Lstr(resource='sharingText'))
630
631    def _delete_playlist(self) -> None:
632        # pylint: disable=cyclic-import
633        from bauiv1lib.purchase import PurchaseWindow
634        from bauiv1lib.confirm import ConfirmWindow
635
636        assert bui.app.classic is not None
637        if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options():
638            PurchaseWindow(items=['pro'])
639            return
640
641        if self._selected_playlist_name is None:
642            return
643        if self._selected_playlist_name == '__default__':
644            bui.getsound('error').play()
645            bui.screenmessage(
646                bui.Lstr(resource=f'{self._r}.cantDeleteDefaultText')
647            )
648        else:
649            ConfirmWindow(
650                bui.Lstr(
651                    resource=f'{self._r}.deleteConfirmText',
652                    subs=[('${LIST}', self._selected_playlist_name)],
653                ),
654                self._do_delete_playlist,
655                450,
656                150,
657            )
658
659    def _get_playlist_display_name(self, playlist: str) -> bui.Lstr:
660        if playlist == '__default__':
661            return self._pvars.default_list_name
662        return (
663            playlist
664            if isinstance(playlist, bui.Lstr)
665            else bui.Lstr(value=playlist)
666        )
667
668    def _duplicate_playlist(self) -> None:
669        # pylint: disable=too-many-branches
670        # pylint: disable=cyclic-import
671        from bauiv1lib.purchase import PurchaseWindow
672
673        plus = bui.app.plus
674        assert plus is not None
675
676        assert bui.app.classic is not None
677        if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options():
678            PurchaseWindow(items=['pro'])
679            return
680        if self._selected_playlist_name is None:
681            return
682        plst: list[dict[str, Any]] | None
683        if self._selected_playlist_name == '__default__':
684            plst = self._pvars.get_default_list_call()
685        else:
686            plst = bui.app.config[self._config_name_full].get(
687                self._selected_playlist_name
688            )
689            if plst is None:
690                bui.getsound('error').play()
691                return
692
693        # Clamp at our max playlist number.
694        if len(bui.app.config[self._config_name_full]) > self._max_playlists:
695            bui.screenmessage(
696                bui.Lstr(
697                    translate=(
698                        'serverResponses',
699                        'Max number of playlists reached.',
700                    )
701                ),
702                color=(1, 0, 0),
703            )
704            bui.getsound('error').play()
705            return
706
707        copy_text = bui.Lstr(resource='copyOfText').evaluate()
708
709        # Get just 'Copy' or whatnot.
710        copy_word = copy_text.replace('${NAME}', '').strip()
711
712        # Find a valid dup name that doesn't exist.
713        test_index = 1
714        base_name = self._get_playlist_display_name(
715            self._selected_playlist_name
716        ).evaluate()
717
718        # If it looks like a copy, strip digits and spaces off the end.
719        if copy_word in base_name:
720            while base_name[-1].isdigit() or base_name[-1] == ' ':
721                base_name = base_name[:-1]
722        while True:
723            if copy_word in base_name:
724                test_name = base_name
725            else:
726                test_name = copy_text.replace('${NAME}', base_name)
727            if test_index > 1:
728                test_name += ' ' + str(test_index)
729            if test_name not in bui.app.config[self._config_name_full]:
730                break
731            test_index += 1
732
733        plus.add_v1_account_transaction(
734            {
735                'type': 'ADD_PLAYLIST',
736                'playlistType': self._pvars.config_name,
737                'playlistName': test_name,
738                'playlist': copy.deepcopy(plst),
739            }
740        )
741        plus.run_v1_account_transactions()
742
743        bui.getsound('gunCocking').play()
744        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        )
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)

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        )
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()

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:
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        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
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()

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_is_auxiliary
main_window_close
main_window_has_control
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget