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

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            395
 40            if uiscale is bui.UIScale.SMALL
 41            else 450 if uiscale is bui.UIScale.MEDIUM else 560
 42        )
 43        super().__init__(
 44            root_widget=bui.containerwidget(
 45                size=(self._width, self._height),
 46                scale=(
 47                    1.8
 48                    if uiscale is bui.UIScale.SMALL
 49                    else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
 50                ),
 51                stack_offset=(
 52                    (0, -37)
 53                    if uiscale is bui.UIScale.SMALL
 54                    else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0)
 55                ),
 56            ),
 57            transition=transition,
 58            origin_widget=origin_widget,
 59        )
 60        cancel_button = bui.buttonwidget(
 61            parent=self._root_widget,
 62            position=(38 + x_inset, self._height - 60),
 63            size=(160, 60),
 64            autoselect=True,
 65            label=bui.Lstr(resource='cancelText'),
 66            scale=0.8,
 67        )
 68        save_button = bui.buttonwidget(
 69            parent=self._root_widget,
 70            position=(self._width - (168 + x_inset), self._height - 60),
 71            autoselect=True,
 72            size=(160, 60),
 73            label=bui.Lstr(resource='saveText'),
 74            scale=0.8,
 75        )
 76        bui.widget(edit=save_button, left_widget=cancel_button)
 77        bui.widget(edit=cancel_button, right_widget=save_button)
 78        bui.textwidget(
 79            parent=self._root_widget,
 80            position=(0, self._height - 50),
 81            size=(self._width, 25),
 82            text=bui.Lstr(
 83                resource=self._r
 84                + (
 85                    '.editSoundtrackText'
 86                    if existing_soundtrack is not None
 87                    else '.newSoundtrackText'
 88                )
 89            ),
 90            color=bui.app.ui_v1.title_color,
 91            h_align='center',
 92            v_align='center',
 93            maxwidth=280,
 94        )
 95        v = self._height - 110
 96        if 'Soundtracks' not in appconfig:
 97            appconfig['Soundtracks'] = {}
 98
 99        self._soundtrack_name: str | None
100        self._existing_soundtrack = existing_soundtrack
101        self._existing_soundtrack_name: str | None
102        if existing_soundtrack is not None:
103            # if they passed just a name, pull info from that soundtrack
104            if isinstance(existing_soundtrack, str):
105                self._soundtrack = copy.deepcopy(
106                    appconfig['Soundtracks'][existing_soundtrack]
107                )
108                self._soundtrack_name = existing_soundtrack
109                self._existing_soundtrack_name = existing_soundtrack
110                self._last_edited_song_type = None
111            else:
112                # Otherwise they can pass info on an in-progress edit.
113                self._soundtrack = existing_soundtrack['soundtrack']
114                self._soundtrack_name = existing_soundtrack['name']
115                self._existing_soundtrack_name = existing_soundtrack[
116                    'existing_name'
117                ]
118                self._last_edited_song_type = existing_soundtrack[
119                    'last_edited_song_type'
120                ]
121        else:
122            self._soundtrack_name = None
123            self._existing_soundtrack_name = None
124            self._soundtrack = {}
125            self._last_edited_song_type = None
126
127        bui.textwidget(
128            parent=self._root_widget,
129            text=bui.Lstr(resource=f'{self._r}.nameText'),
130            maxwidth=80,
131            scale=0.8,
132            position=(105 + x_inset, v + 19),
133            color=(0.8, 0.8, 0.8, 0.5),
134            size=(0, 0),
135            h_align='right',
136            v_align='center',
137        )
138
139        # if there's no initial value, find a good initial unused name
140        if existing_soundtrack is None:
141            i = 1
142            st_name_text = bui.Lstr(
143                resource=f'{self._r}.newSoundtrackNameText'
144            ).evaluate()
145            if '${COUNT}' not in st_name_text:
146                # make sure we insert number *somewhere*
147                st_name_text = st_name_text + ' ${COUNT}'
148            while True:
149                self._soundtrack_name = st_name_text.replace('${COUNT}', str(i))
150                if self._soundtrack_name not in appconfig['Soundtracks']:
151                    break
152                i += 1
153
154        self._text_field = bui.textwidget(
155            parent=self._root_widget,
156            position=(120 + x_inset, v - 5),
157            size=(self._width - (160 + 2 * x_inset), 43),
158            text=self._soundtrack_name,
159            h_align='left',
160            v_align='center',
161            max_chars=32,
162            autoselect=True,
163            description=bui.Lstr(resource=f'{self._r}.nameText'),
164            editable=True,
165            padding=4,
166            on_return_press_call=self._do_it_with_sound,
167        )
168
169        scroll_height = self._height - 180
170        self._scrollwidget = scrollwidget = bui.scrollwidget(
171            parent=self._root_widget,
172            highlight=False,
173            position=(40 + x_inset, v - (scroll_height + 10)),
174            size=(self._width - (80 + 2 * x_inset), scroll_height),
175            simple_culling_v=10,
176            claims_left_right=True,
177            claims_tab=True,
178            selection_loops_to_parent=True,
179        )
180        bui.widget(edit=self._text_field, down_widget=self._scrollwidget)
181        self._col = bui.columnwidget(
182            parent=scrollwidget,
183            claims_left_right=True,
184            claims_tab=True,
185            selection_loops_to_parent=True,
186        )
187
188        self._song_type_buttons: dict[str, bui.Widget] = {}
189        self._refresh()
190        bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
191        bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button)
192        bui.buttonwidget(edit=save_button, on_activate_call=self._do_it)
193        bui.containerwidget(edit=self._root_widget, start_button=save_button)
194        bui.widget(edit=self._text_field, up_widget=cancel_button)
195        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:
197    @override
198    def get_main_window_state(self) -> bui.MainWindowState:
199        # Support recreating our window for back/refresh purposes.
200        cls = type(self)
201
202        # Pull this out of self here; if we do it in the lambda we'll
203        # keep our window alive due to the 'self' reference.
204        existing_soundtrack = {
205            'name': self._soundtrack_name,
206            'existing_name': self._existing_soundtrack_name,
207            'soundtrack': self._soundtrack,
208            'last_edited_song_type': self._last_edited_song_type,
209        }
210
211        return bui.BasicMainWindowState(
212            create_call=lambda transition, origin_widget: cls(
213                transition=transition,
214                origin_widget=origin_widget,
215                existing_soundtrack=existing_soundtrack,
216            )
217        )

Return a WindowState to recreate this window, if supported.

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