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