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