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            claims_tab=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            claims_tab=True,
188            selection_loops_to_parent=True,
189        )
190
191        self._song_type_buttons: dict[str, bui.Widget] = {}
192        self._refresh()
193        bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
194        bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button)
195        bui.buttonwidget(edit=save_button, on_activate_call=self._do_it)
196        bui.containerwidget(edit=self._root_widget, start_button=save_button)
197        bui.widget(edit=self._text_field, up_widget=cancel_button)
198        bui.widget(edit=cancel_button, down_widget=self._text_field)
199
200    @override
201    def get_main_window_state(self) -> bui.MainWindowState:
202        # Support recreating our window for back/refresh purposes.
203        cls = type(self)
204
205        # Pull this out of self here; if we do it in the lambda we'll
206        # keep our window alive due to the 'self' reference.
207        existing_soundtrack = {
208            'name': self._soundtrack_name,
209            'existing_name': self._existing_soundtrack_name,
210            'soundtrack': self._soundtrack,
211            'last_edited_song_type': self._last_edited_song_type,
212        }
213
214        return bui.BasicMainWindowState(
215            create_call=lambda transition, origin_widget: cls(
216                transition=transition,
217                origin_widget=origin_widget,
218                existing_soundtrack=existing_soundtrack,
219            )
220        )
221
222    def _refresh(self) -> None:
223        for widget in self._col.get_children():
224            widget.delete()
225
226        types = [
227            'Menu',
228            'CharSelect',
229            'ToTheDeath',
230            'Onslaught',
231            'Keep Away',
232            'Race',
233            'Epic Race',
234            'ForwardMarch',
235            'FlagCatcher',
236            'Survival',
237            'Epic',
238            'Hockey',
239            'Football',
240            'Flying',
241            'Scary',
242            'Marching',
243            'GrandRomp',
244            'Chosen One',
245            'Scores',
246            'Victory',
247        ]
248
249        # FIXME: We should probably convert this to use translations.
250        type_names_translated = bui.app.lang.get_resource('soundtrackTypeNames')
251        prev_type_button: bui.Widget | None = None
252        prev_test_button: bui.Widget | None = None
253
254        for index, song_type in enumerate(types):
255            row = bui.rowwidget(
256                parent=self._col,
257                size=(self._width - 40, 40),
258                claims_left_right=True,
259                claims_tab=True,
260                selection_loops_to_parent=True,
261            )
262            type_name = type_names_translated.get(song_type, song_type)
263            bui.textwidget(
264                parent=row,
265                size=(230, 25),
266                always_highlight=True,
267                text=type_name,
268                scale=0.7,
269                h_align='left',
270                v_align='center',
271                maxwidth=190,
272            )
273
274            if song_type in self._soundtrack:
275                entry = self._soundtrack[song_type]
276            else:
277                entry = None
278
279            if entry is not None:
280                # Make sure they don't muck with this after it gets to us.
281                entry = copy.deepcopy(entry)
282
283            icon_type = self._get_entry_button_display_icon_type(entry)
284            self._song_type_buttons[song_type] = btn = bui.buttonwidget(
285                parent=row,
286                size=(230, 32),
287                label=self._get_entry_button_display_name(entry),
288                text_scale=0.6,
289                on_activate_call=bui.Call(
290                    self._get_entry, song_type, entry, type_name
291                ),
292                icon=(
293                    self._file_tex
294                    if icon_type == 'file'
295                    else self._folder_tex if icon_type == 'folder' else None
296                ),
297                icon_color=(
298                    (1.1, 0.8, 0.2) if icon_type == 'folder' else (1, 1, 1)
299                ),
300                left_widget=self._text_field,
301                iconscale=0.7,
302                autoselect=True,
303                up_widget=prev_type_button,
304            )
305            if index == 0:
306                bui.widget(edit=btn, up_widget=self._text_field)
307            bui.widget(edit=btn, down_widget=btn)
308
309            if (
310                self._last_edited_song_type is not None
311                and song_type == self._last_edited_song_type
312            ):
313                bui.containerwidget(
314                    edit=row, selected_child=btn, visible_child=btn
315                )
316                bui.containerwidget(
317                    edit=self._col, selected_child=row, visible_child=row
318                )
319                bui.containerwidget(
320                    edit=self._scrollwidget,
321                    selected_child=self._col,
322                    visible_child=self._col,
323                )
324                bui.containerwidget(
325                    edit=self._root_widget,
326                    selected_child=self._scrollwidget,
327                    visible_child=self._scrollwidget,
328                )
329
330            if prev_type_button is not None:
331                bui.widget(edit=prev_type_button, down_widget=btn)
332            prev_type_button = btn
333            bui.textwidget(parent=row, size=(10, 32), text='')  # spacing
334            assert bui.app.classic is not None
335            btn = bui.buttonwidget(
336                parent=row,
337                size=(50, 32),
338                label=bui.Lstr(resource=f'{self._r}.testText'),
339                text_scale=0.6,
340                on_activate_call=bui.Call(self._test, bs.MusicType(song_type)),
341                up_widget=(
342                    prev_test_button
343                    if prev_test_button is not None
344                    else self._text_field
345                ),
346            )
347            if prev_test_button is not None:
348                bui.widget(edit=prev_test_button, down_widget=btn)
349            bui.widget(edit=btn, down_widget=btn, right_widget=btn)
350            prev_test_button = btn
351
352    @classmethod
353    def _restore_editor(
354        cls, state: dict[str, Any], musictype: str, entry: Any
355    ) -> None:
356        assert bui.app.classic is not None
357        music = bui.app.classic.music
358
359        # Apply the change and recreate the window.
360        soundtrack = state['soundtrack']
361        existing_entry = (
362            None if musictype not in soundtrack else soundtrack[musictype]
363        )
364        if existing_entry != entry:
365            bui.getsound('gunCocking').play()
366
367        # Make sure this doesn't get mucked with after we get it.
368        if entry is not None:
369            entry = copy.deepcopy(entry)
370
371        entry_type = music.get_soundtrack_entry_type(entry)
372        if entry_type == 'default':
373            # For 'default' entries simply exclude them from the list.
374            if musictype in soundtrack:
375                del soundtrack[musictype]
376        else:
377            soundtrack[musictype] = entry
378
379        mainwindow = bui.app.ui_v1.get_main_window()
380        assert mainwindow is not None
381
382        mainwindow.main_window_back_state = state['back_state']
383        mainwindow.main_window_back()
384
385    def _get_entry(
386        self, song_type: str, entry: Any, selection_target_name: str
387    ) -> None:
388        assert bui.app.classic is not None
389        music = bui.app.classic.music
390
391        # no-op if we're not in control.
392        if not self.main_window_has_control():
393            return
394
395        if selection_target_name != '':
396            selection_target_name = "'" + selection_target_name + "'"
397        state = {
398            'name': self._soundtrack_name,
399            'existing_name': self._existing_soundtrack_name,
400            'soundtrack': self._soundtrack,
401            'last_edited_song_type': song_type,
402        }
403        new_win = music.get_music_player().select_entry(
404            bui.Call(self._restore_editor, state, song_type),
405            entry,
406            selection_target_name,
407        )
408        self.main_window_replace(new_win)
409
410        # Once we've set the new window, grab the back-state; we'll use
411        # that to jump back here after selection completes.
412        assert new_win.main_window_back_state is not None
413        state['back_state'] = new_win.main_window_back_state
414
415    def _test(self, song_type: bs.MusicType) -> None:
416        assert bui.app.classic is not None
417        music = bui.app.classic.music
418
419        # Warn if volume is zero.
420        if bui.app.config.resolve('Music Volume') < 0.01:
421            bui.getsound('error').play()
422            bui.screenmessage(
423                bui.Lstr(resource=f'{self._r}.musicVolumeZeroWarning'),
424                color=(1, 0.5, 0),
425            )
426        music.set_music_play_mode(bui.app.classic.MusicPlayMode.TEST)
427        music.do_play_music(
428            song_type,
429            mode=bui.app.classic.MusicPlayMode.TEST,
430            testsoundtrack=self._soundtrack,
431        )
432
433    def _get_entry_button_display_name(self, entry: Any) -> str | bui.Lstr:
434        assert bui.app.classic is not None
435        music = bui.app.classic.music
436        etype = music.get_soundtrack_entry_type(entry)
437        ename: str | bui.Lstr
438        if etype == 'default':
439            ename = bui.Lstr(resource=f'{self._r}.defaultGameMusicText')
440        elif etype in ('musicFile', 'musicFolder'):
441            ename = os.path.basename(music.get_soundtrack_entry_name(entry))
442        else:
443            ename = music.get_soundtrack_entry_name(entry)
444        return ename
445
446    def _get_entry_button_display_icon_type(self, entry: Any) -> str | None:
447        assert bui.app.classic is not None
448        music = bui.app.classic.music
449        etype = music.get_soundtrack_entry_type(entry)
450        if etype == 'musicFile':
451            return 'file'
452        if etype == 'musicFolder':
453            return 'folder'
454        return None
455
456    def _cancel(self) -> None:
457        # from bauiv1lib.soundtrack.browser import SoundtrackBrowserWindow
458
459        # no-op if our underlying widget is dead or on its way out.
460        if not self._root_widget or self._root_widget.transitioning_out:
461            return
462
463        assert bui.app.classic is not None
464        music = bui.app.classic.music
465
466        # Resets music back to normal.
467        music.set_music_play_mode(bui.app.classic.MusicPlayMode.REGULAR)
468
469        self.main_window_back()
470
471    def _do_it(self) -> None:
472
473        # no-op if our underlying widget is dead or on its way out.
474        if not self._root_widget or self._root_widget.transitioning_out:
475            return
476
477        assert bui.app.classic is not None
478        music = bui.app.classic.music
479        cfg = bui.app.config
480        new_name = cast(str, bui.textwidget(query=self._text_field))
481        if new_name != self._soundtrack_name and new_name in cfg['Soundtracks']:
482            bui.screenmessage(
483                bui.Lstr(resource=f'{self._r}.cantSaveAlreadyExistsText')
484            )
485            bui.getsound('error').play()
486            return
487        if not new_name:
488            bui.getsound('error').play()
489            return
490        if (
491            new_name
492            == bui.Lstr(
493                resource=f'{self._r}.defaultSoundtrackNameText'
494            ).evaluate()
495        ):
496            bui.screenmessage(
497                bui.Lstr(resource=f'{self._r}.cantOverwriteDefaultText')
498            )
499            bui.getsound('error').play()
500            return
501
502        # Make sure config exists.
503        if 'Soundtracks' not in cfg:
504            cfg['Soundtracks'] = {}
505
506        # If we had an old one, delete it.
507        if (
508            self._existing_soundtrack_name is not None
509            and self._existing_soundtrack_name in cfg['Soundtracks']
510        ):
511            del cfg['Soundtracks'][self._existing_soundtrack_name]
512        cfg['Soundtracks'][new_name] = self._soundtrack
513        cfg['Soundtrack'] = new_name
514
515        cfg.commit()
516        bui.getsound('gunCocking').play()
517
518        # Resets music back to normal.
519        music.set_music_play_mode(
520            bui.app.classic.MusicPlayMode.REGULAR, force_restart=True
521        )
522
523        self.main_window_back()
524
525    def _do_it_with_sound(self) -> None:
526        bui.getsound('swish').play()
527        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            claims_tab=True,
182            selection_loops_to_parent=True,
183        )
184        bui.widget(edit=self._text_field, down_widget=self._scrollwidget)
185        self._col = bui.columnwidget(
186            parent=scrollwidget,
187            claims_left_right=True,
188            claims_tab=True,
189            selection_loops_to_parent=True,
190        )
191
192        self._song_type_buttons: dict[str, bui.Widget] = {}
193        self._refresh()
194        bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
195        bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button)
196        bui.buttonwidget(edit=save_button, on_activate_call=self._do_it)
197        bui.containerwidget(edit=self._root_widget, start_button=save_button)
198        bui.widget(edit=self._text_field, up_widget=cancel_button)
199        bui.widget(edit=cancel_button, down_widget=self._text_field)
200
201    @override
202    def get_main_window_state(self) -> bui.MainWindowState:
203        # Support recreating our window for back/refresh purposes.
204        cls = type(self)
205
206        # Pull this out of self here; if we do it in the lambda we'll
207        # keep our window alive due to the 'self' reference.
208        existing_soundtrack = {
209            'name': self._soundtrack_name,
210            'existing_name': self._existing_soundtrack_name,
211            'soundtrack': self._soundtrack,
212            'last_edited_song_type': self._last_edited_song_type,
213        }
214
215        return bui.BasicMainWindowState(
216            create_call=lambda transition, origin_widget: cls(
217                transition=transition,
218                origin_widget=origin_widget,
219                existing_soundtrack=existing_soundtrack,
220            )
221        )
222
223    def _refresh(self) -> None:
224        for widget in self._col.get_children():
225            widget.delete()
226
227        types = [
228            'Menu',
229            'CharSelect',
230            'ToTheDeath',
231            'Onslaught',
232            'Keep Away',
233            'Race',
234            'Epic Race',
235            'ForwardMarch',
236            'FlagCatcher',
237            'Survival',
238            'Epic',
239            'Hockey',
240            'Football',
241            'Flying',
242            'Scary',
243            'Marching',
244            'GrandRomp',
245            'Chosen One',
246            'Scores',
247            'Victory',
248        ]
249
250        # FIXME: We should probably convert this to use translations.
251        type_names_translated = bui.app.lang.get_resource('soundtrackTypeNames')
252        prev_type_button: bui.Widget | None = None
253        prev_test_button: bui.Widget | None = None
254
255        for index, song_type in enumerate(types):
256            row = bui.rowwidget(
257                parent=self._col,
258                size=(self._width - 40, 40),
259                claims_left_right=True,
260                claims_tab=True,
261                selection_loops_to_parent=True,
262            )
263            type_name = type_names_translated.get(song_type, song_type)
264            bui.textwidget(
265                parent=row,
266                size=(230, 25),
267                always_highlight=True,
268                text=type_name,
269                scale=0.7,
270                h_align='left',
271                v_align='center',
272                maxwidth=190,
273            )
274
275            if song_type in self._soundtrack:
276                entry = self._soundtrack[song_type]
277            else:
278                entry = None
279
280            if entry is not None:
281                # Make sure they don't muck with this after it gets to us.
282                entry = copy.deepcopy(entry)
283
284            icon_type = self._get_entry_button_display_icon_type(entry)
285            self._song_type_buttons[song_type] = btn = bui.buttonwidget(
286                parent=row,
287                size=(230, 32),
288                label=self._get_entry_button_display_name(entry),
289                text_scale=0.6,
290                on_activate_call=bui.Call(
291                    self._get_entry, song_type, entry, type_name
292                ),
293                icon=(
294                    self._file_tex
295                    if icon_type == 'file'
296                    else self._folder_tex if icon_type == 'folder' else None
297                ),
298                icon_color=(
299                    (1.1, 0.8, 0.2) if icon_type == 'folder' else (1, 1, 1)
300                ),
301                left_widget=self._text_field,
302                iconscale=0.7,
303                autoselect=True,
304                up_widget=prev_type_button,
305            )
306            if index == 0:
307                bui.widget(edit=btn, up_widget=self._text_field)
308            bui.widget(edit=btn, down_widget=btn)
309
310            if (
311                self._last_edited_song_type is not None
312                and song_type == self._last_edited_song_type
313            ):
314                bui.containerwidget(
315                    edit=row, selected_child=btn, visible_child=btn
316                )
317                bui.containerwidget(
318                    edit=self._col, selected_child=row, visible_child=row
319                )
320                bui.containerwidget(
321                    edit=self._scrollwidget,
322                    selected_child=self._col,
323                    visible_child=self._col,
324                )
325                bui.containerwidget(
326                    edit=self._root_widget,
327                    selected_child=self._scrollwidget,
328                    visible_child=self._scrollwidget,
329                )
330
331            if prev_type_button is not None:
332                bui.widget(edit=prev_type_button, down_widget=btn)
333            prev_type_button = btn
334            bui.textwidget(parent=row, size=(10, 32), text='')  # spacing
335            assert bui.app.classic is not None
336            btn = bui.buttonwidget(
337                parent=row,
338                size=(50, 32),
339                label=bui.Lstr(resource=f'{self._r}.testText'),
340                text_scale=0.6,
341                on_activate_call=bui.Call(self._test, bs.MusicType(song_type)),
342                up_widget=(
343                    prev_test_button
344                    if prev_test_button is not None
345                    else self._text_field
346                ),
347            )
348            if prev_test_button is not None:
349                bui.widget(edit=prev_test_button, down_widget=btn)
350            bui.widget(edit=btn, down_widget=btn, right_widget=btn)
351            prev_test_button = btn
352
353    @classmethod
354    def _restore_editor(
355        cls, state: dict[str, Any], musictype: str, entry: Any
356    ) -> None:
357        assert bui.app.classic is not None
358        music = bui.app.classic.music
359
360        # Apply the change and recreate the window.
361        soundtrack = state['soundtrack']
362        existing_entry = (
363            None if musictype not in soundtrack else soundtrack[musictype]
364        )
365        if existing_entry != entry:
366            bui.getsound('gunCocking').play()
367
368        # Make sure this doesn't get mucked with after we get it.
369        if entry is not None:
370            entry = copy.deepcopy(entry)
371
372        entry_type = music.get_soundtrack_entry_type(entry)
373        if entry_type == 'default':
374            # For 'default' entries simply exclude them from the list.
375            if musictype in soundtrack:
376                del soundtrack[musictype]
377        else:
378            soundtrack[musictype] = entry
379
380        mainwindow = bui.app.ui_v1.get_main_window()
381        assert mainwindow is not None
382
383        mainwindow.main_window_back_state = state['back_state']
384        mainwindow.main_window_back()
385
386    def _get_entry(
387        self, song_type: str, entry: Any, selection_target_name: str
388    ) -> None:
389        assert bui.app.classic is not None
390        music = bui.app.classic.music
391
392        # no-op if we're not in control.
393        if not self.main_window_has_control():
394            return
395
396        if selection_target_name != '':
397            selection_target_name = "'" + selection_target_name + "'"
398        state = {
399            'name': self._soundtrack_name,
400            'existing_name': self._existing_soundtrack_name,
401            'soundtrack': self._soundtrack,
402            'last_edited_song_type': song_type,
403        }
404        new_win = music.get_music_player().select_entry(
405            bui.Call(self._restore_editor, state, song_type),
406            entry,
407            selection_target_name,
408        )
409        self.main_window_replace(new_win)
410
411        # Once we've set the new window, grab the back-state; we'll use
412        # that to jump back here after selection completes.
413        assert new_win.main_window_back_state is not None
414        state['back_state'] = new_win.main_window_back_state
415
416    def _test(self, song_type: bs.MusicType) -> None:
417        assert bui.app.classic is not None
418        music = bui.app.classic.music
419
420        # Warn if volume is zero.
421        if bui.app.config.resolve('Music Volume') < 0.01:
422            bui.getsound('error').play()
423            bui.screenmessage(
424                bui.Lstr(resource=f'{self._r}.musicVolumeZeroWarning'),
425                color=(1, 0.5, 0),
426            )
427        music.set_music_play_mode(bui.app.classic.MusicPlayMode.TEST)
428        music.do_play_music(
429            song_type,
430            mode=bui.app.classic.MusicPlayMode.TEST,
431            testsoundtrack=self._soundtrack,
432        )
433
434    def _get_entry_button_display_name(self, entry: Any) -> str | bui.Lstr:
435        assert bui.app.classic is not None
436        music = bui.app.classic.music
437        etype = music.get_soundtrack_entry_type(entry)
438        ename: str | bui.Lstr
439        if etype == 'default':
440            ename = bui.Lstr(resource=f'{self._r}.defaultGameMusicText')
441        elif etype in ('musicFile', 'musicFolder'):
442            ename = os.path.basename(music.get_soundtrack_entry_name(entry))
443        else:
444            ename = music.get_soundtrack_entry_name(entry)
445        return ename
446
447    def _get_entry_button_display_icon_type(self, entry: Any) -> str | None:
448        assert bui.app.classic is not None
449        music = bui.app.classic.music
450        etype = music.get_soundtrack_entry_type(entry)
451        if etype == 'musicFile':
452            return 'file'
453        if etype == 'musicFolder':
454            return 'folder'
455        return None
456
457    def _cancel(self) -> None:
458        # from bauiv1lib.soundtrack.browser import SoundtrackBrowserWindow
459
460        # no-op if our underlying widget is dead or on its way out.
461        if not self._root_widget or self._root_widget.transitioning_out:
462            return
463
464        assert bui.app.classic is not None
465        music = bui.app.classic.music
466
467        # Resets music back to normal.
468        music.set_music_play_mode(bui.app.classic.MusicPlayMode.REGULAR)
469
470        self.main_window_back()
471
472    def _do_it(self) -> None:
473
474        # no-op if our underlying widget is dead or on its way out.
475        if not self._root_widget or self._root_widget.transitioning_out:
476            return
477
478        assert bui.app.classic is not None
479        music = bui.app.classic.music
480        cfg = bui.app.config
481        new_name = cast(str, bui.textwidget(query=self._text_field))
482        if new_name != self._soundtrack_name and new_name in cfg['Soundtracks']:
483            bui.screenmessage(
484                bui.Lstr(resource=f'{self._r}.cantSaveAlreadyExistsText')
485            )
486            bui.getsound('error').play()
487            return
488        if not new_name:
489            bui.getsound('error').play()
490            return
491        if (
492            new_name
493            == bui.Lstr(
494                resource=f'{self._r}.defaultSoundtrackNameText'
495            ).evaluate()
496        ):
497            bui.screenmessage(
498                bui.Lstr(resource=f'{self._r}.cantOverwriteDefaultText')
499            )
500            bui.getsound('error').play()
501            return
502
503        # Make sure config exists.
504        if 'Soundtracks' not in cfg:
505            cfg['Soundtracks'] = {}
506
507        # If we had an old one, delete it.
508        if (
509            self._existing_soundtrack_name is not None
510            and self._existing_soundtrack_name in cfg['Soundtracks']
511        ):
512            del cfg['Soundtracks'][self._existing_soundtrack_name]
513        cfg['Soundtracks'][new_name] = self._soundtrack
514        cfg['Soundtrack'] = new_name
515
516        cfg.commit()
517        bui.getsound('gunCocking').play()
518
519        # Resets music back to normal.
520        music.set_music_play_mode(
521            bui.app.classic.MusicPlayMode.REGULAR, force_restart=True
522        )
523
524        self.main_window_back()
525
526    def _do_it_with_sound(self) -> None:
527        bui.getsound('swish').play()
528        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            claims_tab=True,
182            selection_loops_to_parent=True,
183        )
184        bui.widget(edit=self._text_field, down_widget=self._scrollwidget)
185        self._col = bui.columnwidget(
186            parent=scrollwidget,
187            claims_left_right=True,
188            claims_tab=True,
189            selection_loops_to_parent=True,
190        )
191
192        self._song_type_buttons: dict[str, bui.Widget] = {}
193        self._refresh()
194        bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
195        bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button)
196        bui.buttonwidget(edit=save_button, on_activate_call=self._do_it)
197        bui.containerwidget(edit=self._root_widget, start_button=save_button)
198        bui.widget(edit=self._text_field, up_widget=cancel_button)
199        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:
201    @override
202    def get_main_window_state(self) -> bui.MainWindowState:
203        # Support recreating our window for back/refresh purposes.
204        cls = type(self)
205
206        # Pull this out of self here; if we do it in the lambda we'll
207        # keep our window alive due to the 'self' reference.
208        existing_soundtrack = {
209            'name': self._soundtrack_name,
210            'existing_name': self._existing_soundtrack_name,
211            'soundtrack': self._soundtrack,
212            'last_edited_song_type': self._last_edited_song_type,
213        }
214
215        return bui.BasicMainWindowState(
216            create_call=lambda transition, origin_widget: cls(
217                transition=transition,
218                origin_widget=origin_widget,
219                existing_soundtrack=existing_soundtrack,
220            )
221        )

Return a WindowState to recreate this window, if supported.