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