bastd.ui.playlist.customizebrowser

Provides UI for viewing/creating/editing playlists.

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

Window for viewing a playlist.

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