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