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