bastd.ui.soundtrack.edit

Provides UI for editing a soundtrack.

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

Window for editing a soundtrack.

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