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