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

Window for editing a soundtrack.

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

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:
199    @override
200    def get_main_window_state(self) -> bui.MainWindowState:
201        # Support recreating our window for back/refresh purposes.
202        cls = type(self)
203
204        # Pull this out of self here; if we do it in the lambda we'll
205        # keep our window alive due to the 'self' reference.
206        existing_soundtrack = {
207            'name': self._soundtrack_name,
208            'existing_name': self._existing_soundtrack_name,
209            'soundtrack': self._soundtrack,
210            'last_edited_song_type': self._last_edited_song_type,
211        }
212
213        return bui.BasicMainWindowState(
214            create_call=lambda transition, origin_widget: cls(
215                transition=transition,
216                origin_widget=origin_widget,
217                existing_soundtrack=existing_soundtrack,
218            )
219        )

Return a WindowState to recreate this window, if supported.