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