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