bauiv1lib.soundtrack.browser
Provides UI for browsing soundtracks.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides UI for browsing soundtracks.""" 4 5from __future__ import annotations 6 7import copy 8import logging 9from typing import TYPE_CHECKING 10 11import bauiv1 as bui 12 13if TYPE_CHECKING: 14 from typing import Any 15 16 17class SoundtrackBrowserWindow(bui.Window): 18 """Window for browsing soundtracks.""" 19 20 def __init__( 21 self, 22 transition: str = 'in_right', 23 origin_widget: bui.Widget | None = None, 24 ): 25 # pylint: disable=too-many-locals 26 # pylint: disable=too-many-statements 27 28 # If they provided an origin-widget, scale up from that. 29 scale_origin: tuple[float, float] | None 30 if origin_widget is not None: 31 self._transition_out = 'out_scale' 32 scale_origin = origin_widget.get_screen_space_center() 33 transition = 'in_scale' 34 else: 35 self._transition_out = 'out_right' 36 scale_origin = None 37 38 self._r = 'editSoundtrackWindow' 39 assert bui.app.classic is not None 40 uiscale = bui.app.ui_v1.uiscale 41 self._width = 800 if uiscale is bui.UIScale.SMALL else 600 42 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 43 self._height = ( 44 340 45 if uiscale is bui.UIScale.SMALL 46 else 370 47 if uiscale is bui.UIScale.MEDIUM 48 else 440 49 ) 50 spacing = 40.0 51 v = self._height - 40.0 52 v -= spacing * 1.0 53 54 super().__init__( 55 root_widget=bui.containerwidget( 56 size=(self._width, self._height), 57 transition=transition, 58 toolbar_visibility='menu_minimal', 59 scale_origin_stack_offset=scale_origin, 60 scale=( 61 2.3 62 if uiscale is bui.UIScale.SMALL 63 else 1.6 64 if uiscale is bui.UIScale.MEDIUM 65 else 1.0 66 ), 67 stack_offset=(0, -18) 68 if uiscale is bui.UIScale.SMALL 69 else (0, 0), 70 ) 71 ) 72 73 assert bui.app.classic is not None 74 if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: 75 self._back_button = None 76 else: 77 self._back_button = bui.buttonwidget( 78 parent=self._root_widget, 79 position=(45 + x_inset, self._height - 60), 80 size=(120, 60), 81 scale=0.8, 82 label=bui.Lstr(resource='backText'), 83 button_type='back', 84 autoselect=True, 85 ) 86 bui.buttonwidget( 87 edit=self._back_button, 88 button_type='backSmall', 89 size=(60, 60), 90 label=bui.charstr(bui.SpecialChar.BACK), 91 ) 92 bui.textwidget( 93 parent=self._root_widget, 94 position=(self._width * 0.5, self._height - 35), 95 size=(0, 0), 96 maxwidth=300, 97 text=bui.Lstr(resource=self._r + '.titleText'), 98 color=bui.app.ui_v1.title_color, 99 h_align='center', 100 v_align='center', 101 ) 102 103 h = 43 + x_inset 104 v = self._height - 60 105 b_color = (0.6, 0.53, 0.63) 106 b_textcolor = (0.75, 0.7, 0.8) 107 lock_tex = bui.gettexture('lock') 108 self._lock_images: list[bui.Widget] = [] 109 110 scl = ( 111 1.0 112 if uiscale is bui.UIScale.SMALL 113 else 1.13 114 if uiscale is bui.UIScale.MEDIUM 115 else 1.4 116 ) 117 v -= 60.0 * scl 118 self._new_button = btn = bui.buttonwidget( 119 parent=self._root_widget, 120 position=(h, v), 121 size=(100, 55.0 * scl), 122 on_activate_call=self._new_soundtrack, 123 color=b_color, 124 button_type='square', 125 autoselect=True, 126 textcolor=b_textcolor, 127 text_scale=0.7, 128 label=bui.Lstr(resource=self._r + '.newText'), 129 ) 130 self._lock_images.append( 131 bui.imagewidget( 132 parent=self._root_widget, 133 size=(30, 30), 134 draw_controller=btn, 135 position=(h - 10, v + 55.0 * scl - 28), 136 texture=lock_tex, 137 ) 138 ) 139 140 if self._back_button is None: 141 bui.widget( 142 edit=btn, 143 left_widget=bui.get_special_widget('back_button'), 144 ) 145 v -= 60.0 * scl 146 147 self._edit_button = btn = bui.buttonwidget( 148 parent=self._root_widget, 149 position=(h, v), 150 size=(100, 55.0 * scl), 151 on_activate_call=self._edit_soundtrack, 152 color=b_color, 153 button_type='square', 154 autoselect=True, 155 textcolor=b_textcolor, 156 text_scale=0.7, 157 label=bui.Lstr(resource=self._r + '.editText'), 158 ) 159 self._lock_images.append( 160 bui.imagewidget( 161 parent=self._root_widget, 162 size=(30, 30), 163 draw_controller=btn, 164 position=(h - 10, v + 55.0 * scl - 28), 165 texture=lock_tex, 166 ) 167 ) 168 if self._back_button is None: 169 bui.widget( 170 edit=btn, 171 left_widget=bui.get_special_widget('back_button'), 172 ) 173 v -= 60.0 * scl 174 175 self._duplicate_button = btn = bui.buttonwidget( 176 parent=self._root_widget, 177 position=(h, v), 178 size=(100, 55.0 * scl), 179 on_activate_call=self._duplicate_soundtrack, 180 button_type='square', 181 autoselect=True, 182 color=b_color, 183 textcolor=b_textcolor, 184 text_scale=0.7, 185 label=bui.Lstr(resource=self._r + '.duplicateText'), 186 ) 187 self._lock_images.append( 188 bui.imagewidget( 189 parent=self._root_widget, 190 size=(30, 30), 191 draw_controller=btn, 192 position=(h - 10, v + 55.0 * scl - 28), 193 texture=lock_tex, 194 ) 195 ) 196 if self._back_button is None: 197 bui.widget( 198 edit=btn, 199 left_widget=bui.get_special_widget('back_button'), 200 ) 201 v -= 60.0 * scl 202 203 self._delete_button = btn = bui.buttonwidget( 204 parent=self._root_widget, 205 position=(h, v), 206 size=(100, 55.0 * scl), 207 on_activate_call=self._delete_soundtrack, 208 color=b_color, 209 button_type='square', 210 autoselect=True, 211 textcolor=b_textcolor, 212 text_scale=0.7, 213 label=bui.Lstr(resource=self._r + '.deleteText'), 214 ) 215 self._lock_images.append( 216 bui.imagewidget( 217 parent=self._root_widget, 218 size=(30, 30), 219 draw_controller=btn, 220 position=(h - 10, v + 55.0 * scl - 28), 221 texture=lock_tex, 222 ) 223 ) 224 if self._back_button is None: 225 bui.widget( 226 edit=btn, 227 left_widget=bui.get_special_widget('back_button'), 228 ) 229 230 # Keep our lock images up to date/etc. 231 self._update_timer = bui.AppTimer( 232 1.0, bui.WeakCall(self._update), repeat=True 233 ) 234 self._update() 235 236 v = self._height - 65 237 scroll_height = self._height - 105 238 v -= scroll_height 239 self._scrollwidget = scrollwidget = bui.scrollwidget( 240 parent=self._root_widget, 241 position=(152 + x_inset, v), 242 highlight=False, 243 size=(self._width - (205 + 2 * x_inset), scroll_height), 244 ) 245 bui.widget( 246 edit=self._scrollwidget, 247 left_widget=self._new_button, 248 right_widget=bui.get_special_widget('party_button') 249 if bui.app.ui_v1.use_toolbars 250 else self._scrollwidget, 251 ) 252 self._col = bui.columnwidget(parent=scrollwidget, border=2, margin=0) 253 254 self._soundtracks: dict[str, Any] | None = None 255 self._selected_soundtrack: str | None = None 256 self._selected_soundtrack_index: int | None = None 257 self._soundtrack_widgets: list[bui.Widget] = [] 258 self._allow_changing_soundtracks = False 259 self._refresh() 260 if self._back_button is not None: 261 bui.buttonwidget( 262 edit=self._back_button, on_activate_call=self._back 263 ) 264 bui.containerwidget( 265 edit=self._root_widget, cancel_button=self._back_button 266 ) 267 else: 268 bui.containerwidget( 269 edit=self._root_widget, on_cancel_call=self._back 270 ) 271 272 def _update(self) -> None: 273 have = ( 274 bui.app.classic is None 275 or bui.app.classic.accounts.have_pro_options() 276 ) 277 for lock in self._lock_images: 278 bui.imagewidget(edit=lock, opacity=0.0 if have else 1.0) 279 280 def _do_delete_soundtrack(self) -> None: 281 cfg = bui.app.config 282 soundtracks = cfg.setdefault('Soundtracks', {}) 283 if self._selected_soundtrack in soundtracks: 284 del soundtracks[self._selected_soundtrack] 285 cfg.commit() 286 bui.getsound('shieldDown').play() 287 assert self._selected_soundtrack_index is not None 288 assert self._soundtracks is not None 289 if self._selected_soundtrack_index >= len(self._soundtracks): 290 self._selected_soundtrack_index = len(self._soundtracks) 291 self._refresh() 292 293 def _delete_soundtrack(self) -> None: 294 # pylint: disable=cyclic-import 295 from bauiv1lib.purchase import PurchaseWindow 296 from bauiv1lib.confirm import ConfirmWindow 297 298 if ( 299 bui.app.classic is not None 300 and not bui.app.classic.accounts.have_pro_options() 301 ): 302 PurchaseWindow(items=['pro']) 303 return 304 if self._selected_soundtrack is None: 305 return 306 if self._selected_soundtrack == '__default__': 307 bui.getsound('error').play() 308 bui.screenmessage( 309 bui.Lstr(resource=self._r + '.cantDeleteDefaultText'), 310 color=(1, 0, 0), 311 ) 312 else: 313 ConfirmWindow( 314 bui.Lstr( 315 resource=self._r + '.deleteConfirmText', 316 subs=[('${NAME}', self._selected_soundtrack)], 317 ), 318 self._do_delete_soundtrack, 319 450, 320 150, 321 ) 322 323 def _duplicate_soundtrack(self) -> None: 324 # pylint: disable=cyclic-import 325 from bauiv1lib.purchase import PurchaseWindow 326 327 if ( 328 bui.app.classic is not None 329 and not bui.app.classic.accounts.have_pro_options() 330 ): 331 PurchaseWindow(items=['pro']) 332 return 333 cfg = bui.app.config 334 cfg.setdefault('Soundtracks', {}) 335 336 if self._selected_soundtrack is None: 337 return 338 sdtk: dict[str, Any] 339 if self._selected_soundtrack == '__default__': 340 sdtk = {} 341 else: 342 sdtk = cfg['Soundtracks'][self._selected_soundtrack] 343 344 # Find a valid dup name that doesn't exist. 345 test_index = 1 346 copy_text = bui.Lstr(resource='copyOfText').evaluate() 347 # Get just 'Copy' or whatnot. 348 copy_word = copy_text.replace('${NAME}', '').strip() 349 base_name = self._get_soundtrack_display_name( 350 self._selected_soundtrack 351 ).evaluate() 352 assert isinstance(base_name, str) 353 354 # If it looks like a copy, strip digits and spaces off the end. 355 if copy_word in base_name: 356 while base_name[-1].isdigit() or base_name[-1] == ' ': 357 base_name = base_name[:-1] 358 while True: 359 if copy_word in base_name: 360 test_name = base_name 361 else: 362 test_name = copy_text.replace('${NAME}', base_name) 363 if test_index > 1: 364 test_name += ' ' + str(test_index) 365 if test_name not in cfg['Soundtracks']: 366 break 367 test_index += 1 368 369 cfg['Soundtracks'][test_name] = copy.deepcopy(sdtk) 370 cfg.commit() 371 self._refresh(select_soundtrack=test_name) 372 373 def _select(self, name: str, index: int) -> None: 374 assert bui.app.classic is not None 375 music = bui.app.classic.music 376 self._selected_soundtrack_index = index 377 self._selected_soundtrack = name 378 cfg = bui.app.config 379 current_soundtrack = cfg.setdefault('Soundtrack', '__default__') 380 381 # If it varies from current, commit and play. 382 if current_soundtrack != name and self._allow_changing_soundtracks: 383 bui.getsound('gunCocking').play() 384 cfg['Soundtrack'] = self._selected_soundtrack 385 cfg.commit() 386 387 # Just play whats already playing.. this'll grab it from the 388 # new soundtrack. 389 music.do_play_music( 390 music.music_types[bui.app.classic.MusicPlayMode.REGULAR] 391 ) 392 393 def _back(self) -> None: 394 # pylint: disable=cyclic-import 395 from bauiv1lib.settings import audio 396 397 self._save_state() 398 bui.containerwidget( 399 edit=self._root_widget, transition=self._transition_out 400 ) 401 assert bui.app.classic is not None 402 bui.app.ui_v1.set_main_menu_window( 403 audio.AudioSettingsWindow(transition='in_left').get_root_widget() 404 ) 405 406 def _edit_soundtrack_with_sound(self) -> None: 407 # pylint: disable=cyclic-import 408 from bauiv1lib.purchase import PurchaseWindow 409 410 if ( 411 bui.app.classic is not None 412 and not bui.app.classic.accounts.have_pro_options() 413 ): 414 PurchaseWindow(items=['pro']) 415 return 416 bui.getsound('swish').play() 417 self._edit_soundtrack() 418 419 def _edit_soundtrack(self) -> None: 420 # pylint: disable=cyclic-import 421 from bauiv1lib.purchase import PurchaseWindow 422 from bauiv1lib.soundtrack.edit import SoundtrackEditWindow 423 424 if ( 425 bui.app.classic is not None 426 and not bui.app.classic.accounts.have_pro_options() 427 ): 428 PurchaseWindow(items=['pro']) 429 return 430 if self._selected_soundtrack is None: 431 return 432 if self._selected_soundtrack == '__default__': 433 bui.getsound('error').play() 434 bui.screenmessage( 435 bui.Lstr(resource=self._r + '.cantEditDefaultText'), 436 color=(1, 0, 0), 437 ) 438 return 439 440 self._save_state() 441 bui.containerwidget(edit=self._root_widget, transition='out_left') 442 assert bui.app.classic is not None 443 bui.app.ui_v1.set_main_menu_window( 444 SoundtrackEditWindow( 445 existing_soundtrack=self._selected_soundtrack 446 ).get_root_widget() 447 ) 448 449 def _get_soundtrack_display_name(self, soundtrack: str) -> bui.Lstr: 450 if soundtrack == '__default__': 451 return bui.Lstr(resource=self._r + '.defaultSoundtrackNameText') 452 return bui.Lstr(value=soundtrack) 453 454 def _refresh(self, select_soundtrack: str | None = None) -> None: 455 from efro.util import asserttype 456 457 self._allow_changing_soundtracks = False 458 old_selection = self._selected_soundtrack 459 460 # If there was no prev selection, look in prefs. 461 if old_selection is None: 462 old_selection = bui.app.config.get('Soundtrack') 463 old_selection_index = self._selected_soundtrack_index 464 465 # Delete old. 466 while self._soundtrack_widgets: 467 self._soundtrack_widgets.pop().delete() 468 469 self._soundtracks = bui.app.config.get('Soundtracks', {}) 470 assert self._soundtracks is not None 471 items = list(self._soundtracks.items()) 472 items.sort(key=lambda x: asserttype(x[0], str).lower()) 473 items = [('__default__', None)] + items # default is always first 474 index = 0 475 for pname, _pval in items: 476 assert pname is not None 477 txtw = bui.textwidget( 478 parent=self._col, 479 size=(self._width - 40, 24), 480 text=self._get_soundtrack_display_name(pname), 481 h_align='left', 482 v_align='center', 483 maxwidth=self._width - 110, 484 always_highlight=True, 485 on_select_call=bui.WeakCall(self._select, pname, index), 486 on_activate_call=self._edit_soundtrack_with_sound, 487 selectable=True, 488 ) 489 if index == 0: 490 bui.widget(edit=txtw, up_widget=self._back_button) 491 self._soundtrack_widgets.append(txtw) 492 493 # Select this one if the user requested it 494 if select_soundtrack is not None: 495 if pname == select_soundtrack: 496 bui.columnwidget( 497 edit=self._col, selected_child=txtw, visible_child=txtw 498 ) 499 else: 500 # Select this one if it was previously selected. 501 # Go by index if there's one. 502 if old_selection_index is not None: 503 if index == old_selection_index: 504 bui.columnwidget( 505 edit=self._col, 506 selected_child=txtw, 507 visible_child=txtw, 508 ) 509 else: # Otherwise look by name. 510 if pname == old_selection: 511 bui.columnwidget( 512 edit=self._col, 513 selected_child=txtw, 514 visible_child=txtw, 515 ) 516 index += 1 517 518 # Explicitly run select callback on current one and re-enable 519 # callbacks. 520 521 # Eww need to run this in a timer so it happens after our select 522 # callbacks. With a small-enough time sometimes it happens before 523 # anyway. Ew. need a way to just schedule a callable i guess. 524 bui.apptimer(0.1, bui.WeakCall(self._set_allow_changing)) 525 526 def _set_allow_changing(self) -> None: 527 self._allow_changing_soundtracks = True 528 assert self._selected_soundtrack is not None 529 assert self._selected_soundtrack_index is not None 530 self._select(self._selected_soundtrack, self._selected_soundtrack_index) 531 532 def _new_soundtrack(self) -> None: 533 # pylint: disable=cyclic-import 534 from bauiv1lib.purchase import PurchaseWindow 535 from bauiv1lib.soundtrack.edit import SoundtrackEditWindow 536 537 if ( 538 bui.app.classic is not None 539 and not bui.app.classic.accounts.have_pro_options() 540 ): 541 PurchaseWindow(items=['pro']) 542 return 543 self._save_state() 544 bui.containerwidget(edit=self._root_widget, transition='out_left') 545 SoundtrackEditWindow(existing_soundtrack=None) 546 547 def _create_done(self, new_soundtrack: str) -> None: 548 if new_soundtrack is not None: 549 bui.getsound('gunCocking').play() 550 self._refresh(select_soundtrack=new_soundtrack) 551 552 def _save_state(self) -> None: 553 try: 554 sel = self._root_widget.get_selected_child() 555 if sel == self._scrollwidget: 556 sel_name = 'Scroll' 557 elif sel == self._new_button: 558 sel_name = 'New' 559 elif sel == self._edit_button: 560 sel_name = 'Edit' 561 elif sel == self._duplicate_button: 562 sel_name = 'Duplicate' 563 elif sel == self._delete_button: 564 sel_name = 'Delete' 565 elif sel == self._back_button: 566 sel_name = 'Back' 567 else: 568 raise ValueError(f'unrecognized selection \'{sel}\'') 569 assert bui.app.classic is not None 570 bui.app.ui_v1.window_states[type(self)] = sel_name 571 except Exception: 572 logging.exception('Error saving state for %s.', self) 573 574 def _restore_state(self) -> None: 575 try: 576 assert bui.app.classic is not None 577 sel_name = bui.app.ui_v1.window_states.get(type(self)) 578 if sel_name == 'Scroll': 579 sel = self._scrollwidget 580 elif sel_name == 'New': 581 sel = self._new_button 582 elif sel_name == 'Edit': 583 sel = self._edit_button 584 elif sel_name == 'Duplicate': 585 sel = self._duplicate_button 586 elif sel_name == 'Delete': 587 sel = self._delete_button 588 else: 589 sel = self._scrollwidget 590 bui.containerwidget(edit=self._root_widget, selected_child=sel) 591 except Exception: 592 logging.exception('Error restoring state for %s.', self)
class
SoundtrackBrowserWindow(bauiv1._uitypes.Window):
18class SoundtrackBrowserWindow(bui.Window): 19 """Window for browsing soundtracks.""" 20 21 def __init__( 22 self, 23 transition: str = 'in_right', 24 origin_widget: bui.Widget | None = None, 25 ): 26 # pylint: disable=too-many-locals 27 # pylint: disable=too-many-statements 28 29 # If they provided an origin-widget, scale up from that. 30 scale_origin: tuple[float, float] | None 31 if origin_widget is not None: 32 self._transition_out = 'out_scale' 33 scale_origin = origin_widget.get_screen_space_center() 34 transition = 'in_scale' 35 else: 36 self._transition_out = 'out_right' 37 scale_origin = None 38 39 self._r = 'editSoundtrackWindow' 40 assert bui.app.classic is not None 41 uiscale = bui.app.ui_v1.uiscale 42 self._width = 800 if uiscale is bui.UIScale.SMALL else 600 43 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 44 self._height = ( 45 340 46 if uiscale is bui.UIScale.SMALL 47 else 370 48 if uiscale is bui.UIScale.MEDIUM 49 else 440 50 ) 51 spacing = 40.0 52 v = self._height - 40.0 53 v -= spacing * 1.0 54 55 super().__init__( 56 root_widget=bui.containerwidget( 57 size=(self._width, self._height), 58 transition=transition, 59 toolbar_visibility='menu_minimal', 60 scale_origin_stack_offset=scale_origin, 61 scale=( 62 2.3 63 if uiscale is bui.UIScale.SMALL 64 else 1.6 65 if uiscale is bui.UIScale.MEDIUM 66 else 1.0 67 ), 68 stack_offset=(0, -18) 69 if uiscale is bui.UIScale.SMALL 70 else (0, 0), 71 ) 72 ) 73 74 assert bui.app.classic is not None 75 if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: 76 self._back_button = None 77 else: 78 self._back_button = bui.buttonwidget( 79 parent=self._root_widget, 80 position=(45 + x_inset, self._height - 60), 81 size=(120, 60), 82 scale=0.8, 83 label=bui.Lstr(resource='backText'), 84 button_type='back', 85 autoselect=True, 86 ) 87 bui.buttonwidget( 88 edit=self._back_button, 89 button_type='backSmall', 90 size=(60, 60), 91 label=bui.charstr(bui.SpecialChar.BACK), 92 ) 93 bui.textwidget( 94 parent=self._root_widget, 95 position=(self._width * 0.5, self._height - 35), 96 size=(0, 0), 97 maxwidth=300, 98 text=bui.Lstr(resource=self._r + '.titleText'), 99 color=bui.app.ui_v1.title_color, 100 h_align='center', 101 v_align='center', 102 ) 103 104 h = 43 + x_inset 105 v = self._height - 60 106 b_color = (0.6, 0.53, 0.63) 107 b_textcolor = (0.75, 0.7, 0.8) 108 lock_tex = bui.gettexture('lock') 109 self._lock_images: list[bui.Widget] = [] 110 111 scl = ( 112 1.0 113 if uiscale is bui.UIScale.SMALL 114 else 1.13 115 if uiscale is bui.UIScale.MEDIUM 116 else 1.4 117 ) 118 v -= 60.0 * scl 119 self._new_button = btn = bui.buttonwidget( 120 parent=self._root_widget, 121 position=(h, v), 122 size=(100, 55.0 * scl), 123 on_activate_call=self._new_soundtrack, 124 color=b_color, 125 button_type='square', 126 autoselect=True, 127 textcolor=b_textcolor, 128 text_scale=0.7, 129 label=bui.Lstr(resource=self._r + '.newText'), 130 ) 131 self._lock_images.append( 132 bui.imagewidget( 133 parent=self._root_widget, 134 size=(30, 30), 135 draw_controller=btn, 136 position=(h - 10, v + 55.0 * scl - 28), 137 texture=lock_tex, 138 ) 139 ) 140 141 if self._back_button is None: 142 bui.widget( 143 edit=btn, 144 left_widget=bui.get_special_widget('back_button'), 145 ) 146 v -= 60.0 * scl 147 148 self._edit_button = btn = bui.buttonwidget( 149 parent=self._root_widget, 150 position=(h, v), 151 size=(100, 55.0 * scl), 152 on_activate_call=self._edit_soundtrack, 153 color=b_color, 154 button_type='square', 155 autoselect=True, 156 textcolor=b_textcolor, 157 text_scale=0.7, 158 label=bui.Lstr(resource=self._r + '.editText'), 159 ) 160 self._lock_images.append( 161 bui.imagewidget( 162 parent=self._root_widget, 163 size=(30, 30), 164 draw_controller=btn, 165 position=(h - 10, v + 55.0 * scl - 28), 166 texture=lock_tex, 167 ) 168 ) 169 if self._back_button is None: 170 bui.widget( 171 edit=btn, 172 left_widget=bui.get_special_widget('back_button'), 173 ) 174 v -= 60.0 * scl 175 176 self._duplicate_button = btn = bui.buttonwidget( 177 parent=self._root_widget, 178 position=(h, v), 179 size=(100, 55.0 * scl), 180 on_activate_call=self._duplicate_soundtrack, 181 button_type='square', 182 autoselect=True, 183 color=b_color, 184 textcolor=b_textcolor, 185 text_scale=0.7, 186 label=bui.Lstr(resource=self._r + '.duplicateText'), 187 ) 188 self._lock_images.append( 189 bui.imagewidget( 190 parent=self._root_widget, 191 size=(30, 30), 192 draw_controller=btn, 193 position=(h - 10, v + 55.0 * scl - 28), 194 texture=lock_tex, 195 ) 196 ) 197 if self._back_button is None: 198 bui.widget( 199 edit=btn, 200 left_widget=bui.get_special_widget('back_button'), 201 ) 202 v -= 60.0 * scl 203 204 self._delete_button = btn = bui.buttonwidget( 205 parent=self._root_widget, 206 position=(h, v), 207 size=(100, 55.0 * scl), 208 on_activate_call=self._delete_soundtrack, 209 color=b_color, 210 button_type='square', 211 autoselect=True, 212 textcolor=b_textcolor, 213 text_scale=0.7, 214 label=bui.Lstr(resource=self._r + '.deleteText'), 215 ) 216 self._lock_images.append( 217 bui.imagewidget( 218 parent=self._root_widget, 219 size=(30, 30), 220 draw_controller=btn, 221 position=(h - 10, v + 55.0 * scl - 28), 222 texture=lock_tex, 223 ) 224 ) 225 if self._back_button is None: 226 bui.widget( 227 edit=btn, 228 left_widget=bui.get_special_widget('back_button'), 229 ) 230 231 # Keep our lock images up to date/etc. 232 self._update_timer = bui.AppTimer( 233 1.0, bui.WeakCall(self._update), repeat=True 234 ) 235 self._update() 236 237 v = self._height - 65 238 scroll_height = self._height - 105 239 v -= scroll_height 240 self._scrollwidget = scrollwidget = bui.scrollwidget( 241 parent=self._root_widget, 242 position=(152 + x_inset, v), 243 highlight=False, 244 size=(self._width - (205 + 2 * x_inset), scroll_height), 245 ) 246 bui.widget( 247 edit=self._scrollwidget, 248 left_widget=self._new_button, 249 right_widget=bui.get_special_widget('party_button') 250 if bui.app.ui_v1.use_toolbars 251 else self._scrollwidget, 252 ) 253 self._col = bui.columnwidget(parent=scrollwidget, border=2, margin=0) 254 255 self._soundtracks: dict[str, Any] | None = None 256 self._selected_soundtrack: str | None = None 257 self._selected_soundtrack_index: int | None = None 258 self._soundtrack_widgets: list[bui.Widget] = [] 259 self._allow_changing_soundtracks = False 260 self._refresh() 261 if self._back_button is not None: 262 bui.buttonwidget( 263 edit=self._back_button, on_activate_call=self._back 264 ) 265 bui.containerwidget( 266 edit=self._root_widget, cancel_button=self._back_button 267 ) 268 else: 269 bui.containerwidget( 270 edit=self._root_widget, on_cancel_call=self._back 271 ) 272 273 def _update(self) -> None: 274 have = ( 275 bui.app.classic is None 276 or bui.app.classic.accounts.have_pro_options() 277 ) 278 for lock in self._lock_images: 279 bui.imagewidget(edit=lock, opacity=0.0 if have else 1.0) 280 281 def _do_delete_soundtrack(self) -> None: 282 cfg = bui.app.config 283 soundtracks = cfg.setdefault('Soundtracks', {}) 284 if self._selected_soundtrack in soundtracks: 285 del soundtracks[self._selected_soundtrack] 286 cfg.commit() 287 bui.getsound('shieldDown').play() 288 assert self._selected_soundtrack_index is not None 289 assert self._soundtracks is not None 290 if self._selected_soundtrack_index >= len(self._soundtracks): 291 self._selected_soundtrack_index = len(self._soundtracks) 292 self._refresh() 293 294 def _delete_soundtrack(self) -> None: 295 # pylint: disable=cyclic-import 296 from bauiv1lib.purchase import PurchaseWindow 297 from bauiv1lib.confirm import ConfirmWindow 298 299 if ( 300 bui.app.classic is not None 301 and not bui.app.classic.accounts.have_pro_options() 302 ): 303 PurchaseWindow(items=['pro']) 304 return 305 if self._selected_soundtrack is None: 306 return 307 if self._selected_soundtrack == '__default__': 308 bui.getsound('error').play() 309 bui.screenmessage( 310 bui.Lstr(resource=self._r + '.cantDeleteDefaultText'), 311 color=(1, 0, 0), 312 ) 313 else: 314 ConfirmWindow( 315 bui.Lstr( 316 resource=self._r + '.deleteConfirmText', 317 subs=[('${NAME}', self._selected_soundtrack)], 318 ), 319 self._do_delete_soundtrack, 320 450, 321 150, 322 ) 323 324 def _duplicate_soundtrack(self) -> None: 325 # pylint: disable=cyclic-import 326 from bauiv1lib.purchase import PurchaseWindow 327 328 if ( 329 bui.app.classic is not None 330 and not bui.app.classic.accounts.have_pro_options() 331 ): 332 PurchaseWindow(items=['pro']) 333 return 334 cfg = bui.app.config 335 cfg.setdefault('Soundtracks', {}) 336 337 if self._selected_soundtrack is None: 338 return 339 sdtk: dict[str, Any] 340 if self._selected_soundtrack == '__default__': 341 sdtk = {} 342 else: 343 sdtk = cfg['Soundtracks'][self._selected_soundtrack] 344 345 # Find a valid dup name that doesn't exist. 346 test_index = 1 347 copy_text = bui.Lstr(resource='copyOfText').evaluate() 348 # Get just 'Copy' or whatnot. 349 copy_word = copy_text.replace('${NAME}', '').strip() 350 base_name = self._get_soundtrack_display_name( 351 self._selected_soundtrack 352 ).evaluate() 353 assert isinstance(base_name, str) 354 355 # If it looks like a copy, strip digits and spaces off the end. 356 if copy_word in base_name: 357 while base_name[-1].isdigit() or base_name[-1] == ' ': 358 base_name = base_name[:-1] 359 while True: 360 if copy_word in base_name: 361 test_name = base_name 362 else: 363 test_name = copy_text.replace('${NAME}', base_name) 364 if test_index > 1: 365 test_name += ' ' + str(test_index) 366 if test_name not in cfg['Soundtracks']: 367 break 368 test_index += 1 369 370 cfg['Soundtracks'][test_name] = copy.deepcopy(sdtk) 371 cfg.commit() 372 self._refresh(select_soundtrack=test_name) 373 374 def _select(self, name: str, index: int) -> None: 375 assert bui.app.classic is not None 376 music = bui.app.classic.music 377 self._selected_soundtrack_index = index 378 self._selected_soundtrack = name 379 cfg = bui.app.config 380 current_soundtrack = cfg.setdefault('Soundtrack', '__default__') 381 382 # If it varies from current, commit and play. 383 if current_soundtrack != name and self._allow_changing_soundtracks: 384 bui.getsound('gunCocking').play() 385 cfg['Soundtrack'] = self._selected_soundtrack 386 cfg.commit() 387 388 # Just play whats already playing.. this'll grab it from the 389 # new soundtrack. 390 music.do_play_music( 391 music.music_types[bui.app.classic.MusicPlayMode.REGULAR] 392 ) 393 394 def _back(self) -> None: 395 # pylint: disable=cyclic-import 396 from bauiv1lib.settings import audio 397 398 self._save_state() 399 bui.containerwidget( 400 edit=self._root_widget, transition=self._transition_out 401 ) 402 assert bui.app.classic is not None 403 bui.app.ui_v1.set_main_menu_window( 404 audio.AudioSettingsWindow(transition='in_left').get_root_widget() 405 ) 406 407 def _edit_soundtrack_with_sound(self) -> None: 408 # pylint: disable=cyclic-import 409 from bauiv1lib.purchase import PurchaseWindow 410 411 if ( 412 bui.app.classic is not None 413 and not bui.app.classic.accounts.have_pro_options() 414 ): 415 PurchaseWindow(items=['pro']) 416 return 417 bui.getsound('swish').play() 418 self._edit_soundtrack() 419 420 def _edit_soundtrack(self) -> None: 421 # pylint: disable=cyclic-import 422 from bauiv1lib.purchase import PurchaseWindow 423 from bauiv1lib.soundtrack.edit import SoundtrackEditWindow 424 425 if ( 426 bui.app.classic is not None 427 and not bui.app.classic.accounts.have_pro_options() 428 ): 429 PurchaseWindow(items=['pro']) 430 return 431 if self._selected_soundtrack is None: 432 return 433 if self._selected_soundtrack == '__default__': 434 bui.getsound('error').play() 435 bui.screenmessage( 436 bui.Lstr(resource=self._r + '.cantEditDefaultText'), 437 color=(1, 0, 0), 438 ) 439 return 440 441 self._save_state() 442 bui.containerwidget(edit=self._root_widget, transition='out_left') 443 assert bui.app.classic is not None 444 bui.app.ui_v1.set_main_menu_window( 445 SoundtrackEditWindow( 446 existing_soundtrack=self._selected_soundtrack 447 ).get_root_widget() 448 ) 449 450 def _get_soundtrack_display_name(self, soundtrack: str) -> bui.Lstr: 451 if soundtrack == '__default__': 452 return bui.Lstr(resource=self._r + '.defaultSoundtrackNameText') 453 return bui.Lstr(value=soundtrack) 454 455 def _refresh(self, select_soundtrack: str | None = None) -> None: 456 from efro.util import asserttype 457 458 self._allow_changing_soundtracks = False 459 old_selection = self._selected_soundtrack 460 461 # If there was no prev selection, look in prefs. 462 if old_selection is None: 463 old_selection = bui.app.config.get('Soundtrack') 464 old_selection_index = self._selected_soundtrack_index 465 466 # Delete old. 467 while self._soundtrack_widgets: 468 self._soundtrack_widgets.pop().delete() 469 470 self._soundtracks = bui.app.config.get('Soundtracks', {}) 471 assert self._soundtracks is not None 472 items = list(self._soundtracks.items()) 473 items.sort(key=lambda x: asserttype(x[0], str).lower()) 474 items = [('__default__', None)] + items # default is always first 475 index = 0 476 for pname, _pval in items: 477 assert pname is not None 478 txtw = bui.textwidget( 479 parent=self._col, 480 size=(self._width - 40, 24), 481 text=self._get_soundtrack_display_name(pname), 482 h_align='left', 483 v_align='center', 484 maxwidth=self._width - 110, 485 always_highlight=True, 486 on_select_call=bui.WeakCall(self._select, pname, index), 487 on_activate_call=self._edit_soundtrack_with_sound, 488 selectable=True, 489 ) 490 if index == 0: 491 bui.widget(edit=txtw, up_widget=self._back_button) 492 self._soundtrack_widgets.append(txtw) 493 494 # Select this one if the user requested it 495 if select_soundtrack is not None: 496 if pname == select_soundtrack: 497 bui.columnwidget( 498 edit=self._col, selected_child=txtw, visible_child=txtw 499 ) 500 else: 501 # Select this one if it was previously selected. 502 # Go by index if there's one. 503 if old_selection_index is not None: 504 if index == old_selection_index: 505 bui.columnwidget( 506 edit=self._col, 507 selected_child=txtw, 508 visible_child=txtw, 509 ) 510 else: # Otherwise look by name. 511 if pname == old_selection: 512 bui.columnwidget( 513 edit=self._col, 514 selected_child=txtw, 515 visible_child=txtw, 516 ) 517 index += 1 518 519 # Explicitly run select callback on current one and re-enable 520 # callbacks. 521 522 # Eww need to run this in a timer so it happens after our select 523 # callbacks. With a small-enough time sometimes it happens before 524 # anyway. Ew. need a way to just schedule a callable i guess. 525 bui.apptimer(0.1, bui.WeakCall(self._set_allow_changing)) 526 527 def _set_allow_changing(self) -> None: 528 self._allow_changing_soundtracks = True 529 assert self._selected_soundtrack is not None 530 assert self._selected_soundtrack_index is not None 531 self._select(self._selected_soundtrack, self._selected_soundtrack_index) 532 533 def _new_soundtrack(self) -> None: 534 # pylint: disable=cyclic-import 535 from bauiv1lib.purchase import PurchaseWindow 536 from bauiv1lib.soundtrack.edit import SoundtrackEditWindow 537 538 if ( 539 bui.app.classic is not None 540 and not bui.app.classic.accounts.have_pro_options() 541 ): 542 PurchaseWindow(items=['pro']) 543 return 544 self._save_state() 545 bui.containerwidget(edit=self._root_widget, transition='out_left') 546 SoundtrackEditWindow(existing_soundtrack=None) 547 548 def _create_done(self, new_soundtrack: str) -> None: 549 if new_soundtrack is not None: 550 bui.getsound('gunCocking').play() 551 self._refresh(select_soundtrack=new_soundtrack) 552 553 def _save_state(self) -> None: 554 try: 555 sel = self._root_widget.get_selected_child() 556 if sel == self._scrollwidget: 557 sel_name = 'Scroll' 558 elif sel == self._new_button: 559 sel_name = 'New' 560 elif sel == self._edit_button: 561 sel_name = 'Edit' 562 elif sel == self._duplicate_button: 563 sel_name = 'Duplicate' 564 elif sel == self._delete_button: 565 sel_name = 'Delete' 566 elif sel == self._back_button: 567 sel_name = 'Back' 568 else: 569 raise ValueError(f'unrecognized selection \'{sel}\'') 570 assert bui.app.classic is not None 571 bui.app.ui_v1.window_states[type(self)] = sel_name 572 except Exception: 573 logging.exception('Error saving state for %s.', self) 574 575 def _restore_state(self) -> None: 576 try: 577 assert bui.app.classic is not None 578 sel_name = bui.app.ui_v1.window_states.get(type(self)) 579 if sel_name == 'Scroll': 580 sel = self._scrollwidget 581 elif sel_name == 'New': 582 sel = self._new_button 583 elif sel_name == 'Edit': 584 sel = self._edit_button 585 elif sel_name == 'Duplicate': 586 sel = self._duplicate_button 587 elif sel_name == 'Delete': 588 sel = self._delete_button 589 else: 590 sel = self._scrollwidget 591 bui.containerwidget(edit=self._root_widget, selected_child=sel) 592 except Exception: 593 logging.exception('Error restoring state for %s.', self)
Window for browsing soundtracks.
SoundtrackBrowserWindow( transition: str = 'in_right', origin_widget: _bauiv1.Widget | None = None)
21 def __init__( 22 self, 23 transition: str = 'in_right', 24 origin_widget: bui.Widget | None = None, 25 ): 26 # pylint: disable=too-many-locals 27 # pylint: disable=too-many-statements 28 29 # If they provided an origin-widget, scale up from that. 30 scale_origin: tuple[float, float] | None 31 if origin_widget is not None: 32 self._transition_out = 'out_scale' 33 scale_origin = origin_widget.get_screen_space_center() 34 transition = 'in_scale' 35 else: 36 self._transition_out = 'out_right' 37 scale_origin = None 38 39 self._r = 'editSoundtrackWindow' 40 assert bui.app.classic is not None 41 uiscale = bui.app.ui_v1.uiscale 42 self._width = 800 if uiscale is bui.UIScale.SMALL else 600 43 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 44 self._height = ( 45 340 46 if uiscale is bui.UIScale.SMALL 47 else 370 48 if uiscale is bui.UIScale.MEDIUM 49 else 440 50 ) 51 spacing = 40.0 52 v = self._height - 40.0 53 v -= spacing * 1.0 54 55 super().__init__( 56 root_widget=bui.containerwidget( 57 size=(self._width, self._height), 58 transition=transition, 59 toolbar_visibility='menu_minimal', 60 scale_origin_stack_offset=scale_origin, 61 scale=( 62 2.3 63 if uiscale is bui.UIScale.SMALL 64 else 1.6 65 if uiscale is bui.UIScale.MEDIUM 66 else 1.0 67 ), 68 stack_offset=(0, -18) 69 if uiscale is bui.UIScale.SMALL 70 else (0, 0), 71 ) 72 ) 73 74 assert bui.app.classic is not None 75 if bui.app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: 76 self._back_button = None 77 else: 78 self._back_button = bui.buttonwidget( 79 parent=self._root_widget, 80 position=(45 + x_inset, self._height - 60), 81 size=(120, 60), 82 scale=0.8, 83 label=bui.Lstr(resource='backText'), 84 button_type='back', 85 autoselect=True, 86 ) 87 bui.buttonwidget( 88 edit=self._back_button, 89 button_type='backSmall', 90 size=(60, 60), 91 label=bui.charstr(bui.SpecialChar.BACK), 92 ) 93 bui.textwidget( 94 parent=self._root_widget, 95 position=(self._width * 0.5, self._height - 35), 96 size=(0, 0), 97 maxwidth=300, 98 text=bui.Lstr(resource=self._r + '.titleText'), 99 color=bui.app.ui_v1.title_color, 100 h_align='center', 101 v_align='center', 102 ) 103 104 h = 43 + x_inset 105 v = self._height - 60 106 b_color = (0.6, 0.53, 0.63) 107 b_textcolor = (0.75, 0.7, 0.8) 108 lock_tex = bui.gettexture('lock') 109 self._lock_images: list[bui.Widget] = [] 110 111 scl = ( 112 1.0 113 if uiscale is bui.UIScale.SMALL 114 else 1.13 115 if uiscale is bui.UIScale.MEDIUM 116 else 1.4 117 ) 118 v -= 60.0 * scl 119 self._new_button = btn = bui.buttonwidget( 120 parent=self._root_widget, 121 position=(h, v), 122 size=(100, 55.0 * scl), 123 on_activate_call=self._new_soundtrack, 124 color=b_color, 125 button_type='square', 126 autoselect=True, 127 textcolor=b_textcolor, 128 text_scale=0.7, 129 label=bui.Lstr(resource=self._r + '.newText'), 130 ) 131 self._lock_images.append( 132 bui.imagewidget( 133 parent=self._root_widget, 134 size=(30, 30), 135 draw_controller=btn, 136 position=(h - 10, v + 55.0 * scl - 28), 137 texture=lock_tex, 138 ) 139 ) 140 141 if self._back_button is None: 142 bui.widget( 143 edit=btn, 144 left_widget=bui.get_special_widget('back_button'), 145 ) 146 v -= 60.0 * scl 147 148 self._edit_button = btn = bui.buttonwidget( 149 parent=self._root_widget, 150 position=(h, v), 151 size=(100, 55.0 * scl), 152 on_activate_call=self._edit_soundtrack, 153 color=b_color, 154 button_type='square', 155 autoselect=True, 156 textcolor=b_textcolor, 157 text_scale=0.7, 158 label=bui.Lstr(resource=self._r + '.editText'), 159 ) 160 self._lock_images.append( 161 bui.imagewidget( 162 parent=self._root_widget, 163 size=(30, 30), 164 draw_controller=btn, 165 position=(h - 10, v + 55.0 * scl - 28), 166 texture=lock_tex, 167 ) 168 ) 169 if self._back_button is None: 170 bui.widget( 171 edit=btn, 172 left_widget=bui.get_special_widget('back_button'), 173 ) 174 v -= 60.0 * scl 175 176 self._duplicate_button = btn = bui.buttonwidget( 177 parent=self._root_widget, 178 position=(h, v), 179 size=(100, 55.0 * scl), 180 on_activate_call=self._duplicate_soundtrack, 181 button_type='square', 182 autoselect=True, 183 color=b_color, 184 textcolor=b_textcolor, 185 text_scale=0.7, 186 label=bui.Lstr(resource=self._r + '.duplicateText'), 187 ) 188 self._lock_images.append( 189 bui.imagewidget( 190 parent=self._root_widget, 191 size=(30, 30), 192 draw_controller=btn, 193 position=(h - 10, v + 55.0 * scl - 28), 194 texture=lock_tex, 195 ) 196 ) 197 if self._back_button is None: 198 bui.widget( 199 edit=btn, 200 left_widget=bui.get_special_widget('back_button'), 201 ) 202 v -= 60.0 * scl 203 204 self._delete_button = btn = bui.buttonwidget( 205 parent=self._root_widget, 206 position=(h, v), 207 size=(100, 55.0 * scl), 208 on_activate_call=self._delete_soundtrack, 209 color=b_color, 210 button_type='square', 211 autoselect=True, 212 textcolor=b_textcolor, 213 text_scale=0.7, 214 label=bui.Lstr(resource=self._r + '.deleteText'), 215 ) 216 self._lock_images.append( 217 bui.imagewidget( 218 parent=self._root_widget, 219 size=(30, 30), 220 draw_controller=btn, 221 position=(h - 10, v + 55.0 * scl - 28), 222 texture=lock_tex, 223 ) 224 ) 225 if self._back_button is None: 226 bui.widget( 227 edit=btn, 228 left_widget=bui.get_special_widget('back_button'), 229 ) 230 231 # Keep our lock images up to date/etc. 232 self._update_timer = bui.AppTimer( 233 1.0, bui.WeakCall(self._update), repeat=True 234 ) 235 self._update() 236 237 v = self._height - 65 238 scroll_height = self._height - 105 239 v -= scroll_height 240 self._scrollwidget = scrollwidget = bui.scrollwidget( 241 parent=self._root_widget, 242 position=(152 + x_inset, v), 243 highlight=False, 244 size=(self._width - (205 + 2 * x_inset), scroll_height), 245 ) 246 bui.widget( 247 edit=self._scrollwidget, 248 left_widget=self._new_button, 249 right_widget=bui.get_special_widget('party_button') 250 if bui.app.ui_v1.use_toolbars 251 else self._scrollwidget, 252 ) 253 self._col = bui.columnwidget(parent=scrollwidget, border=2, margin=0) 254 255 self._soundtracks: dict[str, Any] | None = None 256 self._selected_soundtrack: str | None = None 257 self._selected_soundtrack_index: int | None = None 258 self._soundtrack_widgets: list[bui.Widget] = [] 259 self._allow_changing_soundtracks = False 260 self._refresh() 261 if self._back_button is not None: 262 bui.buttonwidget( 263 edit=self._back_button, on_activate_call=self._back 264 ) 265 bui.containerwidget( 266 edit=self._root_widget, cancel_button=self._back_button 267 ) 268 else: 269 bui.containerwidget( 270 edit=self._root_widget, on_cancel_call=self._back 271 )
Inherited Members
- bauiv1._uitypes.Window
- get_root_widget