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