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