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