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