bauiv1lib.soundtrack.browser

Provides UI for browsing soundtracks.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides UI for browsing soundtracks."""
  4
  5from __future__ import annotations
  6
  7import copy
  8import logging
  9from typing import TYPE_CHECKING
 10
 11import bauiv1 as bui
 12
 13if TYPE_CHECKING:
 14    from typing import Any
 15
 16
 17class SoundtrackBrowserWindow(bui.Window):
 18    """Window for browsing soundtracks."""
 19
 20    def __init__(
 21        self,
 22        transition: str = 'in_right',
 23        origin_widget: bui.Widget | None = None,
 24    ):
 25        # pylint: disable=too-many-locals
 26        # pylint: disable=too-many-statements
 27
 28        # If they provided an origin-widget, scale up from that.
 29        scale_origin: tuple[float, float] | None
 30        if origin_widget is not None:
 31            self._transition_out = 'out_scale'
 32            scale_origin = origin_widget.get_screen_space_center()
 33            transition = 'in_scale'
 34        else:
 35            self._transition_out = 'out_right'
 36            scale_origin = None
 37
 38        self._r = 'editSoundtrackWindow'
 39        assert bui.app.classic is not None
 40        uiscale = bui.app.ui_v1.uiscale
 41        self._width = 800 if uiscale is bui.UIScale.SMALL else 600
 42        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 43        self._height = (
 44            340
 45            if uiscale is bui.UIScale.SMALL
 46            else 370
 47            if uiscale is bui.UIScale.MEDIUM
 48            else 440
 49        )
 50        spacing = 40.0
 51        v = self._height - 40.0
 52        v -= spacing * 1.0
 53
 54        super().__init__(
 55            root_widget=bui.containerwidget(
 56                size=(self._width, self._height),
 57                transition=transition,
 58                toolbar_visibility='menu_minimal',
 59                scale_origin_stack_offset=scale_origin,
 60                scale=(
 61                    2.3
 62                    if uiscale is bui.UIScale.SMALL
 63                    else 1.6
 64                    if uiscale is bui.UIScale.MEDIUM
 65                    else 1.0
 66                ),
 67                stack_offset=(0, -18)
 68                if uiscale is bui.UIScale.SMALL
 69                else (0, 0),
 70            )
 71        )
 72
 73        assert bui.app.classic is not None
 74        if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
 75            self._back_button = None
 76        else:
 77            self._back_button = bui.buttonwidget(
 78                parent=self._root_widget,
 79                position=(45 + x_inset, self._height - 60),
 80                size=(120, 60),
 81                scale=0.8,
 82                label=bui.Lstr(resource='backText'),
 83                button_type='back',
 84                autoselect=True,
 85            )
 86            bui.buttonwidget(
 87                edit=self._back_button,
 88                button_type='backSmall',
 89                size=(60, 60),
 90                label=bui.charstr(bui.SpecialChar.BACK),
 91            )
 92        bui.textwidget(
 93            parent=self._root_widget,
 94            position=(self._width * 0.5, self._height - 35),
 95            size=(0, 0),
 96            maxwidth=300,
 97            text=bui.Lstr(resource=self._r + '.titleText'),
 98            color=bui.app.ui_v1.title_color,
 99            h_align='center',
100            v_align='center',
101        )
102
103        h = 43 + x_inset
104        v = self._height - 60
105        b_color = (0.6, 0.53, 0.63)
106        b_textcolor = (0.75, 0.7, 0.8)
107        lock_tex = bui.gettexture('lock')
108        self._lock_images: list[bui.Widget] = []
109
110        scl = (
111            1.0
112            if uiscale is bui.UIScale.SMALL
113            else 1.13
114            if uiscale is bui.UIScale.MEDIUM
115            else 1.4
116        )
117        v -= 60.0 * scl
118        self._new_button = btn = bui.buttonwidget(
119            parent=self._root_widget,
120            position=(h, v),
121            size=(100, 55.0 * scl),
122            on_activate_call=self._new_soundtrack,
123            color=b_color,
124            button_type='square',
125            autoselect=True,
126            textcolor=b_textcolor,
127            text_scale=0.7,
128            label=bui.Lstr(resource=self._r + '.newText'),
129        )
130        self._lock_images.append(
131            bui.imagewidget(
132                parent=self._root_widget,
133                size=(30, 30),
134                draw_controller=btn,
135                position=(h - 10, v + 55.0 * scl - 28),
136                texture=lock_tex,
137            )
138        )
139
140        if self._back_button is None:
141            bui.widget(
142                edit=btn,
143                left_widget=bui.get_special_widget('back_button'),
144            )
145        v -= 60.0 * scl
146
147        self._edit_button = btn = bui.buttonwidget(
148            parent=self._root_widget,
149            position=(h, v),
150            size=(100, 55.0 * scl),
151            on_activate_call=self._edit_soundtrack,
152            color=b_color,
153            button_type='square',
154            autoselect=True,
155            textcolor=b_textcolor,
156            text_scale=0.7,
157            label=bui.Lstr(resource=self._r + '.editText'),
158        )
159        self._lock_images.append(
160            bui.imagewidget(
161                parent=self._root_widget,
162                size=(30, 30),
163                draw_controller=btn,
164                position=(h - 10, v + 55.0 * scl - 28),
165                texture=lock_tex,
166            )
167        )
168        if self._back_button is None:
169            bui.widget(
170                edit=btn,
171                left_widget=bui.get_special_widget('back_button'),
172            )
173        v -= 60.0 * scl
174
175        self._duplicate_button = btn = bui.buttonwidget(
176            parent=self._root_widget,
177            position=(h, v),
178            size=(100, 55.0 * scl),
179            on_activate_call=self._duplicate_soundtrack,
180            button_type='square',
181            autoselect=True,
182            color=b_color,
183            textcolor=b_textcolor,
184            text_scale=0.7,
185            label=bui.Lstr(resource=self._r + '.duplicateText'),
186        )
187        self._lock_images.append(
188            bui.imagewidget(
189                parent=self._root_widget,
190                size=(30, 30),
191                draw_controller=btn,
192                position=(h - 10, v + 55.0 * scl - 28),
193                texture=lock_tex,
194            )
195        )
196        if self._back_button is None:
197            bui.widget(
198                edit=btn,
199                left_widget=bui.get_special_widget('back_button'),
200            )
201        v -= 60.0 * scl
202
203        self._delete_button = btn = bui.buttonwidget(
204            parent=self._root_widget,
205            position=(h, v),
206            size=(100, 55.0 * scl),
207            on_activate_call=self._delete_soundtrack,
208            color=b_color,
209            button_type='square',
210            autoselect=True,
211            textcolor=b_textcolor,
212            text_scale=0.7,
213            label=bui.Lstr(resource=self._r + '.deleteText'),
214        )
215        self._lock_images.append(
216            bui.imagewidget(
217                parent=self._root_widget,
218                size=(30, 30),
219                draw_controller=btn,
220                position=(h - 10, v + 55.0 * scl - 28),
221                texture=lock_tex,
222            )
223        )
224        if self._back_button is None:
225            bui.widget(
226                edit=btn,
227                left_widget=bui.get_special_widget('back_button'),
228            )
229
230        # Keep our lock images up to date/etc.
231        self._update_timer = bui.AppTimer(
232            1.0, bui.WeakCall(self._update), repeat=True
233        )
234        self._update()
235
236        v = self._height - 65
237        scroll_height = self._height - 105
238        v -= scroll_height
239        self._scrollwidget = scrollwidget = bui.scrollwidget(
240            parent=self._root_widget,
241            position=(152 + x_inset, v),
242            highlight=False,
243            size=(self._width - (205 + 2 * x_inset), scroll_height),
244        )
245        bui.widget(
246            edit=self._scrollwidget,
247            left_widget=self._new_button,
248            right_widget=bui.get_special_widget('party_button')
249            if bui.app.ui_v1.use_toolbars
250            else self._scrollwidget,
251        )
252        self._col = bui.columnwidget(parent=scrollwidget, border=2, margin=0)
253
254        self._soundtracks: dict[str, Any] | None = None
255        self._selected_soundtrack: str | None = None
256        self._selected_soundtrack_index: int | None = None
257        self._soundtrack_widgets: list[bui.Widget] = []
258        self._allow_changing_soundtracks = False
259        self._refresh()
260        if self._back_button is not None:
261            bui.buttonwidget(
262                edit=self._back_button, on_activate_call=self._back
263            )
264            bui.containerwidget(
265                edit=self._root_widget, cancel_button=self._back_button
266            )
267        else:
268            bui.containerwidget(
269                edit=self._root_widget, on_cancel_call=self._back
270            )
271
272    def _update(self) -> None:
273        have = (
274            bui.app.classic is None
275            or bui.app.classic.accounts.have_pro_options()
276        )
277        for lock in self._lock_images:
278            bui.imagewidget(edit=lock, opacity=0.0 if have else 1.0)
279
280    def _do_delete_soundtrack(self) -> None:
281        cfg = bui.app.config
282        soundtracks = cfg.setdefault('Soundtracks', {})
283        if self._selected_soundtrack in soundtracks:
284            del soundtracks[self._selected_soundtrack]
285        cfg.commit()
286        bui.getsound('shieldDown').play()
287        assert self._selected_soundtrack_index is not None
288        assert self._soundtracks is not None
289        if self._selected_soundtrack_index >= len(self._soundtracks):
290            self._selected_soundtrack_index = len(self._soundtracks)
291        self._refresh()
292
293    def _delete_soundtrack(self) -> None:
294        # pylint: disable=cyclic-import
295        from bauiv1lib.purchase import PurchaseWindow
296        from bauiv1lib.confirm import ConfirmWindow
297
298        if (
299            bui.app.classic is not None
300            and not bui.app.classic.accounts.have_pro_options()
301        ):
302            PurchaseWindow(items=['pro'])
303            return
304        if self._selected_soundtrack is None:
305            return
306        if self._selected_soundtrack == '__default__':
307            bui.getsound('error').play()
308            bui.screenmessage(
309                bui.Lstr(resource=self._r + '.cantDeleteDefaultText'),
310                color=(1, 0, 0),
311            )
312        else:
313            ConfirmWindow(
314                bui.Lstr(
315                    resource=self._r + '.deleteConfirmText',
316                    subs=[('${NAME}', self._selected_soundtrack)],
317                ),
318                self._do_delete_soundtrack,
319                450,
320                150,
321            )
322
323    def _duplicate_soundtrack(self) -> None:
324        # pylint: disable=cyclic-import
325        from bauiv1lib.purchase import PurchaseWindow
326
327        if (
328            bui.app.classic is not None
329            and not bui.app.classic.accounts.have_pro_options()
330        ):
331            PurchaseWindow(items=['pro'])
332            return
333        cfg = bui.app.config
334        cfg.setdefault('Soundtracks', {})
335
336        if self._selected_soundtrack is None:
337            return
338        sdtk: dict[str, Any]
339        if self._selected_soundtrack == '__default__':
340            sdtk = {}
341        else:
342            sdtk = cfg['Soundtracks'][self._selected_soundtrack]
343
344        # Find a valid dup name that doesn't exist.
345        test_index = 1
346        copy_text = bui.Lstr(resource='copyOfText').evaluate()
347        # Get just 'Copy' or whatnot.
348        copy_word = copy_text.replace('${NAME}', '').strip()
349        base_name = self._get_soundtrack_display_name(
350            self._selected_soundtrack
351        ).evaluate()
352        assert isinstance(base_name, str)
353
354        # If it looks like a copy, strip digits and spaces off the end.
355        if copy_word in base_name:
356            while base_name[-1].isdigit() or base_name[-1] == ' ':
357                base_name = base_name[:-1]
358        while True:
359            if copy_word in base_name:
360                test_name = base_name
361            else:
362                test_name = copy_text.replace('${NAME}', base_name)
363            if test_index > 1:
364                test_name += ' ' + str(test_index)
365            if test_name not in cfg['Soundtracks']:
366                break
367            test_index += 1
368
369        cfg['Soundtracks'][test_name] = copy.deepcopy(sdtk)
370        cfg.commit()
371        self._refresh(select_soundtrack=test_name)
372
373    def _select(self, name: str, index: int) -> None:
374        assert bui.app.classic is not None
375        music = bui.app.classic.music
376        self._selected_soundtrack_index = index
377        self._selected_soundtrack = name
378        cfg = bui.app.config
379        current_soundtrack = cfg.setdefault('Soundtrack', '__default__')
380
381        # If it varies from current, commit and play.
382        if current_soundtrack != name and self._allow_changing_soundtracks:
383            bui.getsound('gunCocking').play()
384            cfg['Soundtrack'] = self._selected_soundtrack
385            cfg.commit()
386
387            # Just play whats already playing.. this'll grab it from the
388            # new soundtrack.
389            music.do_play_music(
390                music.music_types[bui.app.classic.MusicPlayMode.REGULAR]
391            )
392
393    def _back(self) -> None:
394        # pylint: disable=cyclic-import
395        from bauiv1lib.settings import audio
396
397        self._save_state()
398        bui.containerwidget(
399            edit=self._root_widget, transition=self._transition_out
400        )
401        assert bui.app.classic is not None
402        bui.app.ui_v1.set_main_menu_window(
403            audio.AudioSettingsWindow(transition='in_left').get_root_widget()
404        )
405
406    def _edit_soundtrack_with_sound(self) -> None:
407        # pylint: disable=cyclic-import
408        from bauiv1lib.purchase import PurchaseWindow
409
410        if (
411            bui.app.classic is not None
412            and not bui.app.classic.accounts.have_pro_options()
413        ):
414            PurchaseWindow(items=['pro'])
415            return
416        bui.getsound('swish').play()
417        self._edit_soundtrack()
418
419    def _edit_soundtrack(self) -> None:
420        # pylint: disable=cyclic-import
421        from bauiv1lib.purchase import PurchaseWindow
422        from bauiv1lib.soundtrack.edit import SoundtrackEditWindow
423
424        if (
425            bui.app.classic is not None
426            and not bui.app.classic.accounts.have_pro_options()
427        ):
428            PurchaseWindow(items=['pro'])
429            return
430        if self._selected_soundtrack is None:
431            return
432        if self._selected_soundtrack == '__default__':
433            bui.getsound('error').play()
434            bui.screenmessage(
435                bui.Lstr(resource=self._r + '.cantEditDefaultText'),
436                color=(1, 0, 0),
437            )
438            return
439
440        self._save_state()
441        bui.containerwidget(edit=self._root_widget, transition='out_left')
442        assert bui.app.classic is not None
443        bui.app.ui_v1.set_main_menu_window(
444            SoundtrackEditWindow(
445                existing_soundtrack=self._selected_soundtrack
446            ).get_root_widget()
447        )
448
449    def _get_soundtrack_display_name(self, soundtrack: str) -> bui.Lstr:
450        if soundtrack == '__default__':
451            return bui.Lstr(resource=self._r + '.defaultSoundtrackNameText')
452        return bui.Lstr(value=soundtrack)
453
454    def _refresh(self, select_soundtrack: str | None = None) -> None:
455        from efro.util import asserttype
456
457        self._allow_changing_soundtracks = False
458        old_selection = self._selected_soundtrack
459
460        # If there was no prev selection, look in prefs.
461        if old_selection is None:
462            old_selection = bui.app.config.get('Soundtrack')
463        old_selection_index = self._selected_soundtrack_index
464
465        # Delete old.
466        while self._soundtrack_widgets:
467            self._soundtrack_widgets.pop().delete()
468
469        self._soundtracks = bui.app.config.get('Soundtracks', {})
470        assert self._soundtracks is not None
471        items = list(self._soundtracks.items())
472        items.sort(key=lambda x: asserttype(x[0], str).lower())
473        items = [('__default__', None)] + items  # default is always first
474        index = 0
475        for pname, _pval in items:
476            assert pname is not None
477            txtw = bui.textwidget(
478                parent=self._col,
479                size=(self._width - 40, 24),
480                text=self._get_soundtrack_display_name(pname),
481                h_align='left',
482                v_align='center',
483                maxwidth=self._width - 110,
484                always_highlight=True,
485                on_select_call=bui.WeakCall(self._select, pname, index),
486                on_activate_call=self._edit_soundtrack_with_sound,
487                selectable=True,
488            )
489            if index == 0:
490                bui.widget(edit=txtw, up_widget=self._back_button)
491            self._soundtrack_widgets.append(txtw)
492
493            # Select this one if the user requested it
494            if select_soundtrack is not None:
495                if pname == select_soundtrack:
496                    bui.columnwidget(
497                        edit=self._col, selected_child=txtw, visible_child=txtw
498                    )
499            else:
500                # Select this one if it was previously selected.
501                # Go by index if there's one.
502                if old_selection_index is not None:
503                    if index == old_selection_index:
504                        bui.columnwidget(
505                            edit=self._col,
506                            selected_child=txtw,
507                            visible_child=txtw,
508                        )
509                else:  # Otherwise look by name.
510                    if pname == old_selection:
511                        bui.columnwidget(
512                            edit=self._col,
513                            selected_child=txtw,
514                            visible_child=txtw,
515                        )
516            index += 1
517
518        # Explicitly run select callback on current one and re-enable
519        # callbacks.
520
521        # Eww need to run this in a timer so it happens after our select
522        # callbacks. With a small-enough time sometimes it happens before
523        # anyway. Ew. need a way to just schedule a callable i guess.
524        bui.apptimer(0.1, bui.WeakCall(self._set_allow_changing))
525
526    def _set_allow_changing(self) -> None:
527        self._allow_changing_soundtracks = True
528        assert self._selected_soundtrack is not None
529        assert self._selected_soundtrack_index is not None
530        self._select(self._selected_soundtrack, self._selected_soundtrack_index)
531
532    def _new_soundtrack(self) -> None:
533        # pylint: disable=cyclic-import
534        from bauiv1lib.purchase import PurchaseWindow
535        from bauiv1lib.soundtrack.edit import SoundtrackEditWindow
536
537        if (
538            bui.app.classic is not None
539            and not bui.app.classic.accounts.have_pro_options()
540        ):
541            PurchaseWindow(items=['pro'])
542            return
543        self._save_state()
544        bui.containerwidget(edit=self._root_widget, transition='out_left')
545        SoundtrackEditWindow(existing_soundtrack=None)
546
547    def _create_done(self, new_soundtrack: str) -> None:
548        if new_soundtrack is not None:
549            bui.getsound('gunCocking').play()
550            self._refresh(select_soundtrack=new_soundtrack)
551
552    def _save_state(self) -> None:
553        try:
554            sel = self._root_widget.get_selected_child()
555            if sel == self._scrollwidget:
556                sel_name = 'Scroll'
557            elif sel == self._new_button:
558                sel_name = 'New'
559            elif sel == self._edit_button:
560                sel_name = 'Edit'
561            elif sel == self._duplicate_button:
562                sel_name = 'Duplicate'
563            elif sel == self._delete_button:
564                sel_name = 'Delete'
565            elif sel == self._back_button:
566                sel_name = 'Back'
567            else:
568                raise ValueError(f'unrecognized selection \'{sel}\'')
569            assert bui.app.classic is not None
570            bui.app.ui_v1.window_states[type(self)] = sel_name
571        except Exception:
572            logging.exception('Error saving state for %s.', self)
573
574    def _restore_state(self) -> None:
575        try:
576            assert bui.app.classic is not None
577            sel_name = bui.app.ui_v1.window_states.get(type(self))
578            if sel_name == 'Scroll':
579                sel = self._scrollwidget
580            elif sel_name == 'New':
581                sel = self._new_button
582            elif sel_name == 'Edit':
583                sel = self._edit_button
584            elif sel_name == 'Duplicate':
585                sel = self._duplicate_button
586            elif sel_name == 'Delete':
587                sel = self._delete_button
588            else:
589                sel = self._scrollwidget
590            bui.containerwidget(edit=self._root_widget, selected_child=sel)
591        except Exception:
592            logging.exception('Error restoring state for %s.', self)
class SoundtrackBrowserWindow(bauiv1._uitypes.Window):
 18class SoundtrackBrowserWindow(bui.Window):
 19    """Window for browsing soundtracks."""
 20
 21    def __init__(
 22        self,
 23        transition: str = 'in_right',
 24        origin_widget: bui.Widget | None = None,
 25    ):
 26        # pylint: disable=too-many-locals
 27        # pylint: disable=too-many-statements
 28
 29        # If they provided an origin-widget, scale up from that.
 30        scale_origin: tuple[float, float] | None
 31        if origin_widget is not None:
 32            self._transition_out = 'out_scale'
 33            scale_origin = origin_widget.get_screen_space_center()
 34            transition = 'in_scale'
 35        else:
 36            self._transition_out = 'out_right'
 37            scale_origin = None
 38
 39        self._r = 'editSoundtrackWindow'
 40        assert bui.app.classic is not None
 41        uiscale = bui.app.ui_v1.uiscale
 42        self._width = 800 if uiscale is bui.UIScale.SMALL else 600
 43        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 44        self._height = (
 45            340
 46            if uiscale is bui.UIScale.SMALL
 47            else 370
 48            if uiscale is bui.UIScale.MEDIUM
 49            else 440
 50        )
 51        spacing = 40.0
 52        v = self._height - 40.0
 53        v -= spacing * 1.0
 54
 55        super().__init__(
 56            root_widget=bui.containerwidget(
 57                size=(self._width, self._height),
 58                transition=transition,
 59                toolbar_visibility='menu_minimal',
 60                scale_origin_stack_offset=scale_origin,
 61                scale=(
 62                    2.3
 63                    if uiscale is bui.UIScale.SMALL
 64                    else 1.6
 65                    if uiscale is bui.UIScale.MEDIUM
 66                    else 1.0
 67                ),
 68                stack_offset=(0, -18)
 69                if uiscale is bui.UIScale.SMALL
 70                else (0, 0),
 71            )
 72        )
 73
 74        assert bui.app.classic is not None
 75        if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
 76            self._back_button = None
 77        else:
 78            self._back_button = bui.buttonwidget(
 79                parent=self._root_widget,
 80                position=(45 + x_inset, self._height - 60),
 81                size=(120, 60),
 82                scale=0.8,
 83                label=bui.Lstr(resource='backText'),
 84                button_type='back',
 85                autoselect=True,
 86            )
 87            bui.buttonwidget(
 88                edit=self._back_button,
 89                button_type='backSmall',
 90                size=(60, 60),
 91                label=bui.charstr(bui.SpecialChar.BACK),
 92            )
 93        bui.textwidget(
 94            parent=self._root_widget,
 95            position=(self._width * 0.5, self._height - 35),
 96            size=(0, 0),
 97            maxwidth=300,
 98            text=bui.Lstr(resource=self._r + '.titleText'),
 99            color=bui.app.ui_v1.title_color,
100            h_align='center',
101            v_align='center',
102        )
103
104        h = 43 + x_inset
105        v = self._height - 60
106        b_color = (0.6, 0.53, 0.63)
107        b_textcolor = (0.75, 0.7, 0.8)
108        lock_tex = bui.gettexture('lock')
109        self._lock_images: list[bui.Widget] = []
110
111        scl = (
112            1.0
113            if uiscale is bui.UIScale.SMALL
114            else 1.13
115            if uiscale is bui.UIScale.MEDIUM
116            else 1.4
117        )
118        v -= 60.0 * scl
119        self._new_button = btn = bui.buttonwidget(
120            parent=self._root_widget,
121            position=(h, v),
122            size=(100, 55.0 * scl),
123            on_activate_call=self._new_soundtrack,
124            color=b_color,
125            button_type='square',
126            autoselect=True,
127            textcolor=b_textcolor,
128            text_scale=0.7,
129            label=bui.Lstr(resource=self._r + '.newText'),
130        )
131        self._lock_images.append(
132            bui.imagewidget(
133                parent=self._root_widget,
134                size=(30, 30),
135                draw_controller=btn,
136                position=(h - 10, v + 55.0 * scl - 28),
137                texture=lock_tex,
138            )
139        )
140
141        if self._back_button is None:
142            bui.widget(
143                edit=btn,
144                left_widget=bui.get_special_widget('back_button'),
145            )
146        v -= 60.0 * scl
147
148        self._edit_button = btn = bui.buttonwidget(
149            parent=self._root_widget,
150            position=(h, v),
151            size=(100, 55.0 * scl),
152            on_activate_call=self._edit_soundtrack,
153            color=b_color,
154            button_type='square',
155            autoselect=True,
156            textcolor=b_textcolor,
157            text_scale=0.7,
158            label=bui.Lstr(resource=self._r + '.editText'),
159        )
160        self._lock_images.append(
161            bui.imagewidget(
162                parent=self._root_widget,
163                size=(30, 30),
164                draw_controller=btn,
165                position=(h - 10, v + 55.0 * scl - 28),
166                texture=lock_tex,
167            )
168        )
169        if self._back_button is None:
170            bui.widget(
171                edit=btn,
172                left_widget=bui.get_special_widget('back_button'),
173            )
174        v -= 60.0 * scl
175
176        self._duplicate_button = btn = bui.buttonwidget(
177            parent=self._root_widget,
178            position=(h, v),
179            size=(100, 55.0 * scl),
180            on_activate_call=self._duplicate_soundtrack,
181            button_type='square',
182            autoselect=True,
183            color=b_color,
184            textcolor=b_textcolor,
185            text_scale=0.7,
186            label=bui.Lstr(resource=self._r + '.duplicateText'),
187        )
188        self._lock_images.append(
189            bui.imagewidget(
190                parent=self._root_widget,
191                size=(30, 30),
192                draw_controller=btn,
193                position=(h - 10, v + 55.0 * scl - 28),
194                texture=lock_tex,
195            )
196        )
197        if self._back_button is None:
198            bui.widget(
199                edit=btn,
200                left_widget=bui.get_special_widget('back_button'),
201            )
202        v -= 60.0 * scl
203
204        self._delete_button = btn = bui.buttonwidget(
205            parent=self._root_widget,
206            position=(h, v),
207            size=(100, 55.0 * scl),
208            on_activate_call=self._delete_soundtrack,
209            color=b_color,
210            button_type='square',
211            autoselect=True,
212            textcolor=b_textcolor,
213            text_scale=0.7,
214            label=bui.Lstr(resource=self._r + '.deleteText'),
215        )
216        self._lock_images.append(
217            bui.imagewidget(
218                parent=self._root_widget,
219                size=(30, 30),
220                draw_controller=btn,
221                position=(h - 10, v + 55.0 * scl - 28),
222                texture=lock_tex,
223            )
224        )
225        if self._back_button is None:
226            bui.widget(
227                edit=btn,
228                left_widget=bui.get_special_widget('back_button'),
229            )
230
231        # Keep our lock images up to date/etc.
232        self._update_timer = bui.AppTimer(
233            1.0, bui.WeakCall(self._update), repeat=True
234        )
235        self._update()
236
237        v = self._height - 65
238        scroll_height = self._height - 105
239        v -= scroll_height
240        self._scrollwidget = scrollwidget = bui.scrollwidget(
241            parent=self._root_widget,
242            position=(152 + x_inset, v),
243            highlight=False,
244            size=(self._width - (205 + 2 * x_inset), scroll_height),
245        )
246        bui.widget(
247            edit=self._scrollwidget,
248            left_widget=self._new_button,
249            right_widget=bui.get_special_widget('party_button')
250            if bui.app.ui_v1.use_toolbars
251            else self._scrollwidget,
252        )
253        self._col = bui.columnwidget(parent=scrollwidget, border=2, margin=0)
254
255        self._soundtracks: dict[str, Any] | None = None
256        self._selected_soundtrack: str | None = None
257        self._selected_soundtrack_index: int | None = None
258        self._soundtrack_widgets: list[bui.Widget] = []
259        self._allow_changing_soundtracks = False
260        self._refresh()
261        if self._back_button is not None:
262            bui.buttonwidget(
263                edit=self._back_button, on_activate_call=self._back
264            )
265            bui.containerwidget(
266                edit=self._root_widget, cancel_button=self._back_button
267            )
268        else:
269            bui.containerwidget(
270                edit=self._root_widget, on_cancel_call=self._back
271            )
272
273    def _update(self) -> None:
274        have = (
275            bui.app.classic is None
276            or bui.app.classic.accounts.have_pro_options()
277        )
278        for lock in self._lock_images:
279            bui.imagewidget(edit=lock, opacity=0.0 if have else 1.0)
280
281    def _do_delete_soundtrack(self) -> None:
282        cfg = bui.app.config
283        soundtracks = cfg.setdefault('Soundtracks', {})
284        if self._selected_soundtrack in soundtracks:
285            del soundtracks[self._selected_soundtrack]
286        cfg.commit()
287        bui.getsound('shieldDown').play()
288        assert self._selected_soundtrack_index is not None
289        assert self._soundtracks is not None
290        if self._selected_soundtrack_index >= len(self._soundtracks):
291            self._selected_soundtrack_index = len(self._soundtracks)
292        self._refresh()
293
294    def _delete_soundtrack(self) -> None:
295        # pylint: disable=cyclic-import
296        from bauiv1lib.purchase import PurchaseWindow
297        from bauiv1lib.confirm import ConfirmWindow
298
299        if (
300            bui.app.classic is not None
301            and not bui.app.classic.accounts.have_pro_options()
302        ):
303            PurchaseWindow(items=['pro'])
304            return
305        if self._selected_soundtrack is None:
306            return
307        if self._selected_soundtrack == '__default__':
308            bui.getsound('error').play()
309            bui.screenmessage(
310                bui.Lstr(resource=self._r + '.cantDeleteDefaultText'),
311                color=(1, 0, 0),
312            )
313        else:
314            ConfirmWindow(
315                bui.Lstr(
316                    resource=self._r + '.deleteConfirmText',
317                    subs=[('${NAME}', self._selected_soundtrack)],
318                ),
319                self._do_delete_soundtrack,
320                450,
321                150,
322            )
323
324    def _duplicate_soundtrack(self) -> None:
325        # pylint: disable=cyclic-import
326        from bauiv1lib.purchase import PurchaseWindow
327
328        if (
329            bui.app.classic is not None
330            and not bui.app.classic.accounts.have_pro_options()
331        ):
332            PurchaseWindow(items=['pro'])
333            return
334        cfg = bui.app.config
335        cfg.setdefault('Soundtracks', {})
336
337        if self._selected_soundtrack is None:
338            return
339        sdtk: dict[str, Any]
340        if self._selected_soundtrack == '__default__':
341            sdtk = {}
342        else:
343            sdtk = cfg['Soundtracks'][self._selected_soundtrack]
344
345        # Find a valid dup name that doesn't exist.
346        test_index = 1
347        copy_text = bui.Lstr(resource='copyOfText').evaluate()
348        # Get just 'Copy' or whatnot.
349        copy_word = copy_text.replace('${NAME}', '').strip()
350        base_name = self._get_soundtrack_display_name(
351            self._selected_soundtrack
352        ).evaluate()
353        assert isinstance(base_name, str)
354
355        # If it looks like a copy, strip digits and spaces off the end.
356        if copy_word in base_name:
357            while base_name[-1].isdigit() or base_name[-1] == ' ':
358                base_name = base_name[:-1]
359        while True:
360            if copy_word in base_name:
361                test_name = base_name
362            else:
363                test_name = copy_text.replace('${NAME}', base_name)
364            if test_index > 1:
365                test_name += ' ' + str(test_index)
366            if test_name not in cfg['Soundtracks']:
367                break
368            test_index += 1
369
370        cfg['Soundtracks'][test_name] = copy.deepcopy(sdtk)
371        cfg.commit()
372        self._refresh(select_soundtrack=test_name)
373
374    def _select(self, name: str, index: int) -> None:
375        assert bui.app.classic is not None
376        music = bui.app.classic.music
377        self._selected_soundtrack_index = index
378        self._selected_soundtrack = name
379        cfg = bui.app.config
380        current_soundtrack = cfg.setdefault('Soundtrack', '__default__')
381
382        # If it varies from current, commit and play.
383        if current_soundtrack != name and self._allow_changing_soundtracks:
384            bui.getsound('gunCocking').play()
385            cfg['Soundtrack'] = self._selected_soundtrack
386            cfg.commit()
387
388            # Just play whats already playing.. this'll grab it from the
389            # new soundtrack.
390            music.do_play_music(
391                music.music_types[bui.app.classic.MusicPlayMode.REGULAR]
392            )
393
394    def _back(self) -> None:
395        # pylint: disable=cyclic-import
396        from bauiv1lib.settings import audio
397
398        self._save_state()
399        bui.containerwidget(
400            edit=self._root_widget, transition=self._transition_out
401        )
402        assert bui.app.classic is not None
403        bui.app.ui_v1.set_main_menu_window(
404            audio.AudioSettingsWindow(transition='in_left').get_root_widget()
405        )
406
407    def _edit_soundtrack_with_sound(self) -> None:
408        # pylint: disable=cyclic-import
409        from bauiv1lib.purchase import PurchaseWindow
410
411        if (
412            bui.app.classic is not None
413            and not bui.app.classic.accounts.have_pro_options()
414        ):
415            PurchaseWindow(items=['pro'])
416            return
417        bui.getsound('swish').play()
418        self._edit_soundtrack()
419
420    def _edit_soundtrack(self) -> None:
421        # pylint: disable=cyclic-import
422        from bauiv1lib.purchase import PurchaseWindow
423        from bauiv1lib.soundtrack.edit import SoundtrackEditWindow
424
425        if (
426            bui.app.classic is not None
427            and not bui.app.classic.accounts.have_pro_options()
428        ):
429            PurchaseWindow(items=['pro'])
430            return
431        if self._selected_soundtrack is None:
432            return
433        if self._selected_soundtrack == '__default__':
434            bui.getsound('error').play()
435            bui.screenmessage(
436                bui.Lstr(resource=self._r + '.cantEditDefaultText'),
437                color=(1, 0, 0),
438            )
439            return
440
441        self._save_state()
442        bui.containerwidget(edit=self._root_widget, transition='out_left')
443        assert bui.app.classic is not None
444        bui.app.ui_v1.set_main_menu_window(
445            SoundtrackEditWindow(
446                existing_soundtrack=self._selected_soundtrack
447            ).get_root_widget()
448        )
449
450    def _get_soundtrack_display_name(self, soundtrack: str) -> bui.Lstr:
451        if soundtrack == '__default__':
452            return bui.Lstr(resource=self._r + '.defaultSoundtrackNameText')
453        return bui.Lstr(value=soundtrack)
454
455    def _refresh(self, select_soundtrack: str | None = None) -> None:
456        from efro.util import asserttype
457
458        self._allow_changing_soundtracks = False
459        old_selection = self._selected_soundtrack
460
461        # If there was no prev selection, look in prefs.
462        if old_selection is None:
463            old_selection = bui.app.config.get('Soundtrack')
464        old_selection_index = self._selected_soundtrack_index
465
466        # Delete old.
467        while self._soundtrack_widgets:
468            self._soundtrack_widgets.pop().delete()
469
470        self._soundtracks = bui.app.config.get('Soundtracks', {})
471        assert self._soundtracks is not None
472        items = list(self._soundtracks.items())
473        items.sort(key=lambda x: asserttype(x[0], str).lower())
474        items = [('__default__', None)] + items  # default is always first
475        index = 0
476        for pname, _pval in items:
477            assert pname is not None
478            txtw = bui.textwidget(
479                parent=self._col,
480                size=(self._width - 40, 24),
481                text=self._get_soundtrack_display_name(pname),
482                h_align='left',
483                v_align='center',
484                maxwidth=self._width - 110,
485                always_highlight=True,
486                on_select_call=bui.WeakCall(self._select, pname, index),
487                on_activate_call=self._edit_soundtrack_with_sound,
488                selectable=True,
489            )
490            if index == 0:
491                bui.widget(edit=txtw, up_widget=self._back_button)
492            self._soundtrack_widgets.append(txtw)
493
494            # Select this one if the user requested it
495            if select_soundtrack is not None:
496                if pname == select_soundtrack:
497                    bui.columnwidget(
498                        edit=self._col, selected_child=txtw, visible_child=txtw
499                    )
500            else:
501                # Select this one if it was previously selected.
502                # Go by index if there's one.
503                if old_selection_index is not None:
504                    if index == old_selection_index:
505                        bui.columnwidget(
506                            edit=self._col,
507                            selected_child=txtw,
508                            visible_child=txtw,
509                        )
510                else:  # Otherwise look by name.
511                    if pname == old_selection:
512                        bui.columnwidget(
513                            edit=self._col,
514                            selected_child=txtw,
515                            visible_child=txtw,
516                        )
517            index += 1
518
519        # Explicitly run select callback on current one and re-enable
520        # callbacks.
521
522        # Eww need to run this in a timer so it happens after our select
523        # callbacks. With a small-enough time sometimes it happens before
524        # anyway. Ew. need a way to just schedule a callable i guess.
525        bui.apptimer(0.1, bui.WeakCall(self._set_allow_changing))
526
527    def _set_allow_changing(self) -> None:
528        self._allow_changing_soundtracks = True
529        assert self._selected_soundtrack is not None
530        assert self._selected_soundtrack_index is not None
531        self._select(self._selected_soundtrack, self._selected_soundtrack_index)
532
533    def _new_soundtrack(self) -> None:
534        # pylint: disable=cyclic-import
535        from bauiv1lib.purchase import PurchaseWindow
536        from bauiv1lib.soundtrack.edit import SoundtrackEditWindow
537
538        if (
539            bui.app.classic is not None
540            and not bui.app.classic.accounts.have_pro_options()
541        ):
542            PurchaseWindow(items=['pro'])
543            return
544        self._save_state()
545        bui.containerwidget(edit=self._root_widget, transition='out_left')
546        SoundtrackEditWindow(existing_soundtrack=None)
547
548    def _create_done(self, new_soundtrack: str) -> None:
549        if new_soundtrack is not None:
550            bui.getsound('gunCocking').play()
551            self._refresh(select_soundtrack=new_soundtrack)
552
553    def _save_state(self) -> None:
554        try:
555            sel = self._root_widget.get_selected_child()
556            if sel == self._scrollwidget:
557                sel_name = 'Scroll'
558            elif sel == self._new_button:
559                sel_name = 'New'
560            elif sel == self._edit_button:
561                sel_name = 'Edit'
562            elif sel == self._duplicate_button:
563                sel_name = 'Duplicate'
564            elif sel == self._delete_button:
565                sel_name = 'Delete'
566            elif sel == self._back_button:
567                sel_name = 'Back'
568            else:
569                raise ValueError(f'unrecognized selection \'{sel}\'')
570            assert bui.app.classic is not None
571            bui.app.ui_v1.window_states[type(self)] = sel_name
572        except Exception:
573            logging.exception('Error saving state for %s.', self)
574
575    def _restore_state(self) -> None:
576        try:
577            assert bui.app.classic is not None
578            sel_name = bui.app.ui_v1.window_states.get(type(self))
579            if sel_name == 'Scroll':
580                sel = self._scrollwidget
581            elif sel_name == 'New':
582                sel = self._new_button
583            elif sel_name == 'Edit':
584                sel = self._edit_button
585            elif sel_name == 'Duplicate':
586                sel = self._duplicate_button
587            elif sel_name == 'Delete':
588                sel = self._delete_button
589            else:
590                sel = self._scrollwidget
591            bui.containerwidget(edit=self._root_widget, selected_child=sel)
592        except Exception:
593            logging.exception('Error restoring state for %s.', self)

Window for browsing soundtracks.

SoundtrackBrowserWindow( transition: str = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 21    def __init__(
 22        self,
 23        transition: str = 'in_right',
 24        origin_widget: bui.Widget | None = None,
 25    ):
 26        # pylint: disable=too-many-locals
 27        # pylint: disable=too-many-statements
 28
 29        # If they provided an origin-widget, scale up from that.
 30        scale_origin: tuple[float, float] | None
 31        if origin_widget is not None:
 32            self._transition_out = 'out_scale'
 33            scale_origin = origin_widget.get_screen_space_center()
 34            transition = 'in_scale'
 35        else:
 36            self._transition_out = 'out_right'
 37            scale_origin = None
 38
 39        self._r = 'editSoundtrackWindow'
 40        assert bui.app.classic is not None
 41        uiscale = bui.app.ui_v1.uiscale
 42        self._width = 800 if uiscale is bui.UIScale.SMALL else 600
 43        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 44        self._height = (
 45            340
 46            if uiscale is bui.UIScale.SMALL
 47            else 370
 48            if uiscale is bui.UIScale.MEDIUM
 49            else 440
 50        )
 51        spacing = 40.0
 52        v = self._height - 40.0
 53        v -= spacing * 1.0
 54
 55        super().__init__(
 56            root_widget=bui.containerwidget(
 57                size=(self._width, self._height),
 58                transition=transition,
 59                toolbar_visibility='menu_minimal',
 60                scale_origin_stack_offset=scale_origin,
 61                scale=(
 62                    2.3
 63                    if uiscale is bui.UIScale.SMALL
 64                    else 1.6
 65                    if uiscale is bui.UIScale.MEDIUM
 66                    else 1.0
 67                ),
 68                stack_offset=(0, -18)
 69                if uiscale is bui.UIScale.SMALL
 70                else (0, 0),
 71            )
 72        )
 73
 74        assert bui.app.classic is not None
 75        if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
 76            self._back_button = None
 77        else:
 78            self._back_button = bui.buttonwidget(
 79                parent=self._root_widget,
 80                position=(45 + x_inset, self._height - 60),
 81                size=(120, 60),
 82                scale=0.8,
 83                label=bui.Lstr(resource='backText'),
 84                button_type='back',
 85                autoselect=True,
 86            )
 87            bui.buttonwidget(
 88                edit=self._back_button,
 89                button_type='backSmall',
 90                size=(60, 60),
 91                label=bui.charstr(bui.SpecialChar.BACK),
 92            )
 93        bui.textwidget(
 94            parent=self._root_widget,
 95            position=(self._width * 0.5, self._height - 35),
 96            size=(0, 0),
 97            maxwidth=300,
 98            text=bui.Lstr(resource=self._r + '.titleText'),
 99            color=bui.app.ui_v1.title_color,
100            h_align='center',
101            v_align='center',
102        )
103
104        h = 43 + x_inset
105        v = self._height - 60
106        b_color = (0.6, 0.53, 0.63)
107        b_textcolor = (0.75, 0.7, 0.8)
108        lock_tex = bui.gettexture('lock')
109        self._lock_images: list[bui.Widget] = []
110
111        scl = (
112            1.0
113            if uiscale is bui.UIScale.SMALL
114            else 1.13
115            if uiscale is bui.UIScale.MEDIUM
116            else 1.4
117        )
118        v -= 60.0 * scl
119        self._new_button = btn = bui.buttonwidget(
120            parent=self._root_widget,
121            position=(h, v),
122            size=(100, 55.0 * scl),
123            on_activate_call=self._new_soundtrack,
124            color=b_color,
125            button_type='square',
126            autoselect=True,
127            textcolor=b_textcolor,
128            text_scale=0.7,
129            label=bui.Lstr(resource=self._r + '.newText'),
130        )
131        self._lock_images.append(
132            bui.imagewidget(
133                parent=self._root_widget,
134                size=(30, 30),
135                draw_controller=btn,
136                position=(h - 10, v + 55.0 * scl - 28),
137                texture=lock_tex,
138            )
139        )
140
141        if self._back_button is None:
142            bui.widget(
143                edit=btn,
144                left_widget=bui.get_special_widget('back_button'),
145            )
146        v -= 60.0 * scl
147
148        self._edit_button = btn = bui.buttonwidget(
149            parent=self._root_widget,
150            position=(h, v),
151            size=(100, 55.0 * scl),
152            on_activate_call=self._edit_soundtrack,
153            color=b_color,
154            button_type='square',
155            autoselect=True,
156            textcolor=b_textcolor,
157            text_scale=0.7,
158            label=bui.Lstr(resource=self._r + '.editText'),
159        )
160        self._lock_images.append(
161            bui.imagewidget(
162                parent=self._root_widget,
163                size=(30, 30),
164                draw_controller=btn,
165                position=(h - 10, v + 55.0 * scl - 28),
166                texture=lock_tex,
167            )
168        )
169        if self._back_button is None:
170            bui.widget(
171                edit=btn,
172                left_widget=bui.get_special_widget('back_button'),
173            )
174        v -= 60.0 * scl
175
176        self._duplicate_button = btn = bui.buttonwidget(
177            parent=self._root_widget,
178            position=(h, v),
179            size=(100, 55.0 * scl),
180            on_activate_call=self._duplicate_soundtrack,
181            button_type='square',
182            autoselect=True,
183            color=b_color,
184            textcolor=b_textcolor,
185            text_scale=0.7,
186            label=bui.Lstr(resource=self._r + '.duplicateText'),
187        )
188        self._lock_images.append(
189            bui.imagewidget(
190                parent=self._root_widget,
191                size=(30, 30),
192                draw_controller=btn,
193                position=(h - 10, v + 55.0 * scl - 28),
194                texture=lock_tex,
195            )
196        )
197        if self._back_button is None:
198            bui.widget(
199                edit=btn,
200                left_widget=bui.get_special_widget('back_button'),
201            )
202        v -= 60.0 * scl
203
204        self._delete_button = btn = bui.buttonwidget(
205            parent=self._root_widget,
206            position=(h, v),
207            size=(100, 55.0 * scl),
208            on_activate_call=self._delete_soundtrack,
209            color=b_color,
210            button_type='square',
211            autoselect=True,
212            textcolor=b_textcolor,
213            text_scale=0.7,
214            label=bui.Lstr(resource=self._r + '.deleteText'),
215        )
216        self._lock_images.append(
217            bui.imagewidget(
218                parent=self._root_widget,
219                size=(30, 30),
220                draw_controller=btn,
221                position=(h - 10, v + 55.0 * scl - 28),
222                texture=lock_tex,
223            )
224        )
225        if self._back_button is None:
226            bui.widget(
227                edit=btn,
228                left_widget=bui.get_special_widget('back_button'),
229            )
230
231        # Keep our lock images up to date/etc.
232        self._update_timer = bui.AppTimer(
233            1.0, bui.WeakCall(self._update), repeat=True
234        )
235        self._update()
236
237        v = self._height - 65
238        scroll_height = self._height - 105
239        v -= scroll_height
240        self._scrollwidget = scrollwidget = bui.scrollwidget(
241            parent=self._root_widget,
242            position=(152 + x_inset, v),
243            highlight=False,
244            size=(self._width - (205 + 2 * x_inset), scroll_height),
245        )
246        bui.widget(
247            edit=self._scrollwidget,
248            left_widget=self._new_button,
249            right_widget=bui.get_special_widget('party_button')
250            if bui.app.ui_v1.use_toolbars
251            else self._scrollwidget,
252        )
253        self._col = bui.columnwidget(parent=scrollwidget, border=2, margin=0)
254
255        self._soundtracks: dict[str, Any] | None = None
256        self._selected_soundtrack: str | None = None
257        self._selected_soundtrack_index: int | None = None
258        self._soundtrack_widgets: list[bui.Widget] = []
259        self._allow_changing_soundtracks = False
260        self._refresh()
261        if self._back_button is not None:
262            bui.buttonwidget(
263                edit=self._back_button, on_activate_call=self._back
264            )
265            bui.containerwidget(
266                edit=self._root_widget, cancel_button=self._back_button
267            )
268        else:
269            bui.containerwidget(
270                edit=self._root_widget, on_cancel_call=self._back
271            )
Inherited Members
bauiv1._uitypes.Window
get_root_widget