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

Window for browsing soundtracks.

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

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
260    @override
261    def get_main_window_state(self) -> bui.MainWindowState:
262        # Support recreating our window for back/refresh purposes.
263        cls = type(self)
264        return bui.BasicMainWindowState(
265            create_call=lambda transition, origin_widget: cls(
266                transition=transition, origin_widget=origin_widget
267            )
268        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
270    @override
271    def on_main_window_close(self) -> None:
272        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_is_top_level
main_window_close
main_window_has_control
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget