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