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