bauiv1lib.playlist.browser
Provides a window for browsing and launching game playlists.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides a window for browsing and launching game playlists.""" 4 5from __future__ import annotations 6 7import copy 8import math 9import logging 10from typing import override, TYPE_CHECKING 11 12import bascenev1 as bs 13import bauiv1 as bui 14 15if TYPE_CHECKING: 16 from bauiv1lib.play import PlaylistSelectContext 17 18 19class PlaylistBrowserWindow(bui.MainWindow): 20 """Window for starting teams games.""" 21 22 def __init__( 23 self, 24 sessiontype: type[bs.Session], 25 transition: str | None = 'in_right', 26 origin_widget: bui.Widget | None = None, 27 playlist_select_context: PlaylistSelectContext | None = None, 28 ): 29 # pylint: disable=cyclic-import 30 from bauiv1lib.playlist import PlaylistTypeVars 31 32 # Store state for when we exit the next game. 33 if issubclass(sessiontype, bs.DualTeamSession): 34 bui.set_analytics_screen('Teams Window') 35 elif issubclass(sessiontype, bs.FreeForAllSession): 36 bui.set_analytics_screen('FreeForAll Window') 37 else: 38 raise TypeError(f'Invalid sessiontype: {sessiontype}.') 39 self._pvars = PlaylistTypeVars(sessiontype) 40 41 self._sessiontype = sessiontype 42 43 self._customize_button: bui.Widget | None = None 44 self._sub_width: float | None = None 45 self._sub_height: float | None = None 46 self._playlist_select_context = playlist_select_context 47 48 self._ensure_standard_playlists_exist() 49 50 # Get the current selection (if any). 51 self._selected_playlist = bui.app.config.get( 52 self._pvars.config_name + ' Playlist Selection' 53 ) 54 55 uiscale = bui.app.ui_v1.uiscale 56 self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0 57 x_inset = 150 if uiscale is bui.UIScale.SMALL else 0 58 self._height = ( 59 440 60 if uiscale is bui.UIScale.SMALL 61 else 510 if uiscale is bui.UIScale.MEDIUM else 580 62 ) 63 64 top_extra = 30 if uiscale is bui.UIScale.SMALL else 0 65 66 super().__init__( 67 root_widget=bui.containerwidget( 68 size=(self._width, self._height + top_extra), 69 toolbar_visibility=( 70 'menu_minimal' 71 if ( 72 uiscale is bui.UIScale.SMALL 73 or playlist_select_context is not None 74 ) 75 else 'menu_full' 76 ), 77 scale=( 78 1.83 79 if uiscale is bui.UIScale.SMALL 80 else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9 81 ), 82 stack_offset=( 83 (0, -46) if uiscale is bui.UIScale.SMALL else (0, 0) 84 ), 85 ), 86 transition=transition, 87 origin_widget=origin_widget, 88 ) 89 90 self._back_button: bui.Widget | None = bui.buttonwidget( 91 parent=self._root_widget, 92 position=(59 + x_inset, self._height - 70), 93 size=(120, 60), 94 scale=1.0, 95 on_activate_call=self._on_back_press, 96 autoselect=True, 97 label=bui.Lstr(resource='backText'), 98 button_type='back', 99 ) 100 bui.containerwidget( 101 edit=self._root_widget, cancel_button=self._back_button 102 ) 103 self._title_text = bui.textwidget( 104 parent=self._root_widget, 105 position=( 106 self._width * 0.5, 107 self._height - (32 if uiscale is bui.UIScale.SMALL else 41), 108 ), 109 size=(0, 0), 110 text=self._pvars.window_title_name, 111 scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3), 112 res_scale=1.5, 113 color=bui.app.ui_v1.heading_color, 114 h_align='center', 115 v_align='center', 116 ) 117 118 bui.buttonwidget( 119 edit=self._back_button, 120 button_type='backSmall', 121 size=(60, 54), 122 position=(59 + x_inset, self._height - 67), 123 label=bui.charstr(bui.SpecialChar.BACK), 124 ) 125 126 if uiscale is bui.UIScale.SMALL: 127 self._back_button.delete() 128 self._back_button = None 129 bui.containerwidget( 130 edit=self._root_widget, on_cancel_call=self._on_back_press 131 ) 132 scroll_offs = 33 133 else: 134 scroll_offs = 0 135 self._scroll_width = self._width - (100 + 2 * x_inset) 136 self._scroll_height = self._height - ( 137 146 if uiscale is bui.UIScale.SMALL else 136 138 ) 139 self._scrollwidget = bui.scrollwidget( 140 parent=self._root_widget, 141 highlight=False, 142 size=(self._scroll_width, self._scroll_height), 143 position=( 144 (self._width - self._scroll_width) * 0.5, 145 65 + scroll_offs, 146 ), 147 ) 148 bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) 149 self._subcontainer: bui.Widget | None = None 150 self._config_name_full = self._pvars.config_name + ' Playlists' 151 self._last_config = None 152 153 # Update now and once per second (this should do our initial 154 # refresh). 155 self._update() 156 self._update_timer = bui.AppTimer( 157 1.0, bui.WeakCall(self._update), repeat=True 158 ) 159 160 @override 161 def get_main_window_state(self) -> bui.MainWindowState: 162 # Support recreating our window for back/refresh purposes. 163 cls = type(self) 164 165 # Pull things out of self here; if we do it below in the lambda 166 # then we keep self alive. 167 sessiontype = self._sessiontype 168 169 # Pull anything out of self here; if we do it in the lambda 170 # we'll inadvertanly keep self alive. 171 playlist_select_context = self._playlist_select_context 172 173 return bui.BasicMainWindowState( 174 create_call=lambda transition, origin_widget: cls( 175 transition=transition, 176 origin_widget=origin_widget, 177 sessiontype=sessiontype, 178 playlist_select_context=playlist_select_context, 179 ) 180 ) 181 182 @override 183 def on_main_window_close(self) -> None: 184 self._save_state() 185 186 def _ensure_standard_playlists_exist(self) -> None: 187 plus = bui.app.plus 188 assert plus is not None 189 190 # On new installations, go ahead and create a few playlists 191 # besides the hard-coded default one: 192 if not plus.get_v1_account_misc_val('madeStandardPlaylists', False): 193 plus.add_v1_account_transaction( 194 { 195 'type': 'ADD_PLAYLIST', 196 'playlistType': 'Free-for-All', 197 'playlistName': bui.Lstr( 198 resource='singleGamePlaylistNameText' 199 ) 200 .evaluate() 201 .replace( 202 '${GAME}', 203 bui.Lstr( 204 translate=('gameNames', 'Death Match') 205 ).evaluate(), 206 ), 207 'playlist': [ 208 { 209 'type': 'bs_death_match.DeathMatchGame', 210 'settings': { 211 'Epic Mode': False, 212 'Kills to Win Per Player': 10, 213 'Respawn Times': 1.0, 214 'Time Limit': 300, 215 'map': 'Doom Shroom', 216 }, 217 }, 218 { 219 'type': 'bs_death_match.DeathMatchGame', 220 'settings': { 221 'Epic Mode': False, 222 'Kills to Win Per Player': 10, 223 'Respawn Times': 1.0, 224 'Time Limit': 300, 225 'map': 'Crag Castle', 226 }, 227 }, 228 ], 229 } 230 ) 231 plus.add_v1_account_transaction( 232 { 233 'type': 'ADD_PLAYLIST', 234 'playlistType': 'Team Tournament', 235 'playlistName': bui.Lstr( 236 resource='singleGamePlaylistNameText' 237 ) 238 .evaluate() 239 .replace( 240 '${GAME}', 241 bui.Lstr( 242 translate=('gameNames', 'Capture the Flag') 243 ).evaluate(), 244 ), 245 'playlist': [ 246 { 247 'type': 'bs_capture_the_flag.CTFGame', 248 'settings': { 249 'map': 'Bridgit', 250 'Score to Win': 3, 251 'Flag Idle Return Time': 30, 252 'Flag Touch Return Time': 0, 253 'Respawn Times': 1.0, 254 'Time Limit': 600, 255 'Epic Mode': False, 256 }, 257 }, 258 { 259 'type': 'bs_capture_the_flag.CTFGame', 260 'settings': { 261 'map': 'Roundabout', 262 'Score to Win': 2, 263 'Flag Idle Return Time': 30, 264 'Flag Touch Return Time': 0, 265 'Respawn Times': 1.0, 266 'Time Limit': 600, 267 'Epic Mode': False, 268 }, 269 }, 270 { 271 'type': 'bs_capture_the_flag.CTFGame', 272 'settings': { 273 'map': 'Tip Top', 274 'Score to Win': 2, 275 'Flag Idle Return Time': 30, 276 'Flag Touch Return Time': 3, 277 'Respawn Times': 1.0, 278 'Time Limit': 300, 279 'Epic Mode': False, 280 }, 281 }, 282 ], 283 } 284 ) 285 plus.add_v1_account_transaction( 286 { 287 'type': 'ADD_PLAYLIST', 288 'playlistType': 'Team Tournament', 289 'playlistName': bui.Lstr( 290 translate=('playlistNames', 'Just Sports') 291 ).evaluate(), 292 'playlist': [ 293 { 294 'type': 'bs_hockey.HockeyGame', 295 'settings': { 296 'Time Limit': 0, 297 'map': 'Hockey Stadium', 298 'Score to Win': 1, 299 'Respawn Times': 1.0, 300 }, 301 }, 302 { 303 'type': 'bs_football.FootballTeamGame', 304 'settings': { 305 'Time Limit': 0, 306 'map': 'Football Stadium', 307 'Score to Win': 21, 308 'Respawn Times': 1.0, 309 }, 310 }, 311 ], 312 } 313 ) 314 plus.add_v1_account_transaction( 315 { 316 'type': 'ADD_PLAYLIST', 317 'playlistType': 'Free-for-All', 318 'playlistName': bui.Lstr( 319 translate=('playlistNames', 'Just Epic') 320 ).evaluate(), 321 'playlist': [ 322 { 323 'type': 'bs_elimination.EliminationGame', 324 'settings': { 325 'Time Limit': 120, 326 'map': 'Tip Top', 327 'Respawn Times': 1.0, 328 'Lives Per Player': 1, 329 'Epic Mode': 1, 330 }, 331 } 332 ], 333 } 334 ) 335 plus.add_v1_account_transaction( 336 { 337 'type': 'SET_MISC_VAL', 338 'name': 'madeStandardPlaylists', 339 'value': True, 340 } 341 ) 342 plus.run_v1_account_transactions() 343 344 def _refresh(self) -> None: 345 # FIXME: Should tidy this up. 346 # pylint: disable=too-many-statements 347 # pylint: disable=too-many-branches 348 # pylint: disable=too-many-locals 349 # pylint: disable=too-many-nested-blocks 350 from efro.util import asserttype 351 from bascenev1 import get_map_class, filter_playlist 352 353 if not self._root_widget: 354 return 355 if self._subcontainer is not None: 356 self._save_state() 357 self._subcontainer.delete() 358 359 # Make sure config exists. 360 if self._config_name_full not in bui.app.config: 361 bui.app.config[self._config_name_full] = {} 362 363 items = list(bui.app.config[self._config_name_full].items()) 364 365 # Make sure everything is unicode. 366 items = [ 367 (i[0].decode(), i[1]) if not isinstance(i[0], str) else i 368 for i in items 369 ] 370 371 items.sort(key=lambda x2: asserttype(x2[0], str).lower()) 372 items = [['__default__', None]] + items # default is always first 373 374 count = len(items) 375 columns = 3 376 rows = int(math.ceil(float(count) / columns)) 377 button_width = 230 378 button_height = 230 379 button_buffer_h = -3 380 button_buffer_v = 0 381 382 self._sub_width = self._scroll_width 383 self._sub_height = ( 384 40.0 + rows * (button_height + 2 * button_buffer_v) + 90 385 ) 386 assert self._sub_width is not None 387 assert self._sub_height is not None 388 self._subcontainer = bui.containerwidget( 389 parent=self._scrollwidget, 390 size=(self._sub_width, self._sub_height), 391 background=False, 392 ) 393 394 children = self._subcontainer.get_children() 395 for child in children: 396 child.delete() 397 398 assert bui.app.classic is not None 399 bui.textwidget( 400 parent=self._subcontainer, 401 text=bui.Lstr(resource='playlistsText'), 402 position=(40, self._sub_height - 26), 403 size=(0, 0), 404 scale=1.0, 405 maxwidth=400, 406 color=bui.app.ui_v1.title_color, 407 h_align='left', 408 v_align='center', 409 ) 410 411 index = 0 412 appconfig = bui.app.config 413 414 mesh_opaque = bui.getmesh('level_select_button_opaque') 415 mesh_transparent = bui.getmesh('level_select_button_transparent') 416 mask_tex = bui.gettexture('mapPreviewMask') 417 418 h_offs = 225 if count == 1 else 115 if count == 2 else 0 419 h_offs_bottom = 0 420 421 uiscale = bui.app.ui_v1.uiscale 422 for y in range(rows): 423 for x in range(columns): 424 name = items[index][0] 425 assert name is not None 426 pos = ( 427 x * (button_width + 2 * button_buffer_h) 428 + button_buffer_h 429 + 8 430 + h_offs, 431 self._sub_height 432 - 47 433 - (y + 1) * (button_height + 2 * button_buffer_v), 434 ) 435 btn = bui.buttonwidget( 436 parent=self._subcontainer, 437 button_type='square', 438 size=(button_width, button_height), 439 autoselect=True, 440 label='', 441 position=pos, 442 ) 443 444 if x == 0 and uiscale is bui.UIScale.SMALL: 445 bui.widget( 446 edit=btn, 447 left_widget=bui.get_special_widget('back_button'), 448 ) 449 if x == columns - 1 and uiscale is bui.UIScale.SMALL: 450 bui.widget( 451 edit=btn, 452 right_widget=bui.get_special_widget('squad_button'), 453 ) 454 bui.buttonwidget( 455 edit=btn, 456 on_activate_call=bui.Call( 457 self._on_playlist_press, btn, name 458 ), 459 on_select_call=bui.Call(self._on_playlist_select, name), 460 ) 461 bui.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50) 462 463 if self._selected_playlist == name: 464 bui.containerwidget( 465 edit=self._subcontainer, 466 selected_child=btn, 467 visible_child=btn, 468 ) 469 470 if self._back_button is not None: 471 if y == 0: 472 bui.widget(edit=btn, up_widget=self._back_button) 473 if x == 0: 474 bui.widget(edit=btn, left_widget=self._back_button) 475 476 print_name: str | bui.Lstr | None 477 if name == '__default__': 478 print_name = self._pvars.default_list_name 479 else: 480 print_name = name 481 bui.textwidget( 482 parent=self._subcontainer, 483 text=print_name, 484 position=( 485 pos[0] + button_width * 0.5, 486 pos[1] + button_height * 0.79, 487 ), 488 size=(0, 0), 489 scale=button_width * 0.003, 490 maxwidth=button_width * 0.7, 491 draw_controller=btn, 492 h_align='center', 493 v_align='center', 494 ) 495 496 # Poke into this playlist and see if we can display some of 497 # its maps. 498 map_images = [] 499 try: 500 map_textures = [] 501 map_texture_entries = [] 502 if name == '__default__': 503 playlist = self._pvars.get_default_list_call() 504 else: 505 if ( 506 name 507 not in appconfig[ 508 self._pvars.config_name + ' Playlists' 509 ] 510 ): 511 print( 512 'NOT FOUND ERR', 513 appconfig[ 514 self._pvars.config_name + ' Playlists' 515 ], 516 ) 517 playlist = appconfig[ 518 self._pvars.config_name + ' Playlists' 519 ][name] 520 playlist = filter_playlist( 521 playlist, 522 self._sessiontype, 523 remove_unowned=False, 524 mark_unowned=True, 525 name=name, 526 ) 527 for entry in playlist: 528 mapname = entry['settings']['map'] 529 maptype: type[bs.Map] | None 530 try: 531 maptype = get_map_class(mapname) 532 except bui.NotFoundError: 533 maptype = None 534 if maptype is not None: 535 tex_name = maptype.get_preview_texture_name() 536 if tex_name is not None: 537 map_textures.append(tex_name) 538 map_texture_entries.append(entry) 539 if len(map_textures) >= 6: 540 break 541 542 if len(map_textures) > 4: 543 img_rows = 3 544 img_columns = 2 545 scl = 0.33 546 h_offs_img = 30 547 v_offs_img = 126 548 elif len(map_textures) > 2: 549 img_rows = 2 550 img_columns = 2 551 scl = 0.35 552 h_offs_img = 24 553 v_offs_img = 110 554 elif len(map_textures) > 1: 555 img_rows = 2 556 img_columns = 1 557 scl = 0.5 558 h_offs_img = 47 559 v_offs_img = 105 560 else: 561 img_rows = 1 562 img_columns = 1 563 scl = 0.75 564 h_offs_img = 20 565 v_offs_img = 65 566 567 v = None 568 for row in range(img_rows): 569 for col in range(img_columns): 570 tex_index = row * img_columns + col 571 if tex_index < len(map_textures): 572 entry = map_texture_entries[tex_index] 573 574 owned = not ( 575 ( 576 'is_unowned_map' in entry 577 and entry['is_unowned_map'] 578 ) 579 or ( 580 'is_unowned_game' in entry 581 and entry['is_unowned_game'] 582 ) 583 ) 584 585 tex_name = map_textures[tex_index] 586 h = pos[0] + h_offs_img + scl * 250 * col 587 v = pos[1] + v_offs_img - scl * 130 * row 588 map_images.append( 589 bui.imagewidget( 590 parent=self._subcontainer, 591 size=(scl * 250.0, scl * 125.0), 592 position=(h, v), 593 texture=bui.gettexture(tex_name), 594 opacity=1.0 if owned else 0.25, 595 draw_controller=btn, 596 mesh_opaque=mesh_opaque, 597 mesh_transparent=mesh_transparent, 598 mask_texture=mask_tex, 599 ) 600 ) 601 if not owned: 602 bui.imagewidget( 603 parent=self._subcontainer, 604 size=(scl * 100.0, scl * 100.0), 605 position=(h + scl * 75, v + scl * 10), 606 texture=bui.gettexture('lock'), 607 draw_controller=btn, 608 ) 609 if v is not None: 610 v -= scl * 130.0 611 612 except Exception: 613 logging.exception('Error listing playlist maps.') 614 615 if not map_images: 616 bui.textwidget( 617 parent=self._subcontainer, 618 text='???', 619 scale=1.5, 620 size=(0, 0), 621 color=(1, 1, 1, 0.5), 622 h_align='center', 623 v_align='center', 624 draw_controller=btn, 625 position=( 626 pos[0] + button_width * 0.5, 627 pos[1] + button_height * 0.5, 628 ), 629 ) 630 631 index += 1 632 633 if index >= count: 634 break 635 if index >= count: 636 break 637 self._customize_button = btn = bui.buttonwidget( 638 parent=self._subcontainer, 639 size=(100, 30), 640 position=(34 + h_offs_bottom, 50), 641 text_scale=0.6, 642 label=bui.Lstr(resource='customizeText'), 643 on_activate_call=self._on_customize_press, 644 color=(0.54, 0.52, 0.67), 645 textcolor=(0.7, 0.65, 0.7), 646 autoselect=True, 647 ) 648 bui.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28) 649 self._restore_state() 650 651 def on_play_options_window_run_game(self) -> None: 652 """(internal)""" 653 654 # No-op if we're not in control. 655 if not self.main_window_has_control(): 656 # if not self._root_widget: 657 return 658 659 if self._playlist_select_context is not None: 660 # Done doing a playlist selection; now back all the way out 661 # of our selection windows to our stored starting point. 662 if self._playlist_select_context.back_state is None: 663 logging.error( 664 'No back state found' 665 ' after playlist select context completion.' 666 ) 667 else: 668 self.main_window_back_state = ( 669 self._playlist_select_context.back_state 670 ) 671 self.main_window_back() 672 else: 673 # Launching a regular game session; simply get our window 674 # transitioning out. 675 self.main_window_close(transition='out_left') 676 677 def _on_playlist_select(self, playlist_name: str) -> None: 678 self._selected_playlist = playlist_name 679 680 def _update(self) -> None: 681 # Make sure config exists. 682 if self._config_name_full not in bui.app.config: 683 bui.app.config[self._config_name_full] = {} 684 685 cfg = bui.app.config[self._config_name_full] 686 if cfg != self._last_config: 687 self._last_config = copy.deepcopy(cfg) 688 self._refresh() 689 690 def _on_playlist_press( 691 self, button: bui.Widget, playlist_name: str 692 ) -> None: 693 # pylint: disable=cyclic-import 694 from bauiv1lib.playoptions import PlayOptionsWindow 695 696 # Make sure the target playlist still exists. 697 exists = ( 698 playlist_name == '__default__' 699 or playlist_name in bui.app.config.get(self._config_name_full, {}) 700 ) 701 if not exists: 702 return 703 704 self._save_state() 705 PlayOptionsWindow( 706 sessiontype=self._sessiontype, 707 scale_origin=button.get_screen_space_center(), 708 playlist=playlist_name, 709 delegate=self, 710 playlist_select_context=self._playlist_select_context, 711 ) 712 713 def _on_customize_press(self) -> None: 714 # pylint: disable=cyclic-import 715 from bauiv1lib.playlist.customizebrowser import ( 716 PlaylistCustomizeBrowserWindow, 717 ) 718 719 # no-op if we're not in control. 720 if not self.main_window_has_control(): 721 return 722 723 self.main_window_replace( 724 PlaylistCustomizeBrowserWindow( 725 origin_widget=self._customize_button, 726 sessiontype=self._sessiontype, 727 ) 728 ) 729 730 def _on_back_press(self) -> None: 731 # pylint: disable=cyclic-import 732 # from bauiv1lib.play import PlayWindow 733 734 # no-op if we're not in control. 735 if not self.main_window_has_control(): 736 return 737 738 # Store our selected playlist if that's changed. 739 if self._selected_playlist is not None: 740 prev_sel = bui.app.config.get( 741 self._pvars.config_name + ' Playlist Selection' 742 ) 743 if self._selected_playlist != prev_sel: 744 cfg = bui.app.config 745 cfg[self._pvars.config_name + ' Playlist Selection'] = ( 746 self._selected_playlist 747 ) 748 cfg.commit() 749 750 self.main_window_back() 751 752 def _save_state(self) -> None: 753 try: 754 sel = self._root_widget.get_selected_child() 755 if sel == self._back_button: 756 sel_name = 'Back' 757 elif sel == self._scrollwidget: 758 assert self._subcontainer is not None 759 subsel = self._subcontainer.get_selected_child() 760 if subsel == self._customize_button: 761 sel_name = 'Customize' 762 else: 763 sel_name = 'Scroll' 764 else: 765 raise RuntimeError('Unrecognized selected widget.') 766 assert bui.app.classic is not None 767 bui.app.ui_v1.window_states[type(self)] = sel_name 768 except Exception: 769 logging.exception('Error saving state for %s.', self) 770 771 def _restore_state(self) -> None: 772 try: 773 assert bui.app.classic is not None 774 sel_name = bui.app.ui_v1.window_states.get(type(self)) 775 if sel_name == 'Back': 776 sel = self._back_button 777 elif sel_name == 'Scroll': 778 sel = self._scrollwidget 779 elif sel_name == 'Customize': 780 sel = self._scrollwidget 781 bui.containerwidget( 782 edit=self._subcontainer, 783 selected_child=self._customize_button, 784 visible_child=self._customize_button, 785 ) 786 else: 787 sel = self._scrollwidget 788 bui.containerwidget(edit=self._root_widget, selected_child=sel) 789 except Exception: 790 logging.exception('Error restoring state for %s.', self)
class
PlaylistBrowserWindow(bauiv1._uitypes.MainWindow):
20class PlaylistBrowserWindow(bui.MainWindow): 21 """Window for starting teams games.""" 22 23 def __init__( 24 self, 25 sessiontype: type[bs.Session], 26 transition: str | None = 'in_right', 27 origin_widget: bui.Widget | None = None, 28 playlist_select_context: PlaylistSelectContext | None = None, 29 ): 30 # pylint: disable=cyclic-import 31 from bauiv1lib.playlist import PlaylistTypeVars 32 33 # Store state for when we exit the next game. 34 if issubclass(sessiontype, bs.DualTeamSession): 35 bui.set_analytics_screen('Teams Window') 36 elif issubclass(sessiontype, bs.FreeForAllSession): 37 bui.set_analytics_screen('FreeForAll Window') 38 else: 39 raise TypeError(f'Invalid sessiontype: {sessiontype}.') 40 self._pvars = PlaylistTypeVars(sessiontype) 41 42 self._sessiontype = sessiontype 43 44 self._customize_button: bui.Widget | None = None 45 self._sub_width: float | None = None 46 self._sub_height: float | None = None 47 self._playlist_select_context = playlist_select_context 48 49 self._ensure_standard_playlists_exist() 50 51 # Get the current selection (if any). 52 self._selected_playlist = bui.app.config.get( 53 self._pvars.config_name + ' Playlist Selection' 54 ) 55 56 uiscale = bui.app.ui_v1.uiscale 57 self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0 58 x_inset = 150 if uiscale is bui.UIScale.SMALL else 0 59 self._height = ( 60 440 61 if uiscale is bui.UIScale.SMALL 62 else 510 if uiscale is bui.UIScale.MEDIUM else 580 63 ) 64 65 top_extra = 30 if uiscale is bui.UIScale.SMALL else 0 66 67 super().__init__( 68 root_widget=bui.containerwidget( 69 size=(self._width, self._height + top_extra), 70 toolbar_visibility=( 71 'menu_minimal' 72 if ( 73 uiscale is bui.UIScale.SMALL 74 or playlist_select_context is not None 75 ) 76 else 'menu_full' 77 ), 78 scale=( 79 1.83 80 if uiscale is bui.UIScale.SMALL 81 else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9 82 ), 83 stack_offset=( 84 (0, -46) if uiscale is bui.UIScale.SMALL else (0, 0) 85 ), 86 ), 87 transition=transition, 88 origin_widget=origin_widget, 89 ) 90 91 self._back_button: bui.Widget | None = bui.buttonwidget( 92 parent=self._root_widget, 93 position=(59 + x_inset, self._height - 70), 94 size=(120, 60), 95 scale=1.0, 96 on_activate_call=self._on_back_press, 97 autoselect=True, 98 label=bui.Lstr(resource='backText'), 99 button_type='back', 100 ) 101 bui.containerwidget( 102 edit=self._root_widget, cancel_button=self._back_button 103 ) 104 self._title_text = bui.textwidget( 105 parent=self._root_widget, 106 position=( 107 self._width * 0.5, 108 self._height - (32 if uiscale is bui.UIScale.SMALL else 41), 109 ), 110 size=(0, 0), 111 text=self._pvars.window_title_name, 112 scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3), 113 res_scale=1.5, 114 color=bui.app.ui_v1.heading_color, 115 h_align='center', 116 v_align='center', 117 ) 118 119 bui.buttonwidget( 120 edit=self._back_button, 121 button_type='backSmall', 122 size=(60, 54), 123 position=(59 + x_inset, self._height - 67), 124 label=bui.charstr(bui.SpecialChar.BACK), 125 ) 126 127 if uiscale is bui.UIScale.SMALL: 128 self._back_button.delete() 129 self._back_button = None 130 bui.containerwidget( 131 edit=self._root_widget, on_cancel_call=self._on_back_press 132 ) 133 scroll_offs = 33 134 else: 135 scroll_offs = 0 136 self._scroll_width = self._width - (100 + 2 * x_inset) 137 self._scroll_height = self._height - ( 138 146 if uiscale is bui.UIScale.SMALL else 136 139 ) 140 self._scrollwidget = bui.scrollwidget( 141 parent=self._root_widget, 142 highlight=False, 143 size=(self._scroll_width, self._scroll_height), 144 position=( 145 (self._width - self._scroll_width) * 0.5, 146 65 + scroll_offs, 147 ), 148 ) 149 bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) 150 self._subcontainer: bui.Widget | None = None 151 self._config_name_full = self._pvars.config_name + ' Playlists' 152 self._last_config = None 153 154 # Update now and once per second (this should do our initial 155 # refresh). 156 self._update() 157 self._update_timer = bui.AppTimer( 158 1.0, bui.WeakCall(self._update), repeat=True 159 ) 160 161 @override 162 def get_main_window_state(self) -> bui.MainWindowState: 163 # Support recreating our window for back/refresh purposes. 164 cls = type(self) 165 166 # Pull things out of self here; if we do it below in the lambda 167 # then we keep self alive. 168 sessiontype = self._sessiontype 169 170 # Pull anything out of self here; if we do it in the lambda 171 # we'll inadvertanly keep self alive. 172 playlist_select_context = self._playlist_select_context 173 174 return bui.BasicMainWindowState( 175 create_call=lambda transition, origin_widget: cls( 176 transition=transition, 177 origin_widget=origin_widget, 178 sessiontype=sessiontype, 179 playlist_select_context=playlist_select_context, 180 ) 181 ) 182 183 @override 184 def on_main_window_close(self) -> None: 185 self._save_state() 186 187 def _ensure_standard_playlists_exist(self) -> None: 188 plus = bui.app.plus 189 assert plus is not None 190 191 # On new installations, go ahead and create a few playlists 192 # besides the hard-coded default one: 193 if not plus.get_v1_account_misc_val('madeStandardPlaylists', False): 194 plus.add_v1_account_transaction( 195 { 196 'type': 'ADD_PLAYLIST', 197 'playlistType': 'Free-for-All', 198 'playlistName': bui.Lstr( 199 resource='singleGamePlaylistNameText' 200 ) 201 .evaluate() 202 .replace( 203 '${GAME}', 204 bui.Lstr( 205 translate=('gameNames', 'Death Match') 206 ).evaluate(), 207 ), 208 'playlist': [ 209 { 210 'type': 'bs_death_match.DeathMatchGame', 211 'settings': { 212 'Epic Mode': False, 213 'Kills to Win Per Player': 10, 214 'Respawn Times': 1.0, 215 'Time Limit': 300, 216 'map': 'Doom Shroom', 217 }, 218 }, 219 { 220 'type': 'bs_death_match.DeathMatchGame', 221 'settings': { 222 'Epic Mode': False, 223 'Kills to Win Per Player': 10, 224 'Respawn Times': 1.0, 225 'Time Limit': 300, 226 'map': 'Crag Castle', 227 }, 228 }, 229 ], 230 } 231 ) 232 plus.add_v1_account_transaction( 233 { 234 'type': 'ADD_PLAYLIST', 235 'playlistType': 'Team Tournament', 236 'playlistName': bui.Lstr( 237 resource='singleGamePlaylistNameText' 238 ) 239 .evaluate() 240 .replace( 241 '${GAME}', 242 bui.Lstr( 243 translate=('gameNames', 'Capture the Flag') 244 ).evaluate(), 245 ), 246 'playlist': [ 247 { 248 'type': 'bs_capture_the_flag.CTFGame', 249 'settings': { 250 'map': 'Bridgit', 251 'Score to Win': 3, 252 'Flag Idle Return Time': 30, 253 'Flag Touch Return Time': 0, 254 'Respawn Times': 1.0, 255 'Time Limit': 600, 256 'Epic Mode': False, 257 }, 258 }, 259 { 260 'type': 'bs_capture_the_flag.CTFGame', 261 'settings': { 262 'map': 'Roundabout', 263 'Score to Win': 2, 264 'Flag Idle Return Time': 30, 265 'Flag Touch Return Time': 0, 266 'Respawn Times': 1.0, 267 'Time Limit': 600, 268 'Epic Mode': False, 269 }, 270 }, 271 { 272 'type': 'bs_capture_the_flag.CTFGame', 273 'settings': { 274 'map': 'Tip Top', 275 'Score to Win': 2, 276 'Flag Idle Return Time': 30, 277 'Flag Touch Return Time': 3, 278 'Respawn Times': 1.0, 279 'Time Limit': 300, 280 'Epic Mode': False, 281 }, 282 }, 283 ], 284 } 285 ) 286 plus.add_v1_account_transaction( 287 { 288 'type': 'ADD_PLAYLIST', 289 'playlistType': 'Team Tournament', 290 'playlistName': bui.Lstr( 291 translate=('playlistNames', 'Just Sports') 292 ).evaluate(), 293 'playlist': [ 294 { 295 'type': 'bs_hockey.HockeyGame', 296 'settings': { 297 'Time Limit': 0, 298 'map': 'Hockey Stadium', 299 'Score to Win': 1, 300 'Respawn Times': 1.0, 301 }, 302 }, 303 { 304 'type': 'bs_football.FootballTeamGame', 305 'settings': { 306 'Time Limit': 0, 307 'map': 'Football Stadium', 308 'Score to Win': 21, 309 'Respawn Times': 1.0, 310 }, 311 }, 312 ], 313 } 314 ) 315 plus.add_v1_account_transaction( 316 { 317 'type': 'ADD_PLAYLIST', 318 'playlistType': 'Free-for-All', 319 'playlistName': bui.Lstr( 320 translate=('playlistNames', 'Just Epic') 321 ).evaluate(), 322 'playlist': [ 323 { 324 'type': 'bs_elimination.EliminationGame', 325 'settings': { 326 'Time Limit': 120, 327 'map': 'Tip Top', 328 'Respawn Times': 1.0, 329 'Lives Per Player': 1, 330 'Epic Mode': 1, 331 }, 332 } 333 ], 334 } 335 ) 336 plus.add_v1_account_transaction( 337 { 338 'type': 'SET_MISC_VAL', 339 'name': 'madeStandardPlaylists', 340 'value': True, 341 } 342 ) 343 plus.run_v1_account_transactions() 344 345 def _refresh(self) -> None: 346 # FIXME: Should tidy this up. 347 # pylint: disable=too-many-statements 348 # pylint: disable=too-many-branches 349 # pylint: disable=too-many-locals 350 # pylint: disable=too-many-nested-blocks 351 from efro.util import asserttype 352 from bascenev1 import get_map_class, filter_playlist 353 354 if not self._root_widget: 355 return 356 if self._subcontainer is not None: 357 self._save_state() 358 self._subcontainer.delete() 359 360 # Make sure config exists. 361 if self._config_name_full not in bui.app.config: 362 bui.app.config[self._config_name_full] = {} 363 364 items = list(bui.app.config[self._config_name_full].items()) 365 366 # Make sure everything is unicode. 367 items = [ 368 (i[0].decode(), i[1]) if not isinstance(i[0], str) else i 369 for i in items 370 ] 371 372 items.sort(key=lambda x2: asserttype(x2[0], str).lower()) 373 items = [['__default__', None]] + items # default is always first 374 375 count = len(items) 376 columns = 3 377 rows = int(math.ceil(float(count) / columns)) 378 button_width = 230 379 button_height = 230 380 button_buffer_h = -3 381 button_buffer_v = 0 382 383 self._sub_width = self._scroll_width 384 self._sub_height = ( 385 40.0 + rows * (button_height + 2 * button_buffer_v) + 90 386 ) 387 assert self._sub_width is not None 388 assert self._sub_height is not None 389 self._subcontainer = bui.containerwidget( 390 parent=self._scrollwidget, 391 size=(self._sub_width, self._sub_height), 392 background=False, 393 ) 394 395 children = self._subcontainer.get_children() 396 for child in children: 397 child.delete() 398 399 assert bui.app.classic is not None 400 bui.textwidget( 401 parent=self._subcontainer, 402 text=bui.Lstr(resource='playlistsText'), 403 position=(40, self._sub_height - 26), 404 size=(0, 0), 405 scale=1.0, 406 maxwidth=400, 407 color=bui.app.ui_v1.title_color, 408 h_align='left', 409 v_align='center', 410 ) 411 412 index = 0 413 appconfig = bui.app.config 414 415 mesh_opaque = bui.getmesh('level_select_button_opaque') 416 mesh_transparent = bui.getmesh('level_select_button_transparent') 417 mask_tex = bui.gettexture('mapPreviewMask') 418 419 h_offs = 225 if count == 1 else 115 if count == 2 else 0 420 h_offs_bottom = 0 421 422 uiscale = bui.app.ui_v1.uiscale 423 for y in range(rows): 424 for x in range(columns): 425 name = items[index][0] 426 assert name is not None 427 pos = ( 428 x * (button_width + 2 * button_buffer_h) 429 + button_buffer_h 430 + 8 431 + h_offs, 432 self._sub_height 433 - 47 434 - (y + 1) * (button_height + 2 * button_buffer_v), 435 ) 436 btn = bui.buttonwidget( 437 parent=self._subcontainer, 438 button_type='square', 439 size=(button_width, button_height), 440 autoselect=True, 441 label='', 442 position=pos, 443 ) 444 445 if x == 0 and uiscale is bui.UIScale.SMALL: 446 bui.widget( 447 edit=btn, 448 left_widget=bui.get_special_widget('back_button'), 449 ) 450 if x == columns - 1 and uiscale is bui.UIScale.SMALL: 451 bui.widget( 452 edit=btn, 453 right_widget=bui.get_special_widget('squad_button'), 454 ) 455 bui.buttonwidget( 456 edit=btn, 457 on_activate_call=bui.Call( 458 self._on_playlist_press, btn, name 459 ), 460 on_select_call=bui.Call(self._on_playlist_select, name), 461 ) 462 bui.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50) 463 464 if self._selected_playlist == name: 465 bui.containerwidget( 466 edit=self._subcontainer, 467 selected_child=btn, 468 visible_child=btn, 469 ) 470 471 if self._back_button is not None: 472 if y == 0: 473 bui.widget(edit=btn, up_widget=self._back_button) 474 if x == 0: 475 bui.widget(edit=btn, left_widget=self._back_button) 476 477 print_name: str | bui.Lstr | None 478 if name == '__default__': 479 print_name = self._pvars.default_list_name 480 else: 481 print_name = name 482 bui.textwidget( 483 parent=self._subcontainer, 484 text=print_name, 485 position=( 486 pos[0] + button_width * 0.5, 487 pos[1] + button_height * 0.79, 488 ), 489 size=(0, 0), 490 scale=button_width * 0.003, 491 maxwidth=button_width * 0.7, 492 draw_controller=btn, 493 h_align='center', 494 v_align='center', 495 ) 496 497 # Poke into this playlist and see if we can display some of 498 # its maps. 499 map_images = [] 500 try: 501 map_textures = [] 502 map_texture_entries = [] 503 if name == '__default__': 504 playlist = self._pvars.get_default_list_call() 505 else: 506 if ( 507 name 508 not in appconfig[ 509 self._pvars.config_name + ' Playlists' 510 ] 511 ): 512 print( 513 'NOT FOUND ERR', 514 appconfig[ 515 self._pvars.config_name + ' Playlists' 516 ], 517 ) 518 playlist = appconfig[ 519 self._pvars.config_name + ' Playlists' 520 ][name] 521 playlist = filter_playlist( 522 playlist, 523 self._sessiontype, 524 remove_unowned=False, 525 mark_unowned=True, 526 name=name, 527 ) 528 for entry in playlist: 529 mapname = entry['settings']['map'] 530 maptype: type[bs.Map] | None 531 try: 532 maptype = get_map_class(mapname) 533 except bui.NotFoundError: 534 maptype = None 535 if maptype is not None: 536 tex_name = maptype.get_preview_texture_name() 537 if tex_name is not None: 538 map_textures.append(tex_name) 539 map_texture_entries.append(entry) 540 if len(map_textures) >= 6: 541 break 542 543 if len(map_textures) > 4: 544 img_rows = 3 545 img_columns = 2 546 scl = 0.33 547 h_offs_img = 30 548 v_offs_img = 126 549 elif len(map_textures) > 2: 550 img_rows = 2 551 img_columns = 2 552 scl = 0.35 553 h_offs_img = 24 554 v_offs_img = 110 555 elif len(map_textures) > 1: 556 img_rows = 2 557 img_columns = 1 558 scl = 0.5 559 h_offs_img = 47 560 v_offs_img = 105 561 else: 562 img_rows = 1 563 img_columns = 1 564 scl = 0.75 565 h_offs_img = 20 566 v_offs_img = 65 567 568 v = None 569 for row in range(img_rows): 570 for col in range(img_columns): 571 tex_index = row * img_columns + col 572 if tex_index < len(map_textures): 573 entry = map_texture_entries[tex_index] 574 575 owned = not ( 576 ( 577 'is_unowned_map' in entry 578 and entry['is_unowned_map'] 579 ) 580 or ( 581 'is_unowned_game' in entry 582 and entry['is_unowned_game'] 583 ) 584 ) 585 586 tex_name = map_textures[tex_index] 587 h = pos[0] + h_offs_img + scl * 250 * col 588 v = pos[1] + v_offs_img - scl * 130 * row 589 map_images.append( 590 bui.imagewidget( 591 parent=self._subcontainer, 592 size=(scl * 250.0, scl * 125.0), 593 position=(h, v), 594 texture=bui.gettexture(tex_name), 595 opacity=1.0 if owned else 0.25, 596 draw_controller=btn, 597 mesh_opaque=mesh_opaque, 598 mesh_transparent=mesh_transparent, 599 mask_texture=mask_tex, 600 ) 601 ) 602 if not owned: 603 bui.imagewidget( 604 parent=self._subcontainer, 605 size=(scl * 100.0, scl * 100.0), 606 position=(h + scl * 75, v + scl * 10), 607 texture=bui.gettexture('lock'), 608 draw_controller=btn, 609 ) 610 if v is not None: 611 v -= scl * 130.0 612 613 except Exception: 614 logging.exception('Error listing playlist maps.') 615 616 if not map_images: 617 bui.textwidget( 618 parent=self._subcontainer, 619 text='???', 620 scale=1.5, 621 size=(0, 0), 622 color=(1, 1, 1, 0.5), 623 h_align='center', 624 v_align='center', 625 draw_controller=btn, 626 position=( 627 pos[0] + button_width * 0.5, 628 pos[1] + button_height * 0.5, 629 ), 630 ) 631 632 index += 1 633 634 if index >= count: 635 break 636 if index >= count: 637 break 638 self._customize_button = btn = bui.buttonwidget( 639 parent=self._subcontainer, 640 size=(100, 30), 641 position=(34 + h_offs_bottom, 50), 642 text_scale=0.6, 643 label=bui.Lstr(resource='customizeText'), 644 on_activate_call=self._on_customize_press, 645 color=(0.54, 0.52, 0.67), 646 textcolor=(0.7, 0.65, 0.7), 647 autoselect=True, 648 ) 649 bui.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28) 650 self._restore_state() 651 652 def on_play_options_window_run_game(self) -> None: 653 """(internal)""" 654 655 # No-op if we're not in control. 656 if not self.main_window_has_control(): 657 # if not self._root_widget: 658 return 659 660 if self._playlist_select_context is not None: 661 # Done doing a playlist selection; now back all the way out 662 # of our selection windows to our stored starting point. 663 if self._playlist_select_context.back_state is None: 664 logging.error( 665 'No back state found' 666 ' after playlist select context completion.' 667 ) 668 else: 669 self.main_window_back_state = ( 670 self._playlist_select_context.back_state 671 ) 672 self.main_window_back() 673 else: 674 # Launching a regular game session; simply get our window 675 # transitioning out. 676 self.main_window_close(transition='out_left') 677 678 def _on_playlist_select(self, playlist_name: str) -> None: 679 self._selected_playlist = playlist_name 680 681 def _update(self) -> None: 682 # Make sure config exists. 683 if self._config_name_full not in bui.app.config: 684 bui.app.config[self._config_name_full] = {} 685 686 cfg = bui.app.config[self._config_name_full] 687 if cfg != self._last_config: 688 self._last_config = copy.deepcopy(cfg) 689 self._refresh() 690 691 def _on_playlist_press( 692 self, button: bui.Widget, playlist_name: str 693 ) -> None: 694 # pylint: disable=cyclic-import 695 from bauiv1lib.playoptions import PlayOptionsWindow 696 697 # Make sure the target playlist still exists. 698 exists = ( 699 playlist_name == '__default__' 700 or playlist_name in bui.app.config.get(self._config_name_full, {}) 701 ) 702 if not exists: 703 return 704 705 self._save_state() 706 PlayOptionsWindow( 707 sessiontype=self._sessiontype, 708 scale_origin=button.get_screen_space_center(), 709 playlist=playlist_name, 710 delegate=self, 711 playlist_select_context=self._playlist_select_context, 712 ) 713 714 def _on_customize_press(self) -> None: 715 # pylint: disable=cyclic-import 716 from bauiv1lib.playlist.customizebrowser import ( 717 PlaylistCustomizeBrowserWindow, 718 ) 719 720 # no-op if we're not in control. 721 if not self.main_window_has_control(): 722 return 723 724 self.main_window_replace( 725 PlaylistCustomizeBrowserWindow( 726 origin_widget=self._customize_button, 727 sessiontype=self._sessiontype, 728 ) 729 ) 730 731 def _on_back_press(self) -> None: 732 # pylint: disable=cyclic-import 733 # from bauiv1lib.play import PlayWindow 734 735 # no-op if we're not in control. 736 if not self.main_window_has_control(): 737 return 738 739 # Store our selected playlist if that's changed. 740 if self._selected_playlist is not None: 741 prev_sel = bui.app.config.get( 742 self._pvars.config_name + ' Playlist Selection' 743 ) 744 if self._selected_playlist != prev_sel: 745 cfg = bui.app.config 746 cfg[self._pvars.config_name + ' Playlist Selection'] = ( 747 self._selected_playlist 748 ) 749 cfg.commit() 750 751 self.main_window_back() 752 753 def _save_state(self) -> None: 754 try: 755 sel = self._root_widget.get_selected_child() 756 if sel == self._back_button: 757 sel_name = 'Back' 758 elif sel == self._scrollwidget: 759 assert self._subcontainer is not None 760 subsel = self._subcontainer.get_selected_child() 761 if subsel == self._customize_button: 762 sel_name = 'Customize' 763 else: 764 sel_name = 'Scroll' 765 else: 766 raise RuntimeError('Unrecognized selected widget.') 767 assert bui.app.classic is not None 768 bui.app.ui_v1.window_states[type(self)] = sel_name 769 except Exception: 770 logging.exception('Error saving state for %s.', self) 771 772 def _restore_state(self) -> None: 773 try: 774 assert bui.app.classic is not None 775 sel_name = bui.app.ui_v1.window_states.get(type(self)) 776 if sel_name == 'Back': 777 sel = self._back_button 778 elif sel_name == 'Scroll': 779 sel = self._scrollwidget 780 elif sel_name == 'Customize': 781 sel = self._scrollwidget 782 bui.containerwidget( 783 edit=self._subcontainer, 784 selected_child=self._customize_button, 785 visible_child=self._customize_button, 786 ) 787 else: 788 sel = self._scrollwidget 789 bui.containerwidget(edit=self._root_widget, selected_child=sel) 790 except Exception: 791 logging.exception('Error restoring state for %s.', self)
Window for starting teams games.
PlaylistBrowserWindow( sessiontype: type[bascenev1.Session], transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None, playlist_select_context: bauiv1lib.play.PlaylistSelectContext | None = None)
23 def __init__( 24 self, 25 sessiontype: type[bs.Session], 26 transition: str | None = 'in_right', 27 origin_widget: bui.Widget | None = None, 28 playlist_select_context: PlaylistSelectContext | None = None, 29 ): 30 # pylint: disable=cyclic-import 31 from bauiv1lib.playlist import PlaylistTypeVars 32 33 # Store state for when we exit the next game. 34 if issubclass(sessiontype, bs.DualTeamSession): 35 bui.set_analytics_screen('Teams Window') 36 elif issubclass(sessiontype, bs.FreeForAllSession): 37 bui.set_analytics_screen('FreeForAll Window') 38 else: 39 raise TypeError(f'Invalid sessiontype: {sessiontype}.') 40 self._pvars = PlaylistTypeVars(sessiontype) 41 42 self._sessiontype = sessiontype 43 44 self._customize_button: bui.Widget | None = None 45 self._sub_width: float | None = None 46 self._sub_height: float | None = None 47 self._playlist_select_context = playlist_select_context 48 49 self._ensure_standard_playlists_exist() 50 51 # Get the current selection (if any). 52 self._selected_playlist = bui.app.config.get( 53 self._pvars.config_name + ' Playlist Selection' 54 ) 55 56 uiscale = bui.app.ui_v1.uiscale 57 self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0 58 x_inset = 150 if uiscale is bui.UIScale.SMALL else 0 59 self._height = ( 60 440 61 if uiscale is bui.UIScale.SMALL 62 else 510 if uiscale is bui.UIScale.MEDIUM else 580 63 ) 64 65 top_extra = 30 if uiscale is bui.UIScale.SMALL else 0 66 67 super().__init__( 68 root_widget=bui.containerwidget( 69 size=(self._width, self._height + top_extra), 70 toolbar_visibility=( 71 'menu_minimal' 72 if ( 73 uiscale is bui.UIScale.SMALL 74 or playlist_select_context is not None 75 ) 76 else 'menu_full' 77 ), 78 scale=( 79 1.83 80 if uiscale is bui.UIScale.SMALL 81 else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9 82 ), 83 stack_offset=( 84 (0, -46) if uiscale is bui.UIScale.SMALL else (0, 0) 85 ), 86 ), 87 transition=transition, 88 origin_widget=origin_widget, 89 ) 90 91 self._back_button: bui.Widget | None = bui.buttonwidget( 92 parent=self._root_widget, 93 position=(59 + x_inset, self._height - 70), 94 size=(120, 60), 95 scale=1.0, 96 on_activate_call=self._on_back_press, 97 autoselect=True, 98 label=bui.Lstr(resource='backText'), 99 button_type='back', 100 ) 101 bui.containerwidget( 102 edit=self._root_widget, cancel_button=self._back_button 103 ) 104 self._title_text = bui.textwidget( 105 parent=self._root_widget, 106 position=( 107 self._width * 0.5, 108 self._height - (32 if uiscale is bui.UIScale.SMALL else 41), 109 ), 110 size=(0, 0), 111 text=self._pvars.window_title_name, 112 scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3), 113 res_scale=1.5, 114 color=bui.app.ui_v1.heading_color, 115 h_align='center', 116 v_align='center', 117 ) 118 119 bui.buttonwidget( 120 edit=self._back_button, 121 button_type='backSmall', 122 size=(60, 54), 123 position=(59 + x_inset, self._height - 67), 124 label=bui.charstr(bui.SpecialChar.BACK), 125 ) 126 127 if uiscale is bui.UIScale.SMALL: 128 self._back_button.delete() 129 self._back_button = None 130 bui.containerwidget( 131 edit=self._root_widget, on_cancel_call=self._on_back_press 132 ) 133 scroll_offs = 33 134 else: 135 scroll_offs = 0 136 self._scroll_width = self._width - (100 + 2 * x_inset) 137 self._scroll_height = self._height - ( 138 146 if uiscale is bui.UIScale.SMALL else 136 139 ) 140 self._scrollwidget = bui.scrollwidget( 141 parent=self._root_widget, 142 highlight=False, 143 size=(self._scroll_width, self._scroll_height), 144 position=( 145 (self._width - self._scroll_width) * 0.5, 146 65 + scroll_offs, 147 ), 148 ) 149 bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) 150 self._subcontainer: bui.Widget | None = None 151 self._config_name_full = self._pvars.config_name + ' Playlists' 152 self._last_config = None 153 154 # Update now and once per second (this should do our initial 155 # refresh). 156 self._update() 157 self._update_timer = bui.AppTimer( 158 1.0, bui.WeakCall(self._update), repeat=True 159 )
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.
161 @override 162 def get_main_window_state(self) -> bui.MainWindowState: 163 # Support recreating our window for back/refresh purposes. 164 cls = type(self) 165 166 # Pull things out of self here; if we do it below in the lambda 167 # then we keep self alive. 168 sessiontype = self._sessiontype 169 170 # Pull anything out of self here; if we do it in the lambda 171 # we'll inadvertanly keep self alive. 172 playlist_select_context = self._playlist_select_context 173 174 return bui.BasicMainWindowState( 175 create_call=lambda transition, origin_widget: cls( 176 transition=transition, 177 origin_widget=origin_widget, 178 sessiontype=sessiontype, 179 playlist_select_context=playlist_select_context, 180 ) 181 )
Return a WindowState to recreate this window, if supported.