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