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