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