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

Window for editing a soundtrack.

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