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 ) 278 if self._back_button is not None: 279 bui.widget(edit=self._back_button, right_widget=scrollwidget) 280 281 self._columnwidget = bui.columnwidget( 282 parent=scrollwidget, border=2, margin=0 283 ) 284 285 h = 145 286 287 self._do_randomize_val = bui.app.config.get( 288 self._pvars.config_name + ' Playlist Randomize', 0 289 ) 290 291 h += 210 292 293 for btn in [new_button, delete_button, edit_button, duplicate_button]: 294 bui.widget(edit=btn, right_widget=scrollwidget) 295 bui.widget( 296 edit=scrollwidget, 297 left_widget=new_button, 298 right_widget=bui.get_special_widget('squad_button'), 299 ) 300 301 # Make sure config exists. 302 self._config_name_full = f'{self._pvars.config_name} Playlists' 303 304 if self._config_name_full not in bui.app.config: 305 bui.app.config[self._config_name_full] = {} 306 307 self._selected_playlist_name: str | None = None 308 self._selected_playlist_index: int | None = None 309 self._playlist_widgets: list[bui.Widget] = [] 310 311 self._refresh(select_playlist=select_playlist) 312 313 if self._back_button is not None: 314 bui.buttonwidget( 315 edit=self._back_button, on_activate_call=self.main_window_back 316 ) 317 bui.containerwidget( 318 edit=self._root_widget, cancel_button=self._back_button 319 ) 320 321 bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget) 322 323 # Keep our lock images up to date/etc. 324 self._update_timer = bui.AppTimer( 325 1.0, bui.WeakCall(self._update), repeat=True 326 ) 327 self._update() 328 329 @override 330 def get_main_window_state(self) -> bui.MainWindowState: 331 # Support recreating our window for back/refresh purposes. 332 cls = type(self) 333 334 # Avoid dereferencing self within the lambda or we'll keep 335 # ourself alive indefinitely. 336 stype = self._sessiontype 337 338 return bui.BasicMainWindowState( 339 create_call=lambda transition, origin_widget: cls( 340 transition=transition, 341 origin_widget=origin_widget, 342 sessiontype=stype, 343 ) 344 ) 345 346 @override 347 def on_main_window_close(self) -> None: 348 if self._selected_playlist_name is not None: 349 cfg = bui.app.config 350 cfg[f'{self._pvars.config_name} Playlist Selection'] = ( 351 self._selected_playlist_name 352 ) 353 cfg.commit() 354 355 def _update(self) -> None: 356 assert bui.app.classic is not None 357 have = bui.app.classic.accounts.have_pro_options() 358 for lock in self._lock_images: 359 bui.imagewidget( 360 edit=lock, opacity=0.0 if (have or not REQUIRE_PRO) else 1.0 361 ) 362 363 def _select(self, name: str, index: int) -> None: 364 self._selected_playlist_name = name 365 self._selected_playlist_index = index 366 367 def _refresh(self, select_playlist: str | None = None) -> None: 368 from efro.util import asserttype 369 370 old_selection = self._selected_playlist_name 371 372 # If there was no prev selection, look in prefs. 373 if old_selection is None: 374 old_selection = bui.app.config.get( 375 self._pvars.config_name + ' Playlist Selection' 376 ) 377 378 old_selection_index = self._selected_playlist_index 379 380 # Delete old. 381 while self._playlist_widgets: 382 self._playlist_widgets.pop().delete() 383 384 items = list(bui.app.config[self._config_name_full].items()) 385 386 # Make sure everything is unicode now. 387 items = [ 388 (i[0].decode(), i[1]) if not isinstance(i[0], str) else i 389 for i in items 390 ] 391 392 items.sort(key=lambda x: asserttype(x[0], str).lower()) 393 394 items = [['__default__', None]] + items # Default is always first. 395 index = 0 396 for pname, _ in items: 397 assert pname is not None 398 txtw = bui.textwidget( 399 parent=self._columnwidget, 400 size=(self._width - 40, 30), 401 maxwidth=440, 402 text=self._get_playlist_display_name(pname), 403 h_align='left', 404 v_align='center', 405 color=( 406 (0.6, 0.6, 0.7, 1.0) 407 if pname == '__default__' 408 else (0.85, 0.85, 0.85, 1) 409 ), 410 always_highlight=True, 411 on_select_call=bui.Call(self._select, pname, index), 412 on_activate_call=bui.Call(self._edit_button.activate), 413 selectable=True, 414 ) 415 bui.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) 416 417 # Hitting up from top widget should jump to 'back'. 418 if index == 0: 419 bui.widget( 420 edit=txtw, 421 up_widget=( 422 self._back_button 423 if self._back_button is not None 424 else bui.get_special_widget('back_button') 425 ), 426 ) 427 428 self._playlist_widgets.append(txtw) 429 430 # Select this one if the user requested it. 431 if select_playlist is not None: 432 if pname == select_playlist: 433 bui.columnwidget( 434 edit=self._columnwidget, 435 selected_child=txtw, 436 visible_child=txtw, 437 ) 438 else: 439 # Select this one if it was previously selected. Go by 440 # index if there's one. 441 if old_selection_index is not None: 442 if index == old_selection_index: 443 bui.columnwidget( 444 edit=self._columnwidget, 445 selected_child=txtw, 446 visible_child=txtw, 447 ) 448 else: # Otherwise look by name. 449 if pname == old_selection: 450 bui.columnwidget( 451 edit=self._columnwidget, 452 selected_child=txtw, 453 visible_child=txtw, 454 ) 455 456 index += 1 457 458 def _save_playlist_selection(self) -> None: 459 # Store the selected playlist in prefs. This serves dual 460 # purposes of letting us re-select it next time if we want and 461 # also lets us pass it to the game (since we reset the whole 462 # python environment that's not actually easy). 463 cfg = bui.app.config 464 cfg[self._pvars.config_name + ' Playlist Selection'] = ( 465 self._selected_playlist_name 466 ) 467 cfg[self._pvars.config_name + ' Playlist Randomize'] = ( 468 self._do_randomize_val 469 ) 470 cfg.commit() 471 472 def _new_playlist(self) -> None: 473 # pylint: disable=cyclic-import 474 from bauiv1lib.playlist.editcontroller import PlaylistEditController 475 from bauiv1lib.purchase import PurchaseWindow 476 477 # No-op if we're not in control. 478 if not self.main_window_has_control(): 479 return 480 481 assert bui.app.classic is not None 482 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 483 PurchaseWindow(items=['pro']) 484 return 485 486 # Clamp at our max playlist number. 487 if len(bui.app.config[self._config_name_full]) > self._max_playlists: 488 bui.screenmessage( 489 bui.Lstr( 490 translate=( 491 'serverResponses', 492 'Max number of playlists reached.', 493 ) 494 ), 495 color=(1, 0, 0), 496 ) 497 bui.getsound('error').play() 498 return 499 500 # In case they cancel so we can return to this state. 501 self._save_playlist_selection() 502 503 # Kick off the edit UI. 504 PlaylistEditController(sessiontype=self._sessiontype, from_window=self) 505 506 def _edit_playlist(self) -> None: 507 # pylint: disable=cyclic-import 508 from bauiv1lib.playlist.editcontroller import PlaylistEditController 509 from bauiv1lib.purchase import PurchaseWindow 510 511 assert bui.app.classic is not None 512 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 513 PurchaseWindow(items=['pro']) 514 return 515 if self._selected_playlist_name is None: 516 return 517 if self._selected_playlist_name == '__default__': 518 bui.getsound('error').play() 519 bui.screenmessage( 520 bui.Lstr(resource=f'{self._r}.cantEditDefaultText') 521 ) 522 return 523 self._save_playlist_selection() 524 PlaylistEditController( 525 existing_playlist_name=self._selected_playlist_name, 526 sessiontype=self._sessiontype, 527 from_window=self, 528 ) 529 530 def _do_delete_playlist(self) -> None: 531 plus = bui.app.plus 532 assert plus is not None 533 plus.add_v1_account_transaction( 534 { 535 'type': 'REMOVE_PLAYLIST', 536 'playlistType': self._pvars.config_name, 537 'playlistName': self._selected_playlist_name, 538 } 539 ) 540 plus.run_v1_account_transactions() 541 bui.getsound('shieldDown').play() 542 543 # (we don't use len()-1 here because the default list adds one) 544 assert self._selected_playlist_index is not None 545 self._selected_playlist_index = min( 546 self._selected_playlist_index, 547 len(bui.app.config[self._pvars.config_name + ' Playlists']), 548 ) 549 self._refresh() 550 551 def _import_playlist(self) -> None: 552 # pylint: disable=cyclic-import 553 from bauiv1lib.playlist import share 554 555 plus = bui.app.plus 556 assert plus is not None 557 558 # Gotta be signed in for this to work. 559 if plus.get_v1_account_state() != 'signed_in': 560 bui.screenmessage( 561 bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) 562 ) 563 bui.getsound('error').play() 564 return 565 566 share.SharePlaylistImportWindow( 567 origin_widget=self._import_button, 568 on_success_callback=bui.WeakCall(self._on_playlist_import_success), 569 ) 570 571 def _on_playlist_import_success(self) -> None: 572 self._refresh() 573 574 def _on_share_playlist_response(self, name: str, response: Any) -> None: 575 # pylint: disable=cyclic-import 576 from bauiv1lib.playlist import share 577 578 if response is None: 579 bui.screenmessage( 580 bui.Lstr(resource='internal.unavailableNoConnectionText'), 581 color=(1, 0, 0), 582 ) 583 bui.getsound('error').play() 584 return 585 share.SharePlaylistResultsWindow(name, response) 586 587 def _share_playlist(self) -> None: 588 # pylint: disable=cyclic-import 589 from bauiv1lib.purchase import PurchaseWindow 590 591 plus = bui.app.plus 592 assert plus is not None 593 594 assert bui.app.classic is not None 595 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 596 PurchaseWindow(items=['pro']) 597 return 598 599 # Gotta be signed in for this to work. 600 if plus.get_v1_account_state() != 'signed_in': 601 bui.screenmessage( 602 bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) 603 ) 604 bui.getsound('error').play() 605 return 606 if self._selected_playlist_name == '__default__': 607 bui.getsound('error').play() 608 bui.screenmessage( 609 bui.Lstr(resource=f'{self._r}.cantShareDefaultText'), 610 color=(1, 0, 0), 611 ) 612 return 613 614 if self._selected_playlist_name is None: 615 return 616 617 plus.add_v1_account_transaction( 618 { 619 'type': 'SHARE_PLAYLIST', 620 'expire_time': time.time() + 5, 621 'playlistType': self._pvars.config_name, 622 'playlistName': self._selected_playlist_name, 623 }, 624 callback=bui.WeakCall( 625 self._on_share_playlist_response, self._selected_playlist_name 626 ), 627 ) 628 plus.run_v1_account_transactions() 629 bui.screenmessage(bui.Lstr(resource='sharingText')) 630 631 def _delete_playlist(self) -> None: 632 # pylint: disable=cyclic-import 633 from bauiv1lib.purchase import PurchaseWindow 634 from bauiv1lib.confirm import ConfirmWindow 635 636 assert bui.app.classic is not None 637 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 638 PurchaseWindow(items=['pro']) 639 return 640 641 if self._selected_playlist_name is None: 642 return 643 if self._selected_playlist_name == '__default__': 644 bui.getsound('error').play() 645 bui.screenmessage( 646 bui.Lstr(resource=f'{self._r}.cantDeleteDefaultText') 647 ) 648 else: 649 ConfirmWindow( 650 bui.Lstr( 651 resource=f'{self._r}.deleteConfirmText', 652 subs=[('${LIST}', self._selected_playlist_name)], 653 ), 654 self._do_delete_playlist, 655 450, 656 150, 657 ) 658 659 def _get_playlist_display_name(self, playlist: str) -> bui.Lstr: 660 if playlist == '__default__': 661 return self._pvars.default_list_name 662 return ( 663 playlist 664 if isinstance(playlist, bui.Lstr) 665 else bui.Lstr(value=playlist) 666 ) 667 668 def _duplicate_playlist(self) -> None: 669 # pylint: disable=too-many-branches 670 # pylint: disable=cyclic-import 671 from bauiv1lib.purchase import PurchaseWindow 672 673 plus = bui.app.plus 674 assert plus is not None 675 676 assert bui.app.classic is not None 677 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 678 PurchaseWindow(items=['pro']) 679 return 680 if self._selected_playlist_name is None: 681 return 682 plst: list[dict[str, Any]] | None 683 if self._selected_playlist_name == '__default__': 684 plst = self._pvars.get_default_list_call() 685 else: 686 plst = bui.app.config[self._config_name_full].get( 687 self._selected_playlist_name 688 ) 689 if plst is None: 690 bui.getsound('error').play() 691 return 692 693 # Clamp at our max playlist number. 694 if len(bui.app.config[self._config_name_full]) > self._max_playlists: 695 bui.screenmessage( 696 bui.Lstr( 697 translate=( 698 'serverResponses', 699 'Max number of playlists reached.', 700 ) 701 ), 702 color=(1, 0, 0), 703 ) 704 bui.getsound('error').play() 705 return 706 707 copy_text = bui.Lstr(resource='copyOfText').evaluate() 708 709 # Get just 'Copy' or whatnot. 710 copy_word = copy_text.replace('${NAME}', '').strip() 711 712 # Find a valid dup name that doesn't exist. 713 test_index = 1 714 base_name = self._get_playlist_display_name( 715 self._selected_playlist_name 716 ).evaluate() 717 718 # If it looks like a copy, strip digits and spaces off the end. 719 if copy_word in base_name: 720 while base_name[-1].isdigit() or base_name[-1] == ' ': 721 base_name = base_name[:-1] 722 while True: 723 if copy_word in base_name: 724 test_name = base_name 725 else: 726 test_name = copy_text.replace('${NAME}', base_name) 727 if test_index > 1: 728 test_name += ' ' + str(test_index) 729 if test_name not in bui.app.config[self._config_name_full]: 730 break 731 test_index += 1 732 733 plus.add_v1_account_transaction( 734 { 735 'type': 'ADD_PLAYLIST', 736 'playlistType': self._pvars.config_name, 737 'playlistName': test_name, 738 'playlist': copy.deepcopy(plst), 739 } 740 ) 741 plus.run_v1_account_transactions() 742 743 bui.getsound('gunCocking').play() 744 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 ) 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)
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 ) 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()
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.
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 )
Return a WindowState to recreate this window, if supported.
@override
def
on_main_window_close(self) -> None:
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()
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_is_auxiliary
- main_window_close
- main_window_has_control
- main_window_back
- main_window_replace
- bauiv1._uitypes.Window
- get_root_widget