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

Create a MainWindow given a root widget and transition info.

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

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
199    @override
200    def get_main_window_state(self) -> bui.MainWindowState:
201        # Support recreating our window for back/refresh purposes.
202        cls = type(self)
203
204        # Pull this out of self here; if we do it in the lambda we'll
205        # keep our window alive due to the 'self' reference.
206        existing_soundtrack = self._existing_soundtrack
207        return bui.BasicMainWindowState(
208            create_call=lambda transition, origin_widget: cls(
209                transition=transition,
210                origin_widget=origin_widget,
211                existing_soundtrack=existing_soundtrack,
212            )
213        )

Return a WindowState to recreate this window, if supported.

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