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