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