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