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