bastd.ui.playlist.customizebrowser
Provides UI for viewing/creating/editing playlists.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides UI for viewing/creating/editing playlists.""" 4 5from __future__ import annotations 6 7import copy 8import time 9from typing import TYPE_CHECKING 10 11import ba 12import ba.internal 13 14if TYPE_CHECKING: 15 from typing import Any 16 17 18class PlaylistCustomizeBrowserWindow(ba.Window): 19 """Window for viewing a playlist.""" 20 21 def __init__( 22 self, 23 sessiontype: type[ba.Session], 24 transition: str = 'in_right', 25 select_playlist: str | None = None, 26 origin_widget: ba.Widget | None = None, 27 ): 28 # Yes this needs tidying. 29 # pylint: disable=too-many-locals 30 # pylint: disable=too-many-statements 31 # pylint: disable=cyclic-import 32 from bastd.ui import playlist 33 34 scale_origin: tuple[float, float] | None 35 if origin_widget is not None: 36 self._transition_out = 'out_scale' 37 scale_origin = origin_widget.get_screen_space_center() 38 transition = 'in_scale' 39 else: 40 self._transition_out = 'out_right' 41 scale_origin = None 42 43 self._sessiontype = sessiontype 44 self._pvars = playlist.PlaylistTypeVars(sessiontype) 45 self._max_playlists = 30 46 self._r = 'gameListWindow' 47 uiscale = ba.app.ui.uiscale 48 self._width = 750.0 if uiscale is ba.UIScale.SMALL else 650.0 49 x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 50 self._height = ( 51 380.0 52 if uiscale is ba.UIScale.SMALL 53 else 420.0 54 if uiscale is ba.UIScale.MEDIUM 55 else 500.0 56 ) 57 top_extra = 20.0 if uiscale is ba.UIScale.SMALL else 0.0 58 59 super().__init__( 60 root_widget=ba.containerwidget( 61 size=(self._width, self._height + top_extra), 62 transition=transition, 63 scale_origin_stack_offset=scale_origin, 64 scale=( 65 2.05 66 if uiscale is ba.UIScale.SMALL 67 else 1.5 68 if uiscale is ba.UIScale.MEDIUM 69 else 1.0 70 ), 71 stack_offset=(0, -10) 72 if uiscale is ba.UIScale.SMALL 73 else (0, 0), 74 ) 75 ) 76 77 self._back_button = back_button = btn = ba.buttonwidget( 78 parent=self._root_widget, 79 position=(43 + x_inset, self._height - 60), 80 size=(160, 68), 81 scale=0.77, 82 autoselect=True, 83 text_scale=1.3, 84 label=ba.Lstr(resource='backText'), 85 button_type='back', 86 ) 87 88 ba.textwidget( 89 parent=self._root_widget, 90 position=(0, self._height - 47), 91 size=(self._width, 25), 92 text=ba.Lstr( 93 resource=self._r + '.titleText', 94 subs=[('${TYPE}', self._pvars.window_title_name)], 95 ), 96 color=ba.app.ui.heading_color, 97 maxwidth=290, 98 h_align='center', 99 v_align='center', 100 ) 101 102 ba.buttonwidget( 103 edit=btn, 104 button_type='backSmall', 105 size=(60, 60), 106 label=ba.charstr(ba.SpecialChar.BACK), 107 ) 108 109 v = self._height - 59.0 110 h = 41 + x_inset 111 b_color = (0.6, 0.53, 0.63) 112 b_textcolor = (0.75, 0.7, 0.8) 113 self._lock_images: list[ba.Widget] = [] 114 lock_tex = ba.gettexture('lock') 115 116 scl = ( 117 1.1 118 if uiscale is ba.UIScale.SMALL 119 else 1.27 120 if uiscale is ba.UIScale.MEDIUM 121 else 1.57 122 ) 123 scl *= 0.63 124 v -= 65.0 * scl 125 new_button = btn = ba.buttonwidget( 126 parent=self._root_widget, 127 position=(h, v), 128 size=(90, 58.0 * scl), 129 on_activate_call=self._new_playlist, 130 color=b_color, 131 autoselect=True, 132 button_type='square', 133 textcolor=b_textcolor, 134 text_scale=0.7, 135 label=ba.Lstr( 136 resource='newText', fallback_resource=self._r + '.newText' 137 ), 138 ) 139 self._lock_images.append( 140 ba.imagewidget( 141 parent=self._root_widget, 142 size=(30, 30), 143 draw_controller=btn, 144 position=(h - 10, v + 58.0 * scl - 28), 145 texture=lock_tex, 146 ) 147 ) 148 149 v -= 65.0 * scl 150 self._edit_button = edit_button = btn = ba.buttonwidget( 151 parent=self._root_widget, 152 position=(h, v), 153 size=(90, 58.0 * scl), 154 on_activate_call=self._edit_playlist, 155 color=b_color, 156 autoselect=True, 157 textcolor=b_textcolor, 158 button_type='square', 159 text_scale=0.7, 160 label=ba.Lstr( 161 resource='editText', fallback_resource=self._r + '.editText' 162 ), 163 ) 164 self._lock_images.append( 165 ba.imagewidget( 166 parent=self._root_widget, 167 size=(30, 30), 168 draw_controller=btn, 169 position=(h - 10, v + 58.0 * scl - 28), 170 texture=lock_tex, 171 ) 172 ) 173 174 v -= 65.0 * scl 175 duplicate_button = btn = ba.buttonwidget( 176 parent=self._root_widget, 177 position=(h, v), 178 size=(90, 58.0 * scl), 179 on_activate_call=self._duplicate_playlist, 180 color=b_color, 181 autoselect=True, 182 textcolor=b_textcolor, 183 button_type='square', 184 text_scale=0.7, 185 label=ba.Lstr( 186 resource='duplicateText', 187 fallback_resource=self._r + '.duplicateText', 188 ), 189 ) 190 self._lock_images.append( 191 ba.imagewidget( 192 parent=self._root_widget, 193 size=(30, 30), 194 draw_controller=btn, 195 position=(h - 10, v + 58.0 * scl - 28), 196 texture=lock_tex, 197 ) 198 ) 199 200 v -= 65.0 * scl 201 delete_button = btn = ba.buttonwidget( 202 parent=self._root_widget, 203 position=(h, v), 204 size=(90, 58.0 * scl), 205 on_activate_call=self._delete_playlist, 206 color=b_color, 207 autoselect=True, 208 textcolor=b_textcolor, 209 button_type='square', 210 text_scale=0.7, 211 label=ba.Lstr( 212 resource='deleteText', fallback_resource=self._r + '.deleteText' 213 ), 214 ) 215 self._lock_images.append( 216 ba.imagewidget( 217 parent=self._root_widget, 218 size=(30, 30), 219 draw_controller=btn, 220 position=(h - 10, v + 58.0 * scl - 28), 221 texture=lock_tex, 222 ) 223 ) 224 v -= 65.0 * scl 225 self._import_button = ba.buttonwidget( 226 parent=self._root_widget, 227 position=(h, v), 228 size=(90, 58.0 * scl), 229 on_activate_call=self._import_playlist, 230 color=b_color, 231 autoselect=True, 232 textcolor=b_textcolor, 233 button_type='square', 234 text_scale=0.7, 235 label=ba.Lstr(resource='importText'), 236 ) 237 v -= 65.0 * scl 238 btn = ba.buttonwidget( 239 parent=self._root_widget, 240 position=(h, v), 241 size=(90, 58.0 * scl), 242 on_activate_call=self._share_playlist, 243 color=b_color, 244 autoselect=True, 245 textcolor=b_textcolor, 246 button_type='square', 247 text_scale=0.7, 248 label=ba.Lstr(resource='shareText'), 249 ) 250 self._lock_images.append( 251 ba.imagewidget( 252 parent=self._root_widget, 253 size=(30, 30), 254 draw_controller=btn, 255 position=(h - 10, v + 58.0 * scl - 28), 256 texture=lock_tex, 257 ) 258 ) 259 260 v = self._height - 75 261 self._scroll_height = self._height - 119 262 scrollwidget = ba.scrollwidget( 263 parent=self._root_widget, 264 position=(140 + x_inset, v - self._scroll_height), 265 size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10), 266 highlight=False, 267 ) 268 ba.widget(edit=back_button, right_widget=scrollwidget) 269 self._columnwidget = ba.columnwidget( 270 parent=scrollwidget, border=2, margin=0 271 ) 272 273 h = 145 274 275 self._do_randomize_val = ba.app.config.get( 276 self._pvars.config_name + ' Playlist Randomize', 0 277 ) 278 279 h += 210 280 281 for btn in [new_button, delete_button, edit_button, duplicate_button]: 282 ba.widget(edit=btn, right_widget=scrollwidget) 283 ba.widget( 284 edit=scrollwidget, 285 left_widget=new_button, 286 right_widget=ba.internal.get_special_widget('party_button') 287 if ba.app.ui.use_toolbars 288 else None, 289 ) 290 291 # make sure config exists 292 self._config_name_full = self._pvars.config_name + ' Playlists' 293 294 if self._config_name_full not in ba.app.config: 295 ba.app.config[self._config_name_full] = {} 296 297 self._selected_playlist_name: str | None = None 298 self._selected_playlist_index: int | None = None 299 self._playlist_widgets: list[ba.Widget] = [] 300 301 self._refresh(select_playlist=select_playlist) 302 303 ba.buttonwidget(edit=back_button, on_activate_call=self._back) 304 ba.containerwidget(edit=self._root_widget, cancel_button=back_button) 305 306 ba.containerwidget(edit=self._root_widget, selected_child=scrollwidget) 307 308 # Keep our lock images up to date/etc. 309 self._update_timer = ba.Timer( 310 1.0, 311 ba.WeakCall(self._update), 312 timetype=ba.TimeType.REAL, 313 repeat=True, 314 ) 315 self._update() 316 317 def _update(self) -> None: 318 have = ba.app.accounts_v1.have_pro_options() 319 for lock in self._lock_images: 320 ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0) 321 322 def _back(self) -> None: 323 # pylint: disable=cyclic-import 324 from bastd.ui.playlist import browser 325 326 if self._selected_playlist_name is not None: 327 cfg = ba.app.config 328 cfg[ 329 self._pvars.config_name + ' Playlist Selection' 330 ] = self._selected_playlist_name 331 cfg.commit() 332 333 ba.containerwidget( 334 edit=self._root_widget, transition=self._transition_out 335 ) 336 ba.app.ui.set_main_menu_window( 337 browser.PlaylistBrowserWindow( 338 transition='in_left', sessiontype=self._sessiontype 339 ).get_root_widget() 340 ) 341 342 def _select(self, name: str, index: int) -> None: 343 self._selected_playlist_name = name 344 self._selected_playlist_index = index 345 346 def _run_selected_playlist(self) -> None: 347 # pylint: disable=cyclic-import 348 ba.internal.unlock_all_input() 349 try: 350 ba.internal.new_host_session(self._sessiontype) 351 except Exception: 352 from bastd import mainmenu 353 354 ba.print_exception(f'Error running session {self._sessiontype}.') 355 356 # Drop back into a main menu session. 357 ba.internal.new_host_session(mainmenu.MainMenuSession) 358 359 def _choose_playlist(self) -> None: 360 if self._selected_playlist_name is None: 361 return 362 self._save_playlist_selection() 363 ba.containerwidget(edit=self._root_widget, transition='out_left') 364 ba.internal.fade_screen(False, endcall=self._run_selected_playlist) 365 ba.internal.lock_all_input() 366 367 def _refresh(self, select_playlist: str | None = None) -> None: 368 from efro.util import asserttype 369 370 old_selection = self._selected_playlist_name 371 372 # If there was no prev selection, look in prefs. 373 if old_selection is None: 374 old_selection = ba.app.config.get( 375 self._pvars.config_name + ' Playlist Selection' 376 ) 377 378 old_selection_index = self._selected_playlist_index 379 380 # Delete old. 381 while self._playlist_widgets: 382 self._playlist_widgets.pop().delete() 383 384 items = list(ba.app.config[self._config_name_full].items()) 385 386 # Make sure everything is unicode now. 387 items = [ 388 (i[0].decode(), i[1]) if not isinstance(i[0], str) else i 389 for i in items 390 ] 391 392 items.sort(key=lambda x: asserttype(x[0], str).lower()) 393 394 items = [['__default__', None]] + items # Default is always first. 395 index = 0 396 for pname, _ in items: 397 assert pname is not None 398 txtw = ba.textwidget( 399 parent=self._columnwidget, 400 size=(self._width - 40, 30), 401 maxwidth=self._width - 110, 402 text=self._get_playlist_display_name(pname), 403 h_align='left', 404 v_align='center', 405 color=(0.6, 0.6, 0.7, 1.0) 406 if pname == '__default__' 407 else (0.85, 0.85, 0.85, 1), 408 always_highlight=True, 409 on_select_call=ba.Call(self._select, pname, index), 410 on_activate_call=ba.Call(self._edit_button.activate), 411 selectable=True, 412 ) 413 ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) 414 415 # Hitting up from top widget should jump to 'back' 416 if index == 0: 417 ba.widget(edit=txtw, up_widget=self._back_button) 418 419 self._playlist_widgets.append(txtw) 420 421 # Select this one if the user requested it. 422 if select_playlist is not None: 423 if pname == select_playlist: 424 ba.columnwidget( 425 edit=self._columnwidget, 426 selected_child=txtw, 427 visible_child=txtw, 428 ) 429 else: 430 # Select this one if it was previously selected. 431 # Go by index if there's one. 432 if old_selection_index is not None: 433 if index == old_selection_index: 434 ba.columnwidget( 435 edit=self._columnwidget, 436 selected_child=txtw, 437 visible_child=txtw, 438 ) 439 else: # Otherwise look by name. 440 if pname == old_selection: 441 ba.columnwidget( 442 edit=self._columnwidget, 443 selected_child=txtw, 444 visible_child=txtw, 445 ) 446 447 index += 1 448 449 def _save_playlist_selection(self) -> None: 450 # Store the selected playlist in prefs. 451 # This serves dual purposes of letting us re-select it next time 452 # if we want and also lets us pass it to the game (since we reset 453 # the whole python environment that's not actually easy). 454 cfg = ba.app.config 455 cfg[ 456 self._pvars.config_name + ' Playlist Selection' 457 ] = self._selected_playlist_name 458 cfg[ 459 self._pvars.config_name + ' Playlist Randomize' 460 ] = self._do_randomize_val 461 cfg.commit() 462 463 def _new_playlist(self) -> None: 464 # pylint: disable=cyclic-import 465 from bastd.ui.playlist.editcontroller import PlaylistEditController 466 from bastd.ui.purchase import PurchaseWindow 467 468 if not ba.app.accounts_v1.have_pro_options(): 469 PurchaseWindow(items=['pro']) 470 return 471 472 # Clamp at our max playlist number. 473 if len(ba.app.config[self._config_name_full]) > self._max_playlists: 474 ba.screenmessage( 475 ba.Lstr( 476 translate=( 477 'serverResponses', 478 'Max number of playlists reached.', 479 ) 480 ), 481 color=(1, 0, 0), 482 ) 483 ba.playsound(ba.getsound('error')) 484 return 485 486 # In case they cancel so we can return to this state. 487 self._save_playlist_selection() 488 489 # Kick off the edit UI. 490 PlaylistEditController(sessiontype=self._sessiontype) 491 ba.containerwidget(edit=self._root_widget, transition='out_left') 492 493 def _edit_playlist(self) -> None: 494 # pylint: disable=cyclic-import 495 from bastd.ui.playlist.editcontroller import PlaylistEditController 496 from bastd.ui.purchase import PurchaseWindow 497 498 if not ba.app.accounts_v1.have_pro_options(): 499 PurchaseWindow(items=['pro']) 500 return 501 if self._selected_playlist_name is None: 502 return 503 if self._selected_playlist_name == '__default__': 504 ba.playsound(ba.getsound('error')) 505 ba.screenmessage(ba.Lstr(resource=self._r + '.cantEditDefaultText')) 506 return 507 self._save_playlist_selection() 508 PlaylistEditController( 509 existing_playlist_name=self._selected_playlist_name, 510 sessiontype=self._sessiontype, 511 ) 512 ba.containerwidget(edit=self._root_widget, transition='out_left') 513 514 def _do_delete_playlist(self) -> None: 515 ba.internal.add_transaction( 516 { 517 'type': 'REMOVE_PLAYLIST', 518 'playlistType': self._pvars.config_name, 519 'playlistName': self._selected_playlist_name, 520 } 521 ) 522 ba.internal.run_transactions() 523 ba.playsound(ba.getsound('shieldDown')) 524 525 # (we don't use len()-1 here because the default list adds one) 526 assert self._selected_playlist_index is not None 527 if self._selected_playlist_index > len( 528 ba.app.config[self._pvars.config_name + ' Playlists'] 529 ): 530 self._selected_playlist_index = len( 531 ba.app.config[self._pvars.config_name + ' Playlists'] 532 ) 533 self._refresh() 534 535 def _import_playlist(self) -> None: 536 # pylint: disable=cyclic-import 537 from bastd.ui.playlist import share 538 539 # Gotta be signed in for this to work. 540 if ba.internal.get_v1_account_state() != 'signed_in': 541 ba.screenmessage( 542 ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) 543 ) 544 ba.playsound(ba.getsound('error')) 545 return 546 547 share.SharePlaylistImportWindow( 548 origin_widget=self._import_button, 549 on_success_callback=ba.WeakCall(self._on_playlist_import_success), 550 ) 551 552 def _on_playlist_import_success(self) -> None: 553 self._refresh() 554 555 def _on_share_playlist_response(self, name: str, response: Any) -> None: 556 # pylint: disable=cyclic-import 557 from bastd.ui.playlist import share 558 559 if response is None: 560 ba.screenmessage( 561 ba.Lstr(resource='internal.unavailableNoConnectionText'), 562 color=(1, 0, 0), 563 ) 564 ba.playsound(ba.getsound('error')) 565 return 566 share.SharePlaylistResultsWindow(name, response) 567 568 def _share_playlist(self) -> None: 569 # pylint: disable=cyclic-import 570 from bastd.ui.purchase import PurchaseWindow 571 572 if not ba.app.accounts_v1.have_pro_options(): 573 PurchaseWindow(items=['pro']) 574 return 575 576 # Gotta be signed in for this to work. 577 if ba.internal.get_v1_account_state() != 'signed_in': 578 ba.screenmessage( 579 ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) 580 ) 581 ba.playsound(ba.getsound('error')) 582 return 583 if self._selected_playlist_name == '__default__': 584 ba.playsound(ba.getsound('error')) 585 ba.screenmessage( 586 ba.Lstr(resource=self._r + '.cantShareDefaultText'), 587 color=(1, 0, 0), 588 ) 589 return 590 591 if self._selected_playlist_name is None: 592 return 593 594 ba.internal.add_transaction( 595 { 596 'type': 'SHARE_PLAYLIST', 597 'expire_time': time.time() + 5, 598 'playlistType': self._pvars.config_name, 599 'playlistName': self._selected_playlist_name, 600 }, 601 callback=ba.WeakCall( 602 self._on_share_playlist_response, self._selected_playlist_name 603 ), 604 ) 605 ba.internal.run_transactions() 606 ba.screenmessage(ba.Lstr(resource='sharingText')) 607 608 def _delete_playlist(self) -> None: 609 # pylint: disable=cyclic-import 610 from bastd.ui.purchase import PurchaseWindow 611 from bastd.ui.confirm import ConfirmWindow 612 613 if not ba.app.accounts_v1.have_pro_options(): 614 PurchaseWindow(items=['pro']) 615 return 616 617 if self._selected_playlist_name is None: 618 return 619 if self._selected_playlist_name == '__default__': 620 ba.playsound(ba.getsound('error')) 621 ba.screenmessage( 622 ba.Lstr(resource=self._r + '.cantDeleteDefaultText') 623 ) 624 else: 625 ConfirmWindow( 626 ba.Lstr( 627 resource=self._r + '.deleteConfirmText', 628 subs=[('${LIST}', self._selected_playlist_name)], 629 ), 630 self._do_delete_playlist, 631 450, 632 150, 633 ) 634 635 def _get_playlist_display_name(self, playlist: str) -> ba.Lstr: 636 if playlist == '__default__': 637 return self._pvars.default_list_name 638 return ( 639 playlist 640 if isinstance(playlist, ba.Lstr) 641 else ba.Lstr(value=playlist) 642 ) 643 644 def _duplicate_playlist(self) -> None: 645 # pylint: disable=too-many-branches 646 # pylint: disable=cyclic-import 647 from bastd.ui.purchase import PurchaseWindow 648 649 if not ba.app.accounts_v1.have_pro_options(): 650 PurchaseWindow(items=['pro']) 651 return 652 if self._selected_playlist_name is None: 653 return 654 plst: list[dict[str, Any]] | None 655 if self._selected_playlist_name == '__default__': 656 plst = self._pvars.get_default_list_call() 657 else: 658 plst = ba.app.config[self._config_name_full].get( 659 self._selected_playlist_name 660 ) 661 if plst is None: 662 ba.playsound(ba.getsound('error')) 663 return 664 665 # clamp at our max playlist number 666 if len(ba.app.config[self._config_name_full]) > self._max_playlists: 667 ba.screenmessage( 668 ba.Lstr( 669 translate=( 670 'serverResponses', 671 'Max number of playlists reached.', 672 ) 673 ), 674 color=(1, 0, 0), 675 ) 676 ba.playsound(ba.getsound('error')) 677 return 678 679 copy_text = ba.Lstr(resource='copyOfText').evaluate() 680 # get just 'Copy' or whatnot 681 copy_word = copy_text.replace('${NAME}', '').strip() 682 # find a valid dup name that doesn't exist 683 684 test_index = 1 685 base_name = self._get_playlist_display_name( 686 self._selected_playlist_name 687 ).evaluate() 688 689 # If it looks like a copy, strip digits and spaces off the end. 690 if copy_word in base_name: 691 while base_name[-1].isdigit() or base_name[-1] == ' ': 692 base_name = base_name[:-1] 693 while True: 694 if copy_word in base_name: 695 test_name = base_name 696 else: 697 test_name = copy_text.replace('${NAME}', base_name) 698 if test_index > 1: 699 test_name += ' ' + str(test_index) 700 if test_name not in ba.app.config[self._config_name_full]: 701 break 702 test_index += 1 703 704 ba.internal.add_transaction( 705 { 706 'type': 'ADD_PLAYLIST', 707 'playlistType': self._pvars.config_name, 708 'playlistName': test_name, 709 'playlist': copy.deepcopy(plst), 710 } 711 ) 712 ba.internal.run_transactions() 713 714 ba.playsound(ba.getsound('gunCocking')) 715 self._refresh(select_playlist=test_name)
class
PlaylistCustomizeBrowserWindow(ba.ui.Window):
19class PlaylistCustomizeBrowserWindow(ba.Window): 20 """Window for viewing a playlist.""" 21 22 def __init__( 23 self, 24 sessiontype: type[ba.Session], 25 transition: str = 'in_right', 26 select_playlist: str | None = None, 27 origin_widget: ba.Widget | None = None, 28 ): 29 # Yes this needs tidying. 30 # pylint: disable=too-many-locals 31 # pylint: disable=too-many-statements 32 # pylint: disable=cyclic-import 33 from bastd.ui import playlist 34 35 scale_origin: tuple[float, float] | None 36 if origin_widget is not None: 37 self._transition_out = 'out_scale' 38 scale_origin = origin_widget.get_screen_space_center() 39 transition = 'in_scale' 40 else: 41 self._transition_out = 'out_right' 42 scale_origin = None 43 44 self._sessiontype = sessiontype 45 self._pvars = playlist.PlaylistTypeVars(sessiontype) 46 self._max_playlists = 30 47 self._r = 'gameListWindow' 48 uiscale = ba.app.ui.uiscale 49 self._width = 750.0 if uiscale is ba.UIScale.SMALL else 650.0 50 x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 51 self._height = ( 52 380.0 53 if uiscale is ba.UIScale.SMALL 54 else 420.0 55 if uiscale is ba.UIScale.MEDIUM 56 else 500.0 57 ) 58 top_extra = 20.0 if uiscale is ba.UIScale.SMALL else 0.0 59 60 super().__init__( 61 root_widget=ba.containerwidget( 62 size=(self._width, self._height + top_extra), 63 transition=transition, 64 scale_origin_stack_offset=scale_origin, 65 scale=( 66 2.05 67 if uiscale is ba.UIScale.SMALL 68 else 1.5 69 if uiscale is ba.UIScale.MEDIUM 70 else 1.0 71 ), 72 stack_offset=(0, -10) 73 if uiscale is ba.UIScale.SMALL 74 else (0, 0), 75 ) 76 ) 77 78 self._back_button = back_button = btn = ba.buttonwidget( 79 parent=self._root_widget, 80 position=(43 + x_inset, self._height - 60), 81 size=(160, 68), 82 scale=0.77, 83 autoselect=True, 84 text_scale=1.3, 85 label=ba.Lstr(resource='backText'), 86 button_type='back', 87 ) 88 89 ba.textwidget( 90 parent=self._root_widget, 91 position=(0, self._height - 47), 92 size=(self._width, 25), 93 text=ba.Lstr( 94 resource=self._r + '.titleText', 95 subs=[('${TYPE}', self._pvars.window_title_name)], 96 ), 97 color=ba.app.ui.heading_color, 98 maxwidth=290, 99 h_align='center', 100 v_align='center', 101 ) 102 103 ba.buttonwidget( 104 edit=btn, 105 button_type='backSmall', 106 size=(60, 60), 107 label=ba.charstr(ba.SpecialChar.BACK), 108 ) 109 110 v = self._height - 59.0 111 h = 41 + x_inset 112 b_color = (0.6, 0.53, 0.63) 113 b_textcolor = (0.75, 0.7, 0.8) 114 self._lock_images: list[ba.Widget] = [] 115 lock_tex = ba.gettexture('lock') 116 117 scl = ( 118 1.1 119 if uiscale is ba.UIScale.SMALL 120 else 1.27 121 if uiscale is ba.UIScale.MEDIUM 122 else 1.57 123 ) 124 scl *= 0.63 125 v -= 65.0 * scl 126 new_button = btn = ba.buttonwidget( 127 parent=self._root_widget, 128 position=(h, v), 129 size=(90, 58.0 * scl), 130 on_activate_call=self._new_playlist, 131 color=b_color, 132 autoselect=True, 133 button_type='square', 134 textcolor=b_textcolor, 135 text_scale=0.7, 136 label=ba.Lstr( 137 resource='newText', fallback_resource=self._r + '.newText' 138 ), 139 ) 140 self._lock_images.append( 141 ba.imagewidget( 142 parent=self._root_widget, 143 size=(30, 30), 144 draw_controller=btn, 145 position=(h - 10, v + 58.0 * scl - 28), 146 texture=lock_tex, 147 ) 148 ) 149 150 v -= 65.0 * scl 151 self._edit_button = edit_button = btn = ba.buttonwidget( 152 parent=self._root_widget, 153 position=(h, v), 154 size=(90, 58.0 * scl), 155 on_activate_call=self._edit_playlist, 156 color=b_color, 157 autoselect=True, 158 textcolor=b_textcolor, 159 button_type='square', 160 text_scale=0.7, 161 label=ba.Lstr( 162 resource='editText', fallback_resource=self._r + '.editText' 163 ), 164 ) 165 self._lock_images.append( 166 ba.imagewidget( 167 parent=self._root_widget, 168 size=(30, 30), 169 draw_controller=btn, 170 position=(h - 10, v + 58.0 * scl - 28), 171 texture=lock_tex, 172 ) 173 ) 174 175 v -= 65.0 * scl 176 duplicate_button = btn = ba.buttonwidget( 177 parent=self._root_widget, 178 position=(h, v), 179 size=(90, 58.0 * scl), 180 on_activate_call=self._duplicate_playlist, 181 color=b_color, 182 autoselect=True, 183 textcolor=b_textcolor, 184 button_type='square', 185 text_scale=0.7, 186 label=ba.Lstr( 187 resource='duplicateText', 188 fallback_resource=self._r + '.duplicateText', 189 ), 190 ) 191 self._lock_images.append( 192 ba.imagewidget( 193 parent=self._root_widget, 194 size=(30, 30), 195 draw_controller=btn, 196 position=(h - 10, v + 58.0 * scl - 28), 197 texture=lock_tex, 198 ) 199 ) 200 201 v -= 65.0 * scl 202 delete_button = btn = ba.buttonwidget( 203 parent=self._root_widget, 204 position=(h, v), 205 size=(90, 58.0 * scl), 206 on_activate_call=self._delete_playlist, 207 color=b_color, 208 autoselect=True, 209 textcolor=b_textcolor, 210 button_type='square', 211 text_scale=0.7, 212 label=ba.Lstr( 213 resource='deleteText', fallback_resource=self._r + '.deleteText' 214 ), 215 ) 216 self._lock_images.append( 217 ba.imagewidget( 218 parent=self._root_widget, 219 size=(30, 30), 220 draw_controller=btn, 221 position=(h - 10, v + 58.0 * scl - 28), 222 texture=lock_tex, 223 ) 224 ) 225 v -= 65.0 * scl 226 self._import_button = ba.buttonwidget( 227 parent=self._root_widget, 228 position=(h, v), 229 size=(90, 58.0 * scl), 230 on_activate_call=self._import_playlist, 231 color=b_color, 232 autoselect=True, 233 textcolor=b_textcolor, 234 button_type='square', 235 text_scale=0.7, 236 label=ba.Lstr(resource='importText'), 237 ) 238 v -= 65.0 * scl 239 btn = ba.buttonwidget( 240 parent=self._root_widget, 241 position=(h, v), 242 size=(90, 58.0 * scl), 243 on_activate_call=self._share_playlist, 244 color=b_color, 245 autoselect=True, 246 textcolor=b_textcolor, 247 button_type='square', 248 text_scale=0.7, 249 label=ba.Lstr(resource='shareText'), 250 ) 251 self._lock_images.append( 252 ba.imagewidget( 253 parent=self._root_widget, 254 size=(30, 30), 255 draw_controller=btn, 256 position=(h - 10, v + 58.0 * scl - 28), 257 texture=lock_tex, 258 ) 259 ) 260 261 v = self._height - 75 262 self._scroll_height = self._height - 119 263 scrollwidget = ba.scrollwidget( 264 parent=self._root_widget, 265 position=(140 + x_inset, v - self._scroll_height), 266 size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10), 267 highlight=False, 268 ) 269 ba.widget(edit=back_button, right_widget=scrollwidget) 270 self._columnwidget = ba.columnwidget( 271 parent=scrollwidget, border=2, margin=0 272 ) 273 274 h = 145 275 276 self._do_randomize_val = ba.app.config.get( 277 self._pvars.config_name + ' Playlist Randomize', 0 278 ) 279 280 h += 210 281 282 for btn in [new_button, delete_button, edit_button, duplicate_button]: 283 ba.widget(edit=btn, right_widget=scrollwidget) 284 ba.widget( 285 edit=scrollwidget, 286 left_widget=new_button, 287 right_widget=ba.internal.get_special_widget('party_button') 288 if ba.app.ui.use_toolbars 289 else None, 290 ) 291 292 # make sure config exists 293 self._config_name_full = self._pvars.config_name + ' Playlists' 294 295 if self._config_name_full not in ba.app.config: 296 ba.app.config[self._config_name_full] = {} 297 298 self._selected_playlist_name: str | None = None 299 self._selected_playlist_index: int | None = None 300 self._playlist_widgets: list[ba.Widget] = [] 301 302 self._refresh(select_playlist=select_playlist) 303 304 ba.buttonwidget(edit=back_button, on_activate_call=self._back) 305 ba.containerwidget(edit=self._root_widget, cancel_button=back_button) 306 307 ba.containerwidget(edit=self._root_widget, selected_child=scrollwidget) 308 309 # Keep our lock images up to date/etc. 310 self._update_timer = ba.Timer( 311 1.0, 312 ba.WeakCall(self._update), 313 timetype=ba.TimeType.REAL, 314 repeat=True, 315 ) 316 self._update() 317 318 def _update(self) -> None: 319 have = ba.app.accounts_v1.have_pro_options() 320 for lock in self._lock_images: 321 ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0) 322 323 def _back(self) -> None: 324 # pylint: disable=cyclic-import 325 from bastd.ui.playlist import browser 326 327 if self._selected_playlist_name is not None: 328 cfg = ba.app.config 329 cfg[ 330 self._pvars.config_name + ' Playlist Selection' 331 ] = self._selected_playlist_name 332 cfg.commit() 333 334 ba.containerwidget( 335 edit=self._root_widget, transition=self._transition_out 336 ) 337 ba.app.ui.set_main_menu_window( 338 browser.PlaylistBrowserWindow( 339 transition='in_left', sessiontype=self._sessiontype 340 ).get_root_widget() 341 ) 342 343 def _select(self, name: str, index: int) -> None: 344 self._selected_playlist_name = name 345 self._selected_playlist_index = index 346 347 def _run_selected_playlist(self) -> None: 348 # pylint: disable=cyclic-import 349 ba.internal.unlock_all_input() 350 try: 351 ba.internal.new_host_session(self._sessiontype) 352 except Exception: 353 from bastd import mainmenu 354 355 ba.print_exception(f'Error running session {self._sessiontype}.') 356 357 # Drop back into a main menu session. 358 ba.internal.new_host_session(mainmenu.MainMenuSession) 359 360 def _choose_playlist(self) -> None: 361 if self._selected_playlist_name is None: 362 return 363 self._save_playlist_selection() 364 ba.containerwidget(edit=self._root_widget, transition='out_left') 365 ba.internal.fade_screen(False, endcall=self._run_selected_playlist) 366 ba.internal.lock_all_input() 367 368 def _refresh(self, select_playlist: str | None = None) -> None: 369 from efro.util import asserttype 370 371 old_selection = self._selected_playlist_name 372 373 # If there was no prev selection, look in prefs. 374 if old_selection is None: 375 old_selection = ba.app.config.get( 376 self._pvars.config_name + ' Playlist Selection' 377 ) 378 379 old_selection_index = self._selected_playlist_index 380 381 # Delete old. 382 while self._playlist_widgets: 383 self._playlist_widgets.pop().delete() 384 385 items = list(ba.app.config[self._config_name_full].items()) 386 387 # Make sure everything is unicode now. 388 items = [ 389 (i[0].decode(), i[1]) if not isinstance(i[0], str) else i 390 for i in items 391 ] 392 393 items.sort(key=lambda x: asserttype(x[0], str).lower()) 394 395 items = [['__default__', None]] + items # Default is always first. 396 index = 0 397 for pname, _ in items: 398 assert pname is not None 399 txtw = ba.textwidget( 400 parent=self._columnwidget, 401 size=(self._width - 40, 30), 402 maxwidth=self._width - 110, 403 text=self._get_playlist_display_name(pname), 404 h_align='left', 405 v_align='center', 406 color=(0.6, 0.6, 0.7, 1.0) 407 if pname == '__default__' 408 else (0.85, 0.85, 0.85, 1), 409 always_highlight=True, 410 on_select_call=ba.Call(self._select, pname, index), 411 on_activate_call=ba.Call(self._edit_button.activate), 412 selectable=True, 413 ) 414 ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) 415 416 # Hitting up from top widget should jump to 'back' 417 if index == 0: 418 ba.widget(edit=txtw, up_widget=self._back_button) 419 420 self._playlist_widgets.append(txtw) 421 422 # Select this one if the user requested it. 423 if select_playlist is not None: 424 if pname == select_playlist: 425 ba.columnwidget( 426 edit=self._columnwidget, 427 selected_child=txtw, 428 visible_child=txtw, 429 ) 430 else: 431 # Select this one if it was previously selected. 432 # Go by index if there's one. 433 if old_selection_index is not None: 434 if index == old_selection_index: 435 ba.columnwidget( 436 edit=self._columnwidget, 437 selected_child=txtw, 438 visible_child=txtw, 439 ) 440 else: # Otherwise look by name. 441 if pname == old_selection: 442 ba.columnwidget( 443 edit=self._columnwidget, 444 selected_child=txtw, 445 visible_child=txtw, 446 ) 447 448 index += 1 449 450 def _save_playlist_selection(self) -> None: 451 # Store the selected playlist in prefs. 452 # This serves dual purposes of letting us re-select it next time 453 # if we want and also lets us pass it to the game (since we reset 454 # the whole python environment that's not actually easy). 455 cfg = ba.app.config 456 cfg[ 457 self._pvars.config_name + ' Playlist Selection' 458 ] = self._selected_playlist_name 459 cfg[ 460 self._pvars.config_name + ' Playlist Randomize' 461 ] = self._do_randomize_val 462 cfg.commit() 463 464 def _new_playlist(self) -> None: 465 # pylint: disable=cyclic-import 466 from bastd.ui.playlist.editcontroller import PlaylistEditController 467 from bastd.ui.purchase import PurchaseWindow 468 469 if not ba.app.accounts_v1.have_pro_options(): 470 PurchaseWindow(items=['pro']) 471 return 472 473 # Clamp at our max playlist number. 474 if len(ba.app.config[self._config_name_full]) > self._max_playlists: 475 ba.screenmessage( 476 ba.Lstr( 477 translate=( 478 'serverResponses', 479 'Max number of playlists reached.', 480 ) 481 ), 482 color=(1, 0, 0), 483 ) 484 ba.playsound(ba.getsound('error')) 485 return 486 487 # In case they cancel so we can return to this state. 488 self._save_playlist_selection() 489 490 # Kick off the edit UI. 491 PlaylistEditController(sessiontype=self._sessiontype) 492 ba.containerwidget(edit=self._root_widget, transition='out_left') 493 494 def _edit_playlist(self) -> None: 495 # pylint: disable=cyclic-import 496 from bastd.ui.playlist.editcontroller import PlaylistEditController 497 from bastd.ui.purchase import PurchaseWindow 498 499 if not ba.app.accounts_v1.have_pro_options(): 500 PurchaseWindow(items=['pro']) 501 return 502 if self._selected_playlist_name is None: 503 return 504 if self._selected_playlist_name == '__default__': 505 ba.playsound(ba.getsound('error')) 506 ba.screenmessage(ba.Lstr(resource=self._r + '.cantEditDefaultText')) 507 return 508 self._save_playlist_selection() 509 PlaylistEditController( 510 existing_playlist_name=self._selected_playlist_name, 511 sessiontype=self._sessiontype, 512 ) 513 ba.containerwidget(edit=self._root_widget, transition='out_left') 514 515 def _do_delete_playlist(self) -> None: 516 ba.internal.add_transaction( 517 { 518 'type': 'REMOVE_PLAYLIST', 519 'playlistType': self._pvars.config_name, 520 'playlistName': self._selected_playlist_name, 521 } 522 ) 523 ba.internal.run_transactions() 524 ba.playsound(ba.getsound('shieldDown')) 525 526 # (we don't use len()-1 here because the default list adds one) 527 assert self._selected_playlist_index is not None 528 if self._selected_playlist_index > len( 529 ba.app.config[self._pvars.config_name + ' Playlists'] 530 ): 531 self._selected_playlist_index = len( 532 ba.app.config[self._pvars.config_name + ' Playlists'] 533 ) 534 self._refresh() 535 536 def _import_playlist(self) -> None: 537 # pylint: disable=cyclic-import 538 from bastd.ui.playlist import share 539 540 # Gotta be signed in for this to work. 541 if ba.internal.get_v1_account_state() != 'signed_in': 542 ba.screenmessage( 543 ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) 544 ) 545 ba.playsound(ba.getsound('error')) 546 return 547 548 share.SharePlaylistImportWindow( 549 origin_widget=self._import_button, 550 on_success_callback=ba.WeakCall(self._on_playlist_import_success), 551 ) 552 553 def _on_playlist_import_success(self) -> None: 554 self._refresh() 555 556 def _on_share_playlist_response(self, name: str, response: Any) -> None: 557 # pylint: disable=cyclic-import 558 from bastd.ui.playlist import share 559 560 if response is None: 561 ba.screenmessage( 562 ba.Lstr(resource='internal.unavailableNoConnectionText'), 563 color=(1, 0, 0), 564 ) 565 ba.playsound(ba.getsound('error')) 566 return 567 share.SharePlaylistResultsWindow(name, response) 568 569 def _share_playlist(self) -> None: 570 # pylint: disable=cyclic-import 571 from bastd.ui.purchase import PurchaseWindow 572 573 if not ba.app.accounts_v1.have_pro_options(): 574 PurchaseWindow(items=['pro']) 575 return 576 577 # Gotta be signed in for this to work. 578 if ba.internal.get_v1_account_state() != 'signed_in': 579 ba.screenmessage( 580 ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) 581 ) 582 ba.playsound(ba.getsound('error')) 583 return 584 if self._selected_playlist_name == '__default__': 585 ba.playsound(ba.getsound('error')) 586 ba.screenmessage( 587 ba.Lstr(resource=self._r + '.cantShareDefaultText'), 588 color=(1, 0, 0), 589 ) 590 return 591 592 if self._selected_playlist_name is None: 593 return 594 595 ba.internal.add_transaction( 596 { 597 'type': 'SHARE_PLAYLIST', 598 'expire_time': time.time() + 5, 599 'playlistType': self._pvars.config_name, 600 'playlistName': self._selected_playlist_name, 601 }, 602 callback=ba.WeakCall( 603 self._on_share_playlist_response, self._selected_playlist_name 604 ), 605 ) 606 ba.internal.run_transactions() 607 ba.screenmessage(ba.Lstr(resource='sharingText')) 608 609 def _delete_playlist(self) -> None: 610 # pylint: disable=cyclic-import 611 from bastd.ui.purchase import PurchaseWindow 612 from bastd.ui.confirm import ConfirmWindow 613 614 if not ba.app.accounts_v1.have_pro_options(): 615 PurchaseWindow(items=['pro']) 616 return 617 618 if self._selected_playlist_name is None: 619 return 620 if self._selected_playlist_name == '__default__': 621 ba.playsound(ba.getsound('error')) 622 ba.screenmessage( 623 ba.Lstr(resource=self._r + '.cantDeleteDefaultText') 624 ) 625 else: 626 ConfirmWindow( 627 ba.Lstr( 628 resource=self._r + '.deleteConfirmText', 629 subs=[('${LIST}', self._selected_playlist_name)], 630 ), 631 self._do_delete_playlist, 632 450, 633 150, 634 ) 635 636 def _get_playlist_display_name(self, playlist: str) -> ba.Lstr: 637 if playlist == '__default__': 638 return self._pvars.default_list_name 639 return ( 640 playlist 641 if isinstance(playlist, ba.Lstr) 642 else ba.Lstr(value=playlist) 643 ) 644 645 def _duplicate_playlist(self) -> None: 646 # pylint: disable=too-many-branches 647 # pylint: disable=cyclic-import 648 from bastd.ui.purchase import PurchaseWindow 649 650 if not ba.app.accounts_v1.have_pro_options(): 651 PurchaseWindow(items=['pro']) 652 return 653 if self._selected_playlist_name is None: 654 return 655 plst: list[dict[str, Any]] | None 656 if self._selected_playlist_name == '__default__': 657 plst = self._pvars.get_default_list_call() 658 else: 659 plst = ba.app.config[self._config_name_full].get( 660 self._selected_playlist_name 661 ) 662 if plst is None: 663 ba.playsound(ba.getsound('error')) 664 return 665 666 # clamp at our max playlist number 667 if len(ba.app.config[self._config_name_full]) > self._max_playlists: 668 ba.screenmessage( 669 ba.Lstr( 670 translate=( 671 'serverResponses', 672 'Max number of playlists reached.', 673 ) 674 ), 675 color=(1, 0, 0), 676 ) 677 ba.playsound(ba.getsound('error')) 678 return 679 680 copy_text = ba.Lstr(resource='copyOfText').evaluate() 681 # get just 'Copy' or whatnot 682 copy_word = copy_text.replace('${NAME}', '').strip() 683 # find a valid dup name that doesn't exist 684 685 test_index = 1 686 base_name = self._get_playlist_display_name( 687 self._selected_playlist_name 688 ).evaluate() 689 690 # If it looks like a copy, strip digits and spaces off the end. 691 if copy_word in base_name: 692 while base_name[-1].isdigit() or base_name[-1] == ' ': 693 base_name = base_name[:-1] 694 while True: 695 if copy_word in base_name: 696 test_name = base_name 697 else: 698 test_name = copy_text.replace('${NAME}', base_name) 699 if test_index > 1: 700 test_name += ' ' + str(test_index) 701 if test_name not in ba.app.config[self._config_name_full]: 702 break 703 test_index += 1 704 705 ba.internal.add_transaction( 706 { 707 'type': 'ADD_PLAYLIST', 708 'playlistType': self._pvars.config_name, 709 'playlistName': test_name, 710 'playlist': copy.deepcopy(plst), 711 } 712 ) 713 ba.internal.run_transactions() 714 715 ba.playsound(ba.getsound('gunCocking')) 716 self._refresh(select_playlist=test_name)
Window for viewing a playlist.
PlaylistCustomizeBrowserWindow( sessiontype: type[ba._session.Session], transition: str = 'in_right', select_playlist: str | None = None, origin_widget: _ba.Widget | None = None)
22 def __init__( 23 self, 24 sessiontype: type[ba.Session], 25 transition: str = 'in_right', 26 select_playlist: str | None = None, 27 origin_widget: ba.Widget | None = None, 28 ): 29 # Yes this needs tidying. 30 # pylint: disable=too-many-locals 31 # pylint: disable=too-many-statements 32 # pylint: disable=cyclic-import 33 from bastd.ui import playlist 34 35 scale_origin: tuple[float, float] | None 36 if origin_widget is not None: 37 self._transition_out = 'out_scale' 38 scale_origin = origin_widget.get_screen_space_center() 39 transition = 'in_scale' 40 else: 41 self._transition_out = 'out_right' 42 scale_origin = None 43 44 self._sessiontype = sessiontype 45 self._pvars = playlist.PlaylistTypeVars(sessiontype) 46 self._max_playlists = 30 47 self._r = 'gameListWindow' 48 uiscale = ba.app.ui.uiscale 49 self._width = 750.0 if uiscale is ba.UIScale.SMALL else 650.0 50 x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 51 self._height = ( 52 380.0 53 if uiscale is ba.UIScale.SMALL 54 else 420.0 55 if uiscale is ba.UIScale.MEDIUM 56 else 500.0 57 ) 58 top_extra = 20.0 if uiscale is ba.UIScale.SMALL else 0.0 59 60 super().__init__( 61 root_widget=ba.containerwidget( 62 size=(self._width, self._height + top_extra), 63 transition=transition, 64 scale_origin_stack_offset=scale_origin, 65 scale=( 66 2.05 67 if uiscale is ba.UIScale.SMALL 68 else 1.5 69 if uiscale is ba.UIScale.MEDIUM 70 else 1.0 71 ), 72 stack_offset=(0, -10) 73 if uiscale is ba.UIScale.SMALL 74 else (0, 0), 75 ) 76 ) 77 78 self._back_button = back_button = btn = ba.buttonwidget( 79 parent=self._root_widget, 80 position=(43 + x_inset, self._height - 60), 81 size=(160, 68), 82 scale=0.77, 83 autoselect=True, 84 text_scale=1.3, 85 label=ba.Lstr(resource='backText'), 86 button_type='back', 87 ) 88 89 ba.textwidget( 90 parent=self._root_widget, 91 position=(0, self._height - 47), 92 size=(self._width, 25), 93 text=ba.Lstr( 94 resource=self._r + '.titleText', 95 subs=[('${TYPE}', self._pvars.window_title_name)], 96 ), 97 color=ba.app.ui.heading_color, 98 maxwidth=290, 99 h_align='center', 100 v_align='center', 101 ) 102 103 ba.buttonwidget( 104 edit=btn, 105 button_type='backSmall', 106 size=(60, 60), 107 label=ba.charstr(ba.SpecialChar.BACK), 108 ) 109 110 v = self._height - 59.0 111 h = 41 + x_inset 112 b_color = (0.6, 0.53, 0.63) 113 b_textcolor = (0.75, 0.7, 0.8) 114 self._lock_images: list[ba.Widget] = [] 115 lock_tex = ba.gettexture('lock') 116 117 scl = ( 118 1.1 119 if uiscale is ba.UIScale.SMALL 120 else 1.27 121 if uiscale is ba.UIScale.MEDIUM 122 else 1.57 123 ) 124 scl *= 0.63 125 v -= 65.0 * scl 126 new_button = btn = ba.buttonwidget( 127 parent=self._root_widget, 128 position=(h, v), 129 size=(90, 58.0 * scl), 130 on_activate_call=self._new_playlist, 131 color=b_color, 132 autoselect=True, 133 button_type='square', 134 textcolor=b_textcolor, 135 text_scale=0.7, 136 label=ba.Lstr( 137 resource='newText', fallback_resource=self._r + '.newText' 138 ), 139 ) 140 self._lock_images.append( 141 ba.imagewidget( 142 parent=self._root_widget, 143 size=(30, 30), 144 draw_controller=btn, 145 position=(h - 10, v + 58.0 * scl - 28), 146 texture=lock_tex, 147 ) 148 ) 149 150 v -= 65.0 * scl 151 self._edit_button = edit_button = btn = ba.buttonwidget( 152 parent=self._root_widget, 153 position=(h, v), 154 size=(90, 58.0 * scl), 155 on_activate_call=self._edit_playlist, 156 color=b_color, 157 autoselect=True, 158 textcolor=b_textcolor, 159 button_type='square', 160 text_scale=0.7, 161 label=ba.Lstr( 162 resource='editText', fallback_resource=self._r + '.editText' 163 ), 164 ) 165 self._lock_images.append( 166 ba.imagewidget( 167 parent=self._root_widget, 168 size=(30, 30), 169 draw_controller=btn, 170 position=(h - 10, v + 58.0 * scl - 28), 171 texture=lock_tex, 172 ) 173 ) 174 175 v -= 65.0 * scl 176 duplicate_button = btn = ba.buttonwidget( 177 parent=self._root_widget, 178 position=(h, v), 179 size=(90, 58.0 * scl), 180 on_activate_call=self._duplicate_playlist, 181 color=b_color, 182 autoselect=True, 183 textcolor=b_textcolor, 184 button_type='square', 185 text_scale=0.7, 186 label=ba.Lstr( 187 resource='duplicateText', 188 fallback_resource=self._r + '.duplicateText', 189 ), 190 ) 191 self._lock_images.append( 192 ba.imagewidget( 193 parent=self._root_widget, 194 size=(30, 30), 195 draw_controller=btn, 196 position=(h - 10, v + 58.0 * scl - 28), 197 texture=lock_tex, 198 ) 199 ) 200 201 v -= 65.0 * scl 202 delete_button = btn = ba.buttonwidget( 203 parent=self._root_widget, 204 position=(h, v), 205 size=(90, 58.0 * scl), 206 on_activate_call=self._delete_playlist, 207 color=b_color, 208 autoselect=True, 209 textcolor=b_textcolor, 210 button_type='square', 211 text_scale=0.7, 212 label=ba.Lstr( 213 resource='deleteText', fallback_resource=self._r + '.deleteText' 214 ), 215 ) 216 self._lock_images.append( 217 ba.imagewidget( 218 parent=self._root_widget, 219 size=(30, 30), 220 draw_controller=btn, 221 position=(h - 10, v + 58.0 * scl - 28), 222 texture=lock_tex, 223 ) 224 ) 225 v -= 65.0 * scl 226 self._import_button = ba.buttonwidget( 227 parent=self._root_widget, 228 position=(h, v), 229 size=(90, 58.0 * scl), 230 on_activate_call=self._import_playlist, 231 color=b_color, 232 autoselect=True, 233 textcolor=b_textcolor, 234 button_type='square', 235 text_scale=0.7, 236 label=ba.Lstr(resource='importText'), 237 ) 238 v -= 65.0 * scl 239 btn = ba.buttonwidget( 240 parent=self._root_widget, 241 position=(h, v), 242 size=(90, 58.0 * scl), 243 on_activate_call=self._share_playlist, 244 color=b_color, 245 autoselect=True, 246 textcolor=b_textcolor, 247 button_type='square', 248 text_scale=0.7, 249 label=ba.Lstr(resource='shareText'), 250 ) 251 self._lock_images.append( 252 ba.imagewidget( 253 parent=self._root_widget, 254 size=(30, 30), 255 draw_controller=btn, 256 position=(h - 10, v + 58.0 * scl - 28), 257 texture=lock_tex, 258 ) 259 ) 260 261 v = self._height - 75 262 self._scroll_height = self._height - 119 263 scrollwidget = ba.scrollwidget( 264 parent=self._root_widget, 265 position=(140 + x_inset, v - self._scroll_height), 266 size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10), 267 highlight=False, 268 ) 269 ba.widget(edit=back_button, right_widget=scrollwidget) 270 self._columnwidget = ba.columnwidget( 271 parent=scrollwidget, border=2, margin=0 272 ) 273 274 h = 145 275 276 self._do_randomize_val = ba.app.config.get( 277 self._pvars.config_name + ' Playlist Randomize', 0 278 ) 279 280 h += 210 281 282 for btn in [new_button, delete_button, edit_button, duplicate_button]: 283 ba.widget(edit=btn, right_widget=scrollwidget) 284 ba.widget( 285 edit=scrollwidget, 286 left_widget=new_button, 287 right_widget=ba.internal.get_special_widget('party_button') 288 if ba.app.ui.use_toolbars 289 else None, 290 ) 291 292 # make sure config exists 293 self._config_name_full = self._pvars.config_name + ' Playlists' 294 295 if self._config_name_full not in ba.app.config: 296 ba.app.config[self._config_name_full] = {} 297 298 self._selected_playlist_name: str | None = None 299 self._selected_playlist_index: int | None = None 300 self._playlist_widgets: list[ba.Widget] = [] 301 302 self._refresh(select_playlist=select_playlist) 303 304 ba.buttonwidget(edit=back_button, on_activate_call=self._back) 305 ba.containerwidget(edit=self._root_widget, cancel_button=back_button) 306 307 ba.containerwidget(edit=self._root_widget, selected_child=scrollwidget) 308 309 # Keep our lock images up to date/etc. 310 self._update_timer = ba.Timer( 311 1.0, 312 ba.WeakCall(self._update), 313 timetype=ba.TimeType.REAL, 314 repeat=True, 315 ) 316 self._update()
Inherited Members
- ba.ui.Window
- get_root_widget