bauiv1lib.soundtrack.edit

Provides UI for editing a soundtrack.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides UI for editing a soundtrack."""
  4
  5from __future__ import annotations
  6
  7import copy
  8import os
  9from typing import TYPE_CHECKING, cast
 10
 11import bascenev1 as bs
 12import bauiv1 as bui
 13
 14if TYPE_CHECKING:
 15    from typing import Any
 16
 17
 18class SoundtrackEditWindow(bui.Window):
 19    """Window for editing a soundtrack."""
 20
 21    def __init__(
 22        self,
 23        existing_soundtrack: str | dict[str, Any] | None,
 24        transition: str = 'in_right',
 25    ):
 26        # pylint: disable=too-many-statements
 27        appconfig = bui.app.config
 28        self._r = 'editSoundtrackWindow'
 29        self._folder_tex = bui.gettexture('folder')
 30        self._file_tex = bui.gettexture('file')
 31        assert bui.app.classic is not None
 32        uiscale = bui.app.ui_v1.uiscale
 33        self._width = 848 if uiscale is bui.UIScale.SMALL else 648
 34        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 35        self._height = (
 36            395
 37            if uiscale is bui.UIScale.SMALL
 38            else 450 if uiscale is bui.UIScale.MEDIUM else 560
 39        )
 40        super().__init__(
 41            root_widget=bui.containerwidget(
 42                size=(self._width, self._height),
 43                transition=transition,
 44                scale=(
 45                    2.08
 46                    if uiscale is bui.UIScale.SMALL
 47                    else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
 48                ),
 49                stack_offset=(
 50                    (0, -48)
 51                    if uiscale is bui.UIScale.SMALL
 52                    else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0)
 53                ),
 54            )
 55        )
 56        cancel_button = bui.buttonwidget(
 57            parent=self._root_widget,
 58            position=(38 + x_inset, self._height - 60),
 59            size=(160, 60),
 60            autoselect=True,
 61            label=bui.Lstr(resource='cancelText'),
 62            scale=0.8,
 63        )
 64        save_button = bui.buttonwidget(
 65            parent=self._root_widget,
 66            position=(self._width - (168 + x_inset), self._height - 60),
 67            autoselect=True,
 68            size=(160, 60),
 69            label=bui.Lstr(resource='saveText'),
 70            scale=0.8,
 71        )
 72        bui.widget(edit=save_button, left_widget=cancel_button)
 73        bui.widget(edit=cancel_button, right_widget=save_button)
 74        bui.textwidget(
 75            parent=self._root_widget,
 76            position=(0, self._height - 50),
 77            size=(self._width, 25),
 78            text=bui.Lstr(
 79                resource=self._r
 80                + (
 81                    '.editSoundtrackText'
 82                    if existing_soundtrack is not None
 83                    else '.newSoundtrackText'
 84                )
 85            ),
 86            color=bui.app.ui_v1.title_color,
 87            h_align='center',
 88            v_align='center',
 89            maxwidth=280,
 90        )
 91        v = self._height - 110
 92        if 'Soundtracks' not in appconfig:
 93            appconfig['Soundtracks'] = {}
 94
 95        self._soundtrack_name: str | None
 96        self._existing_soundtrack_name: str | None
 97        if existing_soundtrack is not None:
 98            # if they passed just a name, pull info from that soundtrack
 99            if isinstance(existing_soundtrack, str):
100                self._soundtrack = copy.deepcopy(
101                    appconfig['Soundtracks'][existing_soundtrack]
102                )
103                self._soundtrack_name = existing_soundtrack
104                self._existing_soundtrack_name = existing_soundtrack
105                self._last_edited_song_type = None
106            else:
107                # otherwise they can pass info on an in-progress edit
108                self._soundtrack = existing_soundtrack['soundtrack']
109                self._soundtrack_name = existing_soundtrack['name']
110                self._existing_soundtrack_name = existing_soundtrack[
111                    'existing_name'
112                ]
113                self._last_edited_song_type = existing_soundtrack[
114                    'last_edited_song_type'
115                ]
116        else:
117            self._soundtrack_name = None
118            self._existing_soundtrack_name = None
119            self._soundtrack = {}
120            self._last_edited_song_type = None
121
122        bui.textwidget(
123            parent=self._root_widget,
124            text=bui.Lstr(resource=self._r + '.nameText'),
125            maxwidth=80,
126            scale=0.8,
127            position=(105 + x_inset, v + 19),
128            color=(0.8, 0.8, 0.8, 0.5),
129            size=(0, 0),
130            h_align='right',
131            v_align='center',
132        )
133
134        # if there's no initial value, find a good initial unused name
135        if existing_soundtrack is None:
136            i = 1
137            st_name_text = bui.Lstr(
138                resource=self._r + '.newSoundtrackNameText'
139            ).evaluate()
140            if '${COUNT}' not in st_name_text:
141                # make sure we insert number *somewhere*
142                st_name_text = st_name_text + ' ${COUNT}'
143            while True:
144                self._soundtrack_name = st_name_text.replace('${COUNT}', str(i))
145                if self._soundtrack_name not in appconfig['Soundtracks']:
146                    break
147                i += 1
148
149        self._text_field = bui.textwidget(
150            parent=self._root_widget,
151            position=(120 + x_inset, v - 5),
152            size=(self._width - (160 + 2 * x_inset), 43),
153            text=self._soundtrack_name,
154            h_align='left',
155            v_align='center',
156            max_chars=32,
157            autoselect=True,
158            description=bui.Lstr(resource=self._r + '.nameText'),
159            editable=True,
160            padding=4,
161            on_return_press_call=self._do_it_with_sound,
162        )
163
164        scroll_height = self._height - 180
165        self._scrollwidget = scrollwidget = bui.scrollwidget(
166            parent=self._root_widget,
167            highlight=False,
168            position=(40 + x_inset, v - (scroll_height + 10)),
169            size=(self._width - (80 + 2 * x_inset), scroll_height),
170            simple_culling_v=10,
171            claims_left_right=True,
172            claims_tab=True,
173            selection_loops_to_parent=True,
174        )
175        bui.widget(edit=self._text_field, down_widget=self._scrollwidget)
176        self._col = bui.columnwidget(
177            parent=scrollwidget,
178            claims_left_right=True,
179            claims_tab=True,
180            selection_loops_to_parent=True,
181        )
182
183        self._song_type_buttons: dict[str, bui.Widget] = {}
184        self._refresh()
185        bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
186        bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button)
187        bui.buttonwidget(edit=save_button, on_activate_call=self._do_it)
188        bui.containerwidget(edit=self._root_widget, start_button=save_button)
189        bui.widget(edit=self._text_field, up_widget=cancel_button)
190        bui.widget(edit=cancel_button, down_widget=self._text_field)
191
192    def _refresh(self) -> None:
193        for widget in self._col.get_children():
194            widget.delete()
195
196        types = [
197            'Menu',
198            'CharSelect',
199            'ToTheDeath',
200            'Onslaught',
201            'Keep Away',
202            'Race',
203            'Epic Race',
204            'ForwardMarch',
205            'FlagCatcher',
206            'Survival',
207            'Epic',
208            'Hockey',
209            'Football',
210            'Flying',
211            'Scary',
212            'Marching',
213            'GrandRomp',
214            'Chosen One',
215            'Scores',
216            'Victory',
217        ]
218
219        # FIXME: We should probably convert this to use translations.
220        type_names_translated = bui.app.lang.get_resource('soundtrackTypeNames')
221        prev_type_button: bui.Widget | None = None
222        prev_test_button: bui.Widget | None = None
223
224        for index, song_type in enumerate(types):
225            row = bui.rowwidget(
226                parent=self._col,
227                size=(self._width - 40, 40),
228                claims_left_right=True,
229                claims_tab=True,
230                selection_loops_to_parent=True,
231            )
232            type_name = type_names_translated.get(song_type, song_type)
233            bui.textwidget(
234                parent=row,
235                size=(230, 25),
236                always_highlight=True,
237                text=type_name,
238                scale=0.7,
239                h_align='left',
240                v_align='center',
241                maxwidth=190,
242            )
243
244            if song_type in self._soundtrack:
245                entry = self._soundtrack[song_type]
246            else:
247                entry = None
248
249            if entry is not None:
250                # Make sure they don't muck with this after it gets to us.
251                entry = copy.deepcopy(entry)
252
253            icon_type = self._get_entry_button_display_icon_type(entry)
254            self._song_type_buttons[song_type] = btn = bui.buttonwidget(
255                parent=row,
256                size=(230, 32),
257                label=self._get_entry_button_display_name(entry),
258                text_scale=0.6,
259                on_activate_call=bui.Call(
260                    self._get_entry, song_type, entry, type_name
261                ),
262                icon=(
263                    self._file_tex
264                    if icon_type == 'file'
265                    else self._folder_tex if icon_type == 'folder' else None
266                ),
267                icon_color=(
268                    (1.1, 0.8, 0.2) if icon_type == 'folder' else (1, 1, 1)
269                ),
270                left_widget=self._text_field,
271                iconscale=0.7,
272                autoselect=True,
273                up_widget=prev_type_button,
274            )
275            if index == 0:
276                bui.widget(edit=btn, up_widget=self._text_field)
277            bui.widget(edit=btn, down_widget=btn)
278
279            if (
280                self._last_edited_song_type is not None
281                and song_type == self._last_edited_song_type
282            ):
283                bui.containerwidget(
284                    edit=row, selected_child=btn, visible_child=btn
285                )
286                bui.containerwidget(
287                    edit=self._col, selected_child=row, visible_child=row
288                )
289                bui.containerwidget(
290                    edit=self._scrollwidget,
291                    selected_child=self._col,
292                    visible_child=self._col,
293                )
294                bui.containerwidget(
295                    edit=self._root_widget,
296                    selected_child=self._scrollwidget,
297                    visible_child=self._scrollwidget,
298                )
299
300            if prev_type_button is not None:
301                bui.widget(edit=prev_type_button, down_widget=btn)
302            prev_type_button = btn
303            bui.textwidget(parent=row, size=(10, 32), text='')  # spacing
304            assert bui.app.classic is not None
305            btn = bui.buttonwidget(
306                parent=row,
307                size=(50, 32),
308                label=bui.Lstr(resource=self._r + '.testText'),
309                text_scale=0.6,
310                on_activate_call=bui.Call(self._test, bs.MusicType(song_type)),
311                up_widget=(
312                    prev_test_button
313                    if prev_test_button is not None
314                    else self._text_field
315                ),
316            )
317            if prev_test_button is not None:
318                bui.widget(edit=prev_test_button, down_widget=btn)
319            bui.widget(edit=btn, down_widget=btn, right_widget=btn)
320            prev_test_button = btn
321
322    @classmethod
323    def _restore_editor(
324        cls, state: dict[str, Any], musictype: str, entry: Any
325    ) -> None:
326        assert bui.app.classic is not None
327        music = bui.app.classic.music
328
329        # Apply the change and recreate the window.
330        soundtrack = state['soundtrack']
331        existing_entry = (
332            None if musictype not in soundtrack else soundtrack[musictype]
333        )
334        if existing_entry != entry:
335            bui.getsound('gunCocking').play()
336
337        # Make sure this doesn't get mucked with after we get it.
338        if entry is not None:
339            entry = copy.deepcopy(entry)
340
341        entry_type = music.get_soundtrack_entry_type(entry)
342        if entry_type == 'default':
343            # For 'default' entries simply exclude them from the list.
344            if musictype in soundtrack:
345                del soundtrack[musictype]
346        else:
347            soundtrack[musictype] = entry
348
349        bui.app.ui_v1.set_main_menu_window(
350            cls(state, transition='in_left').get_root_widget(),
351            from_window=False,  # Disable check here.
352        )
353
354    def _get_entry(
355        self, song_type: str, entry: Any, selection_target_name: str
356    ) -> None:
357        assert bui.app.classic is not None
358        music = bui.app.classic.music
359
360        # no-op if our underlying widget is dead or on its way out.
361        if not self._root_widget or self._root_widget.transitioning_out:
362            return
363
364        if selection_target_name != '':
365            selection_target_name = "'" + selection_target_name + "'"
366        state = {
367            'name': self._soundtrack_name,
368            'existing_name': self._existing_soundtrack_name,
369            'soundtrack': self._soundtrack,
370            'last_edited_song_type': song_type,
371        }
372        bui.containerwidget(edit=self._root_widget, transition='out_left')
373        bui.app.ui_v1.set_main_menu_window(
374            music.get_music_player()
375            .select_entry(
376                bui.Call(self._restore_editor, state, song_type),
377                entry,
378                selection_target_name,
379            )
380            .get_root_widget(),
381            from_window=self._root_widget,
382        )
383
384    def _test(self, song_type: bs.MusicType) -> None:
385        assert bui.app.classic is not None
386        music = bui.app.classic.music
387
388        # Warn if volume is zero.
389        if bui.app.config.resolve('Music Volume') < 0.01:
390            bui.getsound('error').play()
391            bui.screenmessage(
392                bui.Lstr(resource=self._r + '.musicVolumeZeroWarning'),
393                color=(1, 0.5, 0),
394            )
395        music.set_music_play_mode(bui.app.classic.MusicPlayMode.TEST)
396        music.do_play_music(
397            song_type,
398            mode=bui.app.classic.MusicPlayMode.TEST,
399            testsoundtrack=self._soundtrack,
400        )
401
402    def _get_entry_button_display_name(self, entry: Any) -> str | bui.Lstr:
403        assert bui.app.classic is not None
404        music = bui.app.classic.music
405        etype = music.get_soundtrack_entry_type(entry)
406        ename: str | bui.Lstr
407        if etype == 'default':
408            ename = bui.Lstr(resource=self._r + '.defaultGameMusicText')
409        elif etype in ('musicFile', 'musicFolder'):
410            ename = os.path.basename(music.get_soundtrack_entry_name(entry))
411        else:
412            ename = music.get_soundtrack_entry_name(entry)
413        return ename
414
415    def _get_entry_button_display_icon_type(self, entry: Any) -> str | None:
416        assert bui.app.classic is not None
417        music = bui.app.classic.music
418        etype = music.get_soundtrack_entry_type(entry)
419        if etype == 'musicFile':
420            return 'file'
421        if etype == 'musicFolder':
422            return 'folder'
423        return None
424
425    def _cancel(self) -> None:
426        from bauiv1lib.soundtrack import browser as stb
427
428        # no-op if our underlying widget is dead or on its way out.
429        if not self._root_widget or self._root_widget.transitioning_out:
430            return
431
432        assert bui.app.classic is not None
433        music = bui.app.classic.music
434
435        # Resets music back to normal.
436        music.set_music_play_mode(bui.app.classic.MusicPlayMode.REGULAR)
437        bui.containerwidget(edit=self._root_widget, transition='out_right')
438        bui.app.ui_v1.set_main_menu_window(
439            stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget(),
440            from_window=self._root_widget,
441        )
442
443    def _do_it(self) -> None:
444        from bauiv1lib.soundtrack import browser as stb
445
446        # no-op if our underlying widget is dead or on its way out.
447        if not self._root_widget or self._root_widget.transitioning_out:
448            return
449
450        assert bui.app.classic is not None
451        music = bui.app.classic.music
452        cfg = bui.app.config
453        new_name = cast(str, bui.textwidget(query=self._text_field))
454        if new_name != self._soundtrack_name and new_name in cfg['Soundtracks']:
455            bui.screenmessage(
456                bui.Lstr(resource=self._r + '.cantSaveAlreadyExistsText')
457            )
458            bui.getsound('error').play()
459            return
460        if not new_name:
461            bui.getsound('error').play()
462            return
463        if (
464            new_name
465            == bui.Lstr(
466                resource=self._r + '.defaultSoundtrackNameText'
467            ).evaluate()
468        ):
469            bui.screenmessage(
470                bui.Lstr(resource=self._r + '.cantOverwriteDefaultText')
471            )
472            bui.getsound('error').play()
473            return
474
475        # Make sure config exists.
476        if 'Soundtracks' not in cfg:
477            cfg['Soundtracks'] = {}
478
479        # If we had an old one, delete it.
480        if (
481            self._existing_soundtrack_name is not None
482            and self._existing_soundtrack_name in cfg['Soundtracks']
483        ):
484            del cfg['Soundtracks'][self._existing_soundtrack_name]
485        cfg['Soundtracks'][new_name] = self._soundtrack
486        cfg['Soundtrack'] = new_name
487
488        cfg.commit()
489        bui.getsound('gunCocking').play()
490        bui.containerwidget(edit=self._root_widget, transition='out_right')
491
492        # Resets music back to normal.
493        music.set_music_play_mode(
494            bui.app.classic.MusicPlayMode.REGULAR, force_restart=True
495        )
496
497        bui.app.ui_v1.set_main_menu_window(
498            stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget(),
499            from_window=self._root_widget,
500        )
501
502    def _do_it_with_sound(self) -> None:
503        bui.getsound('swish').play()
504        self._do_it()
class SoundtrackEditWindow(bauiv1._uitypes.Window):
 19class SoundtrackEditWindow(bui.Window):
 20    """Window for editing a soundtrack."""
 21
 22    def __init__(
 23        self,
 24        existing_soundtrack: str | dict[str, Any] | None,
 25        transition: str = 'in_right',
 26    ):
 27        # pylint: disable=too-many-statements
 28        appconfig = bui.app.config
 29        self._r = 'editSoundtrackWindow'
 30        self._folder_tex = bui.gettexture('folder')
 31        self._file_tex = bui.gettexture('file')
 32        assert bui.app.classic is not None
 33        uiscale = bui.app.ui_v1.uiscale
 34        self._width = 848 if uiscale is bui.UIScale.SMALL else 648
 35        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 36        self._height = (
 37            395
 38            if uiscale is bui.UIScale.SMALL
 39            else 450 if uiscale is bui.UIScale.MEDIUM else 560
 40        )
 41        super().__init__(
 42            root_widget=bui.containerwidget(
 43                size=(self._width, self._height),
 44                transition=transition,
 45                scale=(
 46                    2.08
 47                    if uiscale is bui.UIScale.SMALL
 48                    else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
 49                ),
 50                stack_offset=(
 51                    (0, -48)
 52                    if uiscale is bui.UIScale.SMALL
 53                    else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0)
 54                ),
 55            )
 56        )
 57        cancel_button = bui.buttonwidget(
 58            parent=self._root_widget,
 59            position=(38 + x_inset, self._height - 60),
 60            size=(160, 60),
 61            autoselect=True,
 62            label=bui.Lstr(resource='cancelText'),
 63            scale=0.8,
 64        )
 65        save_button = bui.buttonwidget(
 66            parent=self._root_widget,
 67            position=(self._width - (168 + x_inset), self._height - 60),
 68            autoselect=True,
 69            size=(160, 60),
 70            label=bui.Lstr(resource='saveText'),
 71            scale=0.8,
 72        )
 73        bui.widget(edit=save_button, left_widget=cancel_button)
 74        bui.widget(edit=cancel_button, right_widget=save_button)
 75        bui.textwidget(
 76            parent=self._root_widget,
 77            position=(0, self._height - 50),
 78            size=(self._width, 25),
 79            text=bui.Lstr(
 80                resource=self._r
 81                + (
 82                    '.editSoundtrackText'
 83                    if existing_soundtrack is not None
 84                    else '.newSoundtrackText'
 85                )
 86            ),
 87            color=bui.app.ui_v1.title_color,
 88            h_align='center',
 89            v_align='center',
 90            maxwidth=280,
 91        )
 92        v = self._height - 110
 93        if 'Soundtracks' not in appconfig:
 94            appconfig['Soundtracks'] = {}
 95
 96        self._soundtrack_name: str | None
 97        self._existing_soundtrack_name: str | None
 98        if existing_soundtrack is not None:
 99            # if they passed just a name, pull info from that soundtrack
100            if isinstance(existing_soundtrack, str):
101                self._soundtrack = copy.deepcopy(
102                    appconfig['Soundtracks'][existing_soundtrack]
103                )
104                self._soundtrack_name = existing_soundtrack
105                self._existing_soundtrack_name = existing_soundtrack
106                self._last_edited_song_type = None
107            else:
108                # otherwise they can pass info on an in-progress edit
109                self._soundtrack = existing_soundtrack['soundtrack']
110                self._soundtrack_name = existing_soundtrack['name']
111                self._existing_soundtrack_name = existing_soundtrack[
112                    'existing_name'
113                ]
114                self._last_edited_song_type = existing_soundtrack[
115                    'last_edited_song_type'
116                ]
117        else:
118            self._soundtrack_name = None
119            self._existing_soundtrack_name = None
120            self._soundtrack = {}
121            self._last_edited_song_type = None
122
123        bui.textwidget(
124            parent=self._root_widget,
125            text=bui.Lstr(resource=self._r + '.nameText'),
126            maxwidth=80,
127            scale=0.8,
128            position=(105 + x_inset, v + 19),
129            color=(0.8, 0.8, 0.8, 0.5),
130            size=(0, 0),
131            h_align='right',
132            v_align='center',
133        )
134
135        # if there's no initial value, find a good initial unused name
136        if existing_soundtrack is None:
137            i = 1
138            st_name_text = bui.Lstr(
139                resource=self._r + '.newSoundtrackNameText'
140            ).evaluate()
141            if '${COUNT}' not in st_name_text:
142                # make sure we insert number *somewhere*
143                st_name_text = st_name_text + ' ${COUNT}'
144            while True:
145                self._soundtrack_name = st_name_text.replace('${COUNT}', str(i))
146                if self._soundtrack_name not in appconfig['Soundtracks']:
147                    break
148                i += 1
149
150        self._text_field = bui.textwidget(
151            parent=self._root_widget,
152            position=(120 + x_inset, v - 5),
153            size=(self._width - (160 + 2 * x_inset), 43),
154            text=self._soundtrack_name,
155            h_align='left',
156            v_align='center',
157            max_chars=32,
158            autoselect=True,
159            description=bui.Lstr(resource=self._r + '.nameText'),
160            editable=True,
161            padding=4,
162            on_return_press_call=self._do_it_with_sound,
163        )
164
165        scroll_height = self._height - 180
166        self._scrollwidget = scrollwidget = bui.scrollwidget(
167            parent=self._root_widget,
168            highlight=False,
169            position=(40 + x_inset, v - (scroll_height + 10)),
170            size=(self._width - (80 + 2 * x_inset), scroll_height),
171            simple_culling_v=10,
172            claims_left_right=True,
173            claims_tab=True,
174            selection_loops_to_parent=True,
175        )
176        bui.widget(edit=self._text_field, down_widget=self._scrollwidget)
177        self._col = bui.columnwidget(
178            parent=scrollwidget,
179            claims_left_right=True,
180            claims_tab=True,
181            selection_loops_to_parent=True,
182        )
183
184        self._song_type_buttons: dict[str, bui.Widget] = {}
185        self._refresh()
186        bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
187        bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button)
188        bui.buttonwidget(edit=save_button, on_activate_call=self._do_it)
189        bui.containerwidget(edit=self._root_widget, start_button=save_button)
190        bui.widget(edit=self._text_field, up_widget=cancel_button)
191        bui.widget(edit=cancel_button, down_widget=self._text_field)
192
193    def _refresh(self) -> None:
194        for widget in self._col.get_children():
195            widget.delete()
196
197        types = [
198            'Menu',
199            'CharSelect',
200            'ToTheDeath',
201            'Onslaught',
202            'Keep Away',
203            'Race',
204            'Epic Race',
205            'ForwardMarch',
206            'FlagCatcher',
207            'Survival',
208            'Epic',
209            'Hockey',
210            'Football',
211            'Flying',
212            'Scary',
213            'Marching',
214            'GrandRomp',
215            'Chosen One',
216            'Scores',
217            'Victory',
218        ]
219
220        # FIXME: We should probably convert this to use translations.
221        type_names_translated = bui.app.lang.get_resource('soundtrackTypeNames')
222        prev_type_button: bui.Widget | None = None
223        prev_test_button: bui.Widget | None = None
224
225        for index, song_type in enumerate(types):
226            row = bui.rowwidget(
227                parent=self._col,
228                size=(self._width - 40, 40),
229                claims_left_right=True,
230                claims_tab=True,
231                selection_loops_to_parent=True,
232            )
233            type_name = type_names_translated.get(song_type, song_type)
234            bui.textwidget(
235                parent=row,
236                size=(230, 25),
237                always_highlight=True,
238                text=type_name,
239                scale=0.7,
240                h_align='left',
241                v_align='center',
242                maxwidth=190,
243            )
244
245            if song_type in self._soundtrack:
246                entry = self._soundtrack[song_type]
247            else:
248                entry = None
249
250            if entry is not None:
251                # Make sure they don't muck with this after it gets to us.
252                entry = copy.deepcopy(entry)
253
254            icon_type = self._get_entry_button_display_icon_type(entry)
255            self._song_type_buttons[song_type] = btn = bui.buttonwidget(
256                parent=row,
257                size=(230, 32),
258                label=self._get_entry_button_display_name(entry),
259                text_scale=0.6,
260                on_activate_call=bui.Call(
261                    self._get_entry, song_type, entry, type_name
262                ),
263                icon=(
264                    self._file_tex
265                    if icon_type == 'file'
266                    else self._folder_tex if icon_type == 'folder' else None
267                ),
268                icon_color=(
269                    (1.1, 0.8, 0.2) if icon_type == 'folder' else (1, 1, 1)
270                ),
271                left_widget=self._text_field,
272                iconscale=0.7,
273                autoselect=True,
274                up_widget=prev_type_button,
275            )
276            if index == 0:
277                bui.widget(edit=btn, up_widget=self._text_field)
278            bui.widget(edit=btn, down_widget=btn)
279
280            if (
281                self._last_edited_song_type is not None
282                and song_type == self._last_edited_song_type
283            ):
284                bui.containerwidget(
285                    edit=row, selected_child=btn, visible_child=btn
286                )
287                bui.containerwidget(
288                    edit=self._col, selected_child=row, visible_child=row
289                )
290                bui.containerwidget(
291                    edit=self._scrollwidget,
292                    selected_child=self._col,
293                    visible_child=self._col,
294                )
295                bui.containerwidget(
296                    edit=self._root_widget,
297                    selected_child=self._scrollwidget,
298                    visible_child=self._scrollwidget,
299                )
300
301            if prev_type_button is not None:
302                bui.widget(edit=prev_type_button, down_widget=btn)
303            prev_type_button = btn
304            bui.textwidget(parent=row, size=(10, 32), text='')  # spacing
305            assert bui.app.classic is not None
306            btn = bui.buttonwidget(
307                parent=row,
308                size=(50, 32),
309                label=bui.Lstr(resource=self._r + '.testText'),
310                text_scale=0.6,
311                on_activate_call=bui.Call(self._test, bs.MusicType(song_type)),
312                up_widget=(
313                    prev_test_button
314                    if prev_test_button is not None
315                    else self._text_field
316                ),
317            )
318            if prev_test_button is not None:
319                bui.widget(edit=prev_test_button, down_widget=btn)
320            bui.widget(edit=btn, down_widget=btn, right_widget=btn)
321            prev_test_button = btn
322
323    @classmethod
324    def _restore_editor(
325        cls, state: dict[str, Any], musictype: str, entry: Any
326    ) -> None:
327        assert bui.app.classic is not None
328        music = bui.app.classic.music
329
330        # Apply the change and recreate the window.
331        soundtrack = state['soundtrack']
332        existing_entry = (
333            None if musictype not in soundtrack else soundtrack[musictype]
334        )
335        if existing_entry != entry:
336            bui.getsound('gunCocking').play()
337
338        # Make sure this doesn't get mucked with after we get it.
339        if entry is not None:
340            entry = copy.deepcopy(entry)
341
342        entry_type = music.get_soundtrack_entry_type(entry)
343        if entry_type == 'default':
344            # For 'default' entries simply exclude them from the list.
345            if musictype in soundtrack:
346                del soundtrack[musictype]
347        else:
348            soundtrack[musictype] = entry
349
350        bui.app.ui_v1.set_main_menu_window(
351            cls(state, transition='in_left').get_root_widget(),
352            from_window=False,  # Disable check here.
353        )
354
355    def _get_entry(
356        self, song_type: str, entry: Any, selection_target_name: str
357    ) -> None:
358        assert bui.app.classic is not None
359        music = bui.app.classic.music
360
361        # no-op if our underlying widget is dead or on its way out.
362        if not self._root_widget or self._root_widget.transitioning_out:
363            return
364
365        if selection_target_name != '':
366            selection_target_name = "'" + selection_target_name + "'"
367        state = {
368            'name': self._soundtrack_name,
369            'existing_name': self._existing_soundtrack_name,
370            'soundtrack': self._soundtrack,
371            'last_edited_song_type': song_type,
372        }
373        bui.containerwidget(edit=self._root_widget, transition='out_left')
374        bui.app.ui_v1.set_main_menu_window(
375            music.get_music_player()
376            .select_entry(
377                bui.Call(self._restore_editor, state, song_type),
378                entry,
379                selection_target_name,
380            )
381            .get_root_widget(),
382            from_window=self._root_widget,
383        )
384
385    def _test(self, song_type: bs.MusicType) -> None:
386        assert bui.app.classic is not None
387        music = bui.app.classic.music
388
389        # Warn if volume is zero.
390        if bui.app.config.resolve('Music Volume') < 0.01:
391            bui.getsound('error').play()
392            bui.screenmessage(
393                bui.Lstr(resource=self._r + '.musicVolumeZeroWarning'),
394                color=(1, 0.5, 0),
395            )
396        music.set_music_play_mode(bui.app.classic.MusicPlayMode.TEST)
397        music.do_play_music(
398            song_type,
399            mode=bui.app.classic.MusicPlayMode.TEST,
400            testsoundtrack=self._soundtrack,
401        )
402
403    def _get_entry_button_display_name(self, entry: Any) -> str | bui.Lstr:
404        assert bui.app.classic is not None
405        music = bui.app.classic.music
406        etype = music.get_soundtrack_entry_type(entry)
407        ename: str | bui.Lstr
408        if etype == 'default':
409            ename = bui.Lstr(resource=self._r + '.defaultGameMusicText')
410        elif etype in ('musicFile', 'musicFolder'):
411            ename = os.path.basename(music.get_soundtrack_entry_name(entry))
412        else:
413            ename = music.get_soundtrack_entry_name(entry)
414        return ename
415
416    def _get_entry_button_display_icon_type(self, entry: Any) -> str | None:
417        assert bui.app.classic is not None
418        music = bui.app.classic.music
419        etype = music.get_soundtrack_entry_type(entry)
420        if etype == 'musicFile':
421            return 'file'
422        if etype == 'musicFolder':
423            return 'folder'
424        return None
425
426    def _cancel(self) -> None:
427        from bauiv1lib.soundtrack import browser as stb
428
429        # no-op if our underlying widget is dead or on its way out.
430        if not self._root_widget or self._root_widget.transitioning_out:
431            return
432
433        assert bui.app.classic is not None
434        music = bui.app.classic.music
435
436        # Resets music back to normal.
437        music.set_music_play_mode(bui.app.classic.MusicPlayMode.REGULAR)
438        bui.containerwidget(edit=self._root_widget, transition='out_right')
439        bui.app.ui_v1.set_main_menu_window(
440            stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget(),
441            from_window=self._root_widget,
442        )
443
444    def _do_it(self) -> None:
445        from bauiv1lib.soundtrack import browser as stb
446
447        # no-op if our underlying widget is dead or on its way out.
448        if not self._root_widget or self._root_widget.transitioning_out:
449            return
450
451        assert bui.app.classic is not None
452        music = bui.app.classic.music
453        cfg = bui.app.config
454        new_name = cast(str, bui.textwidget(query=self._text_field))
455        if new_name != self._soundtrack_name and new_name in cfg['Soundtracks']:
456            bui.screenmessage(
457                bui.Lstr(resource=self._r + '.cantSaveAlreadyExistsText')
458            )
459            bui.getsound('error').play()
460            return
461        if not new_name:
462            bui.getsound('error').play()
463            return
464        if (
465            new_name
466            == bui.Lstr(
467                resource=self._r + '.defaultSoundtrackNameText'
468            ).evaluate()
469        ):
470            bui.screenmessage(
471                bui.Lstr(resource=self._r + '.cantOverwriteDefaultText')
472            )
473            bui.getsound('error').play()
474            return
475
476        # Make sure config exists.
477        if 'Soundtracks' not in cfg:
478            cfg['Soundtracks'] = {}
479
480        # If we had an old one, delete it.
481        if (
482            self._existing_soundtrack_name is not None
483            and self._existing_soundtrack_name in cfg['Soundtracks']
484        ):
485            del cfg['Soundtracks'][self._existing_soundtrack_name]
486        cfg['Soundtracks'][new_name] = self._soundtrack
487        cfg['Soundtrack'] = new_name
488
489        cfg.commit()
490        bui.getsound('gunCocking').play()
491        bui.containerwidget(edit=self._root_widget, transition='out_right')
492
493        # Resets music back to normal.
494        music.set_music_play_mode(
495            bui.app.classic.MusicPlayMode.REGULAR, force_restart=True
496        )
497
498        bui.app.ui_v1.set_main_menu_window(
499            stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget(),
500            from_window=self._root_widget,
501        )
502
503    def _do_it_with_sound(self) -> None:
504        bui.getsound('swish').play()
505        self._do_it()

Window for editing a soundtrack.

SoundtrackEditWindow( existing_soundtrack: str | dict[str, typing.Any] | None, transition: str = 'in_right')
 22    def __init__(
 23        self,
 24        existing_soundtrack: str | dict[str, Any] | None,
 25        transition: str = 'in_right',
 26    ):
 27        # pylint: disable=too-many-statements
 28        appconfig = bui.app.config
 29        self._r = 'editSoundtrackWindow'
 30        self._folder_tex = bui.gettexture('folder')
 31        self._file_tex = bui.gettexture('file')
 32        assert bui.app.classic is not None
 33        uiscale = bui.app.ui_v1.uiscale
 34        self._width = 848 if uiscale is bui.UIScale.SMALL else 648
 35        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 36        self._height = (
 37            395
 38            if uiscale is bui.UIScale.SMALL
 39            else 450 if uiscale is bui.UIScale.MEDIUM else 560
 40        )
 41        super().__init__(
 42            root_widget=bui.containerwidget(
 43                size=(self._width, self._height),
 44                transition=transition,
 45                scale=(
 46                    2.08
 47                    if uiscale is bui.UIScale.SMALL
 48                    else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
 49                ),
 50                stack_offset=(
 51                    (0, -48)
 52                    if uiscale is bui.UIScale.SMALL
 53                    else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0)
 54                ),
 55            )
 56        )
 57        cancel_button = bui.buttonwidget(
 58            parent=self._root_widget,
 59            position=(38 + x_inset, self._height - 60),
 60            size=(160, 60),
 61            autoselect=True,
 62            label=bui.Lstr(resource='cancelText'),
 63            scale=0.8,
 64        )
 65        save_button = bui.buttonwidget(
 66            parent=self._root_widget,
 67            position=(self._width - (168 + x_inset), self._height - 60),
 68            autoselect=True,
 69            size=(160, 60),
 70            label=bui.Lstr(resource='saveText'),
 71            scale=0.8,
 72        )
 73        bui.widget(edit=save_button, left_widget=cancel_button)
 74        bui.widget(edit=cancel_button, right_widget=save_button)
 75        bui.textwidget(
 76            parent=self._root_widget,
 77            position=(0, self._height - 50),
 78            size=(self._width, 25),
 79            text=bui.Lstr(
 80                resource=self._r
 81                + (
 82                    '.editSoundtrackText'
 83                    if existing_soundtrack is not None
 84                    else '.newSoundtrackText'
 85                )
 86            ),
 87            color=bui.app.ui_v1.title_color,
 88            h_align='center',
 89            v_align='center',
 90            maxwidth=280,
 91        )
 92        v = self._height - 110
 93        if 'Soundtracks' not in appconfig:
 94            appconfig['Soundtracks'] = {}
 95
 96        self._soundtrack_name: str | None
 97        self._existing_soundtrack_name: str | None
 98        if existing_soundtrack is not None:
 99            # if they passed just a name, pull info from that soundtrack
100            if isinstance(existing_soundtrack, str):
101                self._soundtrack = copy.deepcopy(
102                    appconfig['Soundtracks'][existing_soundtrack]
103                )
104                self._soundtrack_name = existing_soundtrack
105                self._existing_soundtrack_name = existing_soundtrack
106                self._last_edited_song_type = None
107            else:
108                # otherwise they can pass info on an in-progress edit
109                self._soundtrack = existing_soundtrack['soundtrack']
110                self._soundtrack_name = existing_soundtrack['name']
111                self._existing_soundtrack_name = existing_soundtrack[
112                    'existing_name'
113                ]
114                self._last_edited_song_type = existing_soundtrack[
115                    'last_edited_song_type'
116                ]
117        else:
118            self._soundtrack_name = None
119            self._existing_soundtrack_name = None
120            self._soundtrack = {}
121            self._last_edited_song_type = None
122
123        bui.textwidget(
124            parent=self._root_widget,
125            text=bui.Lstr(resource=self._r + '.nameText'),
126            maxwidth=80,
127            scale=0.8,
128            position=(105 + x_inset, v + 19),
129            color=(0.8, 0.8, 0.8, 0.5),
130            size=(0, 0),
131            h_align='right',
132            v_align='center',
133        )
134
135        # if there's no initial value, find a good initial unused name
136        if existing_soundtrack is None:
137            i = 1
138            st_name_text = bui.Lstr(
139                resource=self._r + '.newSoundtrackNameText'
140            ).evaluate()
141            if '${COUNT}' not in st_name_text:
142                # make sure we insert number *somewhere*
143                st_name_text = st_name_text + ' ${COUNT}'
144            while True:
145                self._soundtrack_name = st_name_text.replace('${COUNT}', str(i))
146                if self._soundtrack_name not in appconfig['Soundtracks']:
147                    break
148                i += 1
149
150        self._text_field = bui.textwidget(
151            parent=self._root_widget,
152            position=(120 + x_inset, v - 5),
153            size=(self._width - (160 + 2 * x_inset), 43),
154            text=self._soundtrack_name,
155            h_align='left',
156            v_align='center',
157            max_chars=32,
158            autoselect=True,
159            description=bui.Lstr(resource=self._r + '.nameText'),
160            editable=True,
161            padding=4,
162            on_return_press_call=self._do_it_with_sound,
163        )
164
165        scroll_height = self._height - 180
166        self._scrollwidget = scrollwidget = bui.scrollwidget(
167            parent=self._root_widget,
168            highlight=False,
169            position=(40 + x_inset, v - (scroll_height + 10)),
170            size=(self._width - (80 + 2 * x_inset), scroll_height),
171            simple_culling_v=10,
172            claims_left_right=True,
173            claims_tab=True,
174            selection_loops_to_parent=True,
175        )
176        bui.widget(edit=self._text_field, down_widget=self._scrollwidget)
177        self._col = bui.columnwidget(
178            parent=scrollwidget,
179            claims_left_right=True,
180            claims_tab=True,
181            selection_loops_to_parent=True,
182        )
183
184        self._song_type_buttons: dict[str, bui.Widget] = {}
185        self._refresh()
186        bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
187        bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button)
188        bui.buttonwidget(edit=save_button, on_activate_call=self._do_it)
189        bui.containerwidget(edit=self._root_widget, start_button=save_button)
190        bui.widget(edit=self._text_field, up_widget=cancel_button)
191        bui.widget(edit=cancel_button, down_widget=self._text_field)
Inherited Members
bauiv1._uitypes.Window
get_root_widget