bastd.ui.watch
Provides UI functionality for watching replays.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides UI functionality for watching replays.""" 4 5from __future__ import annotations 6 7import os 8from enum import Enum 9from typing import TYPE_CHECKING, cast 10 11import ba 12import ba.internal 13 14if TYPE_CHECKING: 15 from typing import Any 16 17 18class WatchWindow(ba.Window): 19 """Window for watching replays.""" 20 21 class TabID(Enum): 22 """Our available tab types.""" 23 24 MY_REPLAYS = 'my_replays' 25 TEST_TAB = 'test_tab' 26 27 def __init__( 28 self, 29 transition: str | None = 'in_right', 30 origin_widget: ba.Widget | None = None, 31 ): 32 # pylint: disable=too-many-locals 33 # pylint: disable=too-many-statements 34 from bastd.ui.tabs import TabRow 35 36 ba.set_analytics_screen('Watch Window') 37 scale_origin: tuple[float, float] | None 38 if origin_widget is not None: 39 self._transition_out = 'out_scale' 40 scale_origin = origin_widget.get_screen_space_center() 41 transition = 'in_scale' 42 else: 43 self._transition_out = 'out_right' 44 scale_origin = None 45 ba.app.ui.set_main_menu_location('Watch') 46 self._tab_data: dict[str, Any] = {} 47 self._my_replays_scroll_width: float | None = None 48 self._my_replays_watch_replay_button: ba.Widget | None = None 49 self._scrollwidget: ba.Widget | None = None 50 self._columnwidget: ba.Widget | None = None 51 self._my_replay_selected: str | None = None 52 self._my_replays_rename_window: ba.Widget | None = None 53 self._my_replay_rename_text: ba.Widget | None = None 54 self._r = 'watchWindow' 55 uiscale = ba.app.ui.uiscale 56 self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 57 x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 58 self._height = ( 59 578 60 if uiscale is ba.UIScale.SMALL 61 else 670 62 if uiscale is ba.UIScale.MEDIUM 63 else 800 64 ) 65 self._current_tab: WatchWindow.TabID | None = None 66 extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 67 68 super().__init__( 69 root_widget=ba.containerwidget( 70 size=(self._width, self._height + extra_top), 71 transition=transition, 72 toolbar_visibility='menu_minimal', 73 scale_origin_stack_offset=scale_origin, 74 scale=( 75 1.3 76 if uiscale is ba.UIScale.SMALL 77 else 0.97 78 if uiscale is ba.UIScale.MEDIUM 79 else 0.8 80 ), 81 stack_offset=(0, -10) 82 if uiscale is ba.UIScale.SMALL 83 else (0, 15) 84 if uiscale is ba.UIScale.MEDIUM 85 else (0, 0), 86 ) 87 ) 88 89 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 90 ba.containerwidget( 91 edit=self._root_widget, on_cancel_call=self._back 92 ) 93 self._back_button = None 94 else: 95 self._back_button = btn = ba.buttonwidget( 96 parent=self._root_widget, 97 autoselect=True, 98 position=(70 + x_inset, self._height - 74), 99 size=(140, 60), 100 scale=1.1, 101 label=ba.Lstr(resource='backText'), 102 button_type='back', 103 on_activate_call=self._back, 104 ) 105 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 106 ba.buttonwidget( 107 edit=btn, 108 button_type='backSmall', 109 size=(60, 60), 110 label=ba.charstr(ba.SpecialChar.BACK), 111 ) 112 113 ba.textwidget( 114 parent=self._root_widget, 115 position=(self._width * 0.5, self._height - 38), 116 size=(0, 0), 117 color=ba.app.ui.title_color, 118 scale=1.5, 119 h_align='center', 120 v_align='center', 121 text=ba.Lstr(resource=self._r + '.titleText'), 122 maxwidth=400, 123 ) 124 125 tabdefs = [ 126 ( 127 self.TabID.MY_REPLAYS, 128 ba.Lstr(resource=self._r + '.myReplaysText'), 129 ), 130 # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')), 131 ] 132 133 scroll_buffer_h = 130 + 2 * x_inset 134 tab_buffer_h = 750 + 2 * x_inset 135 136 self._tab_row = TabRow( 137 self._root_widget, 138 tabdefs, 139 pos=(tab_buffer_h * 0.5, self._height - 130), 140 size=(self._width - tab_buffer_h, 50), 141 on_select_call=self._set_tab, 142 ) 143 144 if ba.app.ui.use_toolbars: 145 first_tab = self._tab_row.tabs[tabdefs[0][0]] 146 last_tab = self._tab_row.tabs[tabdefs[-1][0]] 147 ba.widget( 148 edit=last_tab.button, 149 right_widget=ba.internal.get_special_widget('party_button'), 150 ) 151 if uiscale is ba.UIScale.SMALL: 152 bbtn = ba.internal.get_special_widget('back_button') 153 ba.widget( 154 edit=first_tab.button, up_widget=bbtn, left_widget=bbtn 155 ) 156 157 self._scroll_width = self._width - scroll_buffer_h 158 self._scroll_height = self._height - 180 159 160 # Not actually using a scroll widget anymore; just an image. 161 scroll_left = (self._width - self._scroll_width) * 0.5 162 scroll_bottom = self._height - self._scroll_height - 79 - 48 163 buffer_h = 10 164 buffer_v = 4 165 ba.imagewidget( 166 parent=self._root_widget, 167 position=(scroll_left - buffer_h, scroll_bottom - buffer_v), 168 size=( 169 self._scroll_width + 2 * buffer_h, 170 self._scroll_height + 2 * buffer_v, 171 ), 172 texture=ba.gettexture('scrollWidget'), 173 model_transparent=ba.getmodel('softEdgeOutside'), 174 ) 175 self._tab_container: ba.Widget | None = None 176 177 self._restore_state() 178 179 def _set_tab(self, tab_id: TabID) -> None: 180 # pylint: disable=too-many-locals 181 182 if self._current_tab == tab_id: 183 return 184 self._current_tab = tab_id 185 186 # Preserve our current tab between runs. 187 cfg = ba.app.config 188 cfg['Watch Tab'] = tab_id.value 189 cfg.commit() 190 191 # Update tab colors based on which is selected. 192 # tabs.update_tab_button_colors(self._tab_buttons, tab) 193 self._tab_row.update_appearance(tab_id) 194 195 if self._tab_container: 196 self._tab_container.delete() 197 scroll_left = (self._width - self._scroll_width) * 0.5 198 scroll_bottom = self._height - self._scroll_height - 79 - 48 199 200 # A place where tabs can store data to get cleared when 201 # switching to a different tab 202 self._tab_data = {} 203 204 uiscale = ba.app.ui.uiscale 205 if tab_id is self.TabID.MY_REPLAYS: 206 c_width = self._scroll_width 207 c_height = self._scroll_height - 20 208 sub_scroll_height = c_height - 63 209 self._my_replays_scroll_width = sub_scroll_width = ( 210 680 if uiscale is ba.UIScale.SMALL else 640 211 ) 212 213 self._tab_container = cnt = ba.containerwidget( 214 parent=self._root_widget, 215 position=( 216 scroll_left, 217 scroll_bottom + (self._scroll_height - c_height) * 0.5, 218 ), 219 size=(c_width, c_height), 220 background=False, 221 selection_loops_to_parent=True, 222 ) 223 224 v = c_height - 30 225 ba.textwidget( 226 parent=cnt, 227 position=(c_width * 0.5, v), 228 color=(0.6, 1.0, 0.6), 229 scale=0.7, 230 size=(0, 0), 231 maxwidth=c_width * 0.9, 232 h_align='center', 233 v_align='center', 234 text=ba.Lstr( 235 resource='replayRenameWarningText', 236 subs=[ 237 ('${REPLAY}', ba.Lstr(resource='replayNameDefaultText')) 238 ], 239 ), 240 ) 241 242 b_width = 140 if uiscale is ba.UIScale.SMALL else 178 243 b_height = ( 244 107 245 if uiscale is ba.UIScale.SMALL 246 else 142 247 if uiscale is ba.UIScale.MEDIUM 248 else 190 249 ) 250 b_space_extra = ( 251 0 252 if uiscale is ba.UIScale.SMALL 253 else -2 254 if uiscale is ba.UIScale.MEDIUM 255 else -5 256 ) 257 258 b_color = (0.6, 0.53, 0.63) 259 b_textcolor = (0.75, 0.7, 0.8) 260 btnv = ( 261 c_height 262 - ( 263 48 264 if uiscale is ba.UIScale.SMALL 265 else 45 266 if uiscale is ba.UIScale.MEDIUM 267 else 40 268 ) 269 - b_height 270 ) 271 btnh = 40 if uiscale is ba.UIScale.SMALL else 40 272 smlh = 190 if uiscale is ba.UIScale.SMALL else 225 273 tscl = 1.0 if uiscale is ba.UIScale.SMALL else 1.2 274 self._my_replays_watch_replay_button = btn1 = ba.buttonwidget( 275 parent=cnt, 276 size=(b_width, b_height), 277 position=(btnh, btnv), 278 button_type='square', 279 color=b_color, 280 textcolor=b_textcolor, 281 on_activate_call=self._on_my_replay_play_press, 282 text_scale=tscl, 283 label=ba.Lstr(resource=self._r + '.watchReplayButtonText'), 284 autoselect=True, 285 ) 286 ba.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button) 287 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 288 ba.widget( 289 edit=btn1, 290 left_widget=ba.internal.get_special_widget('back_button'), 291 ) 292 btnv -= b_height + b_space_extra 293 ba.buttonwidget( 294 parent=cnt, 295 size=(b_width, b_height), 296 position=(btnh, btnv), 297 button_type='square', 298 color=b_color, 299 textcolor=b_textcolor, 300 on_activate_call=self._on_my_replay_rename_press, 301 text_scale=tscl, 302 label=ba.Lstr(resource=self._r + '.renameReplayButtonText'), 303 autoselect=True, 304 ) 305 btnv -= b_height + b_space_extra 306 ba.buttonwidget( 307 parent=cnt, 308 size=(b_width, b_height), 309 position=(btnh, btnv), 310 button_type='square', 311 color=b_color, 312 textcolor=b_textcolor, 313 on_activate_call=self._on_my_replay_delete_press, 314 text_scale=tscl, 315 label=ba.Lstr(resource=self._r + '.deleteReplayButtonText'), 316 autoselect=True, 317 ) 318 319 v -= sub_scroll_height + 23 320 self._scrollwidget = scrlw = ba.scrollwidget( 321 parent=cnt, 322 position=(smlh, v), 323 size=(sub_scroll_width, sub_scroll_height), 324 ) 325 ba.containerwidget(edit=cnt, selected_child=scrlw) 326 self._columnwidget = ba.columnwidget( 327 parent=scrlw, left_border=10, border=2, margin=0 328 ) 329 330 ba.widget( 331 edit=scrlw, 332 autoselect=True, 333 left_widget=btn1, 334 up_widget=self._tab_row.tabs[tab_id].button, 335 ) 336 ba.widget(edit=self._tab_row.tabs[tab_id].button, down_widget=scrlw) 337 338 self._my_replay_selected = None 339 self._refresh_my_replays() 340 341 def _no_replay_selected_error(self) -> None: 342 ba.screenmessage( 343 ba.Lstr(resource=self._r + '.noReplaySelectedErrorText'), 344 color=(1, 0, 0), 345 ) 346 ba.playsound(ba.getsound('error')) 347 348 def _on_my_replay_play_press(self) -> None: 349 if self._my_replay_selected is None: 350 self._no_replay_selected_error() 351 return 352 ba.internal.increment_analytics_count('Replay watch') 353 354 def do_it() -> None: 355 try: 356 # Reset to normal speed. 357 ba.internal.set_replay_speed_exponent(0) 358 ba.internal.fade_screen(True) 359 assert self._my_replay_selected is not None 360 ba.internal.new_replay_session( 361 ba.internal.get_replays_dir() 362 + '/' 363 + self._my_replay_selected 364 ) 365 except Exception: 366 ba.print_exception('Error running replay session.') 367 368 # Drop back into a fresh main menu session 369 # in case we half-launched or something. 370 from bastd import mainmenu 371 372 ba.internal.new_host_session(mainmenu.MainMenuSession) 373 374 ba.internal.fade_screen(False, endcall=ba.Call(ba.pushcall, do_it)) 375 ba.containerwidget(edit=self._root_widget, transition='out_left') 376 377 def _on_my_replay_rename_press(self) -> None: 378 if self._my_replay_selected is None: 379 self._no_replay_selected_error() 380 return 381 c_width = 600 382 c_height = 250 383 uiscale = ba.app.ui.uiscale 384 self._my_replays_rename_window = cnt = ba.containerwidget( 385 scale=( 386 1.8 387 if uiscale is ba.UIScale.SMALL 388 else 1.55 389 if uiscale is ba.UIScale.MEDIUM 390 else 1.0 391 ), 392 size=(c_width, c_height), 393 transition='in_scale', 394 ) 395 dname = self._get_replay_display_name(self._my_replay_selected) 396 ba.textwidget( 397 parent=cnt, 398 size=(0, 0), 399 h_align='center', 400 v_align='center', 401 text=ba.Lstr( 402 resource=self._r + '.renameReplayText', 403 subs=[('${REPLAY}', dname)], 404 ), 405 maxwidth=c_width * 0.8, 406 position=(c_width * 0.5, c_height - 60), 407 ) 408 self._my_replay_rename_text = txt = ba.textwidget( 409 parent=cnt, 410 size=(c_width * 0.8, 40), 411 h_align='left', 412 v_align='center', 413 text=dname, 414 editable=True, 415 description=ba.Lstr(resource=self._r + '.replayNameText'), 416 position=(c_width * 0.1, c_height - 140), 417 autoselect=True, 418 maxwidth=c_width * 0.7, 419 max_chars=200, 420 ) 421 cbtn = ba.buttonwidget( 422 parent=cnt, 423 label=ba.Lstr(resource='cancelText'), 424 on_activate_call=ba.Call( 425 lambda c: ba.containerwidget(edit=c, transition='out_scale'), 426 cnt, 427 ), 428 size=(180, 60), 429 position=(30, 30), 430 autoselect=True, 431 ) 432 okb = ba.buttonwidget( 433 parent=cnt, 434 label=ba.Lstr(resource=self._r + '.renameText'), 435 size=(180, 60), 436 position=(c_width - 230, 30), 437 on_activate_call=ba.Call( 438 self._rename_my_replay, self._my_replay_selected 439 ), 440 autoselect=True, 441 ) 442 ba.widget(edit=cbtn, right_widget=okb) 443 ba.widget(edit=okb, left_widget=cbtn) 444 ba.textwidget(edit=txt, on_return_press_call=okb.activate) 445 ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) 446 447 def _rename_my_replay(self, replay: str) -> None: 448 new_name = None 449 try: 450 if not self._my_replay_rename_text: 451 return 452 new_name_raw = cast( 453 str, ba.textwidget(query=self._my_replay_rename_text) 454 ) 455 new_name = new_name_raw + '.brp' 456 457 # Ignore attempts to change it to what it already is 458 # (or what it looks like to the user). 459 if ( 460 replay != new_name 461 and self._get_replay_display_name(replay) != new_name_raw 462 ): 463 old_name_full = ( 464 ba.internal.get_replays_dir() + '/' + replay 465 ).encode('utf-8') 466 new_name_full = ( 467 ba.internal.get_replays_dir() + '/' + new_name 468 ).encode('utf-8') 469 # False alarm; ba.textwidget can return non-None val. 470 # pylint: disable=unsupported-membership-test 471 if os.path.exists(new_name_full): 472 ba.playsound(ba.getsound('error')) 473 ba.screenmessage( 474 ba.Lstr( 475 resource=self._r 476 + '.replayRenameErrorAlreadyExistsText' 477 ), 478 color=(1, 0, 0), 479 ) 480 elif any(char in new_name_raw for char in ['/', '\\', ':']): 481 ba.playsound(ba.getsound('error')) 482 ba.screenmessage( 483 ba.Lstr( 484 resource=self._r + '.replayRenameErrorInvalidName' 485 ), 486 color=(1, 0, 0), 487 ) 488 else: 489 ba.internal.increment_analytics_count('Replay rename') 490 os.rename(old_name_full, new_name_full) 491 self._refresh_my_replays() 492 ba.playsound(ba.getsound('gunCocking')) 493 except Exception: 494 ba.print_exception( 495 f"Error renaming replay '{replay}' to '{new_name}'." 496 ) 497 ba.playsound(ba.getsound('error')) 498 ba.screenmessage( 499 ba.Lstr(resource=self._r + '.replayRenameErrorText'), 500 color=(1, 0, 0), 501 ) 502 503 ba.containerwidget( 504 edit=self._my_replays_rename_window, transition='out_scale' 505 ) 506 507 def _on_my_replay_delete_press(self) -> None: 508 from bastd.ui import confirm 509 510 if self._my_replay_selected is None: 511 self._no_replay_selected_error() 512 return 513 confirm.ConfirmWindow( 514 ba.Lstr( 515 resource=self._r + '.deleteConfirmText', 516 subs=[ 517 ( 518 '${REPLAY}', 519 self._get_replay_display_name(self._my_replay_selected), 520 ) 521 ], 522 ), 523 ba.Call(self._delete_replay, self._my_replay_selected), 524 450, 525 150, 526 ) 527 528 def _get_replay_display_name(self, replay: str) -> str: 529 if replay.endswith('.brp'): 530 replay = replay[:-4] 531 if replay == '__lastReplay': 532 return ba.Lstr(resource='replayNameDefaultText').evaluate() 533 return replay 534 535 def _delete_replay(self, replay: str) -> None: 536 try: 537 ba.internal.increment_analytics_count('Replay delete') 538 os.remove( 539 (ba.internal.get_replays_dir() + '/' + replay).encode('utf-8') 540 ) 541 self._refresh_my_replays() 542 ba.playsound(ba.getsound('shieldDown')) 543 if replay == self._my_replay_selected: 544 self._my_replay_selected = None 545 except Exception: 546 ba.print_exception(f"Error deleting replay '{replay}'.") 547 ba.playsound(ba.getsound('error')) 548 ba.screenmessage( 549 ba.Lstr(resource=self._r + '.replayDeleteErrorText'), 550 color=(1, 0, 0), 551 ) 552 553 def _on_my_replay_select(self, replay: str) -> None: 554 self._my_replay_selected = replay 555 556 def _refresh_my_replays(self) -> None: 557 assert self._columnwidget is not None 558 for child in self._columnwidget.get_children(): 559 child.delete() 560 t_scale = 1.6 561 try: 562 names = os.listdir(ba.internal.get_replays_dir()) 563 564 # Ignore random other files in there. 565 names = [n for n in names if n.endswith('.brp')] 566 names.sort(key=lambda x: x.lower()) 567 except Exception: 568 ba.print_exception('Error listing replays dir.') 569 names = [] 570 571 assert self._my_replays_scroll_width is not None 572 assert self._my_replays_watch_replay_button is not None 573 for i, name in enumerate(names): 574 txt = ba.textwidget( 575 parent=self._columnwidget, 576 size=(self._my_replays_scroll_width / t_scale, 30), 577 selectable=True, 578 color=(1.0, 1, 0.4) 579 if name == '__lastReplay.brp' 580 else (1, 1, 1), 581 always_highlight=True, 582 on_select_call=ba.Call(self._on_my_replay_select, name), 583 on_activate_call=self._my_replays_watch_replay_button.activate, 584 text=self._get_replay_display_name(name), 585 h_align='left', 586 v_align='center', 587 corner_scale=t_scale, 588 maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93, 589 ) 590 if i == 0: 591 ba.widget( 592 edit=txt, 593 up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button, 594 ) 595 596 def _save_state(self) -> None: 597 try: 598 sel = self._root_widget.get_selected_child() 599 selected_tab_ids = [ 600 tab_id 601 for tab_id, tab in self._tab_row.tabs.items() 602 if sel == tab.button 603 ] 604 if sel == self._back_button: 605 sel_name = 'Back' 606 elif selected_tab_ids: 607 assert len(selected_tab_ids) == 1 608 sel_name = f'Tab:{selected_tab_ids[0].value}' 609 elif sel == self._tab_container: 610 sel_name = 'TabContainer' 611 else: 612 raise ValueError(f'unrecognized selection {sel}') 613 ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} 614 except Exception: 615 ba.print_exception(f'Error saving state for {self}.') 616 617 def _restore_state(self) -> None: 618 from efro.util import enum_by_value 619 620 try: 621 sel: ba.Widget | None 622 sel_name = ba.app.ui.window_states.get(type(self), {}).get( 623 'sel_name' 624 ) 625 assert isinstance(sel_name, (str, type(None))) 626 try: 627 current_tab = enum_by_value( 628 self.TabID, ba.app.config.get('Watch Tab') 629 ) 630 except ValueError: 631 current_tab = self.TabID.MY_REPLAYS 632 self._set_tab(current_tab) 633 634 if sel_name == 'Back': 635 sel = self._back_button 636 elif sel_name == 'TabContainer': 637 sel = self._tab_container 638 elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): 639 try: 640 sel_tab_id = enum_by_value( 641 self.TabID, sel_name.split(':')[-1] 642 ) 643 except ValueError: 644 sel_tab_id = self.TabID.MY_REPLAYS 645 sel = self._tab_row.tabs[sel_tab_id].button 646 else: 647 if self._tab_container is not None: 648 sel = self._tab_container 649 else: 650 sel = self._tab_row.tabs[current_tab].button 651 ba.containerwidget(edit=self._root_widget, selected_child=sel) 652 except Exception: 653 ba.print_exception(f'Error restoring state for {self}.') 654 655 def _back(self) -> None: 656 from bastd.ui.mainmenu import MainMenuWindow 657 658 self._save_state() 659 ba.containerwidget( 660 edit=self._root_widget, transition=self._transition_out 661 ) 662 ba.app.ui.set_main_menu_window( 663 MainMenuWindow(transition='in_left').get_root_widget() 664 )
class
WatchWindow(ba.ui.Window):
19class WatchWindow(ba.Window): 20 """Window for watching replays.""" 21 22 class TabID(Enum): 23 """Our available tab types.""" 24 25 MY_REPLAYS = 'my_replays' 26 TEST_TAB = 'test_tab' 27 28 def __init__( 29 self, 30 transition: str | None = 'in_right', 31 origin_widget: ba.Widget | None = None, 32 ): 33 # pylint: disable=too-many-locals 34 # pylint: disable=too-many-statements 35 from bastd.ui.tabs import TabRow 36 37 ba.set_analytics_screen('Watch Window') 38 scale_origin: tuple[float, float] | None 39 if origin_widget is not None: 40 self._transition_out = 'out_scale' 41 scale_origin = origin_widget.get_screen_space_center() 42 transition = 'in_scale' 43 else: 44 self._transition_out = 'out_right' 45 scale_origin = None 46 ba.app.ui.set_main_menu_location('Watch') 47 self._tab_data: dict[str, Any] = {} 48 self._my_replays_scroll_width: float | None = None 49 self._my_replays_watch_replay_button: ba.Widget | None = None 50 self._scrollwidget: ba.Widget | None = None 51 self._columnwidget: ba.Widget | None = None 52 self._my_replay_selected: str | None = None 53 self._my_replays_rename_window: ba.Widget | None = None 54 self._my_replay_rename_text: ba.Widget | None = None 55 self._r = 'watchWindow' 56 uiscale = ba.app.ui.uiscale 57 self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 58 x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 59 self._height = ( 60 578 61 if uiscale is ba.UIScale.SMALL 62 else 670 63 if uiscale is ba.UIScale.MEDIUM 64 else 800 65 ) 66 self._current_tab: WatchWindow.TabID | None = None 67 extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 68 69 super().__init__( 70 root_widget=ba.containerwidget( 71 size=(self._width, self._height + extra_top), 72 transition=transition, 73 toolbar_visibility='menu_minimal', 74 scale_origin_stack_offset=scale_origin, 75 scale=( 76 1.3 77 if uiscale is ba.UIScale.SMALL 78 else 0.97 79 if uiscale is ba.UIScale.MEDIUM 80 else 0.8 81 ), 82 stack_offset=(0, -10) 83 if uiscale is ba.UIScale.SMALL 84 else (0, 15) 85 if uiscale is ba.UIScale.MEDIUM 86 else (0, 0), 87 ) 88 ) 89 90 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 91 ba.containerwidget( 92 edit=self._root_widget, on_cancel_call=self._back 93 ) 94 self._back_button = None 95 else: 96 self._back_button = btn = ba.buttonwidget( 97 parent=self._root_widget, 98 autoselect=True, 99 position=(70 + x_inset, self._height - 74), 100 size=(140, 60), 101 scale=1.1, 102 label=ba.Lstr(resource='backText'), 103 button_type='back', 104 on_activate_call=self._back, 105 ) 106 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 107 ba.buttonwidget( 108 edit=btn, 109 button_type='backSmall', 110 size=(60, 60), 111 label=ba.charstr(ba.SpecialChar.BACK), 112 ) 113 114 ba.textwidget( 115 parent=self._root_widget, 116 position=(self._width * 0.5, self._height - 38), 117 size=(0, 0), 118 color=ba.app.ui.title_color, 119 scale=1.5, 120 h_align='center', 121 v_align='center', 122 text=ba.Lstr(resource=self._r + '.titleText'), 123 maxwidth=400, 124 ) 125 126 tabdefs = [ 127 ( 128 self.TabID.MY_REPLAYS, 129 ba.Lstr(resource=self._r + '.myReplaysText'), 130 ), 131 # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')), 132 ] 133 134 scroll_buffer_h = 130 + 2 * x_inset 135 tab_buffer_h = 750 + 2 * x_inset 136 137 self._tab_row = TabRow( 138 self._root_widget, 139 tabdefs, 140 pos=(tab_buffer_h * 0.5, self._height - 130), 141 size=(self._width - tab_buffer_h, 50), 142 on_select_call=self._set_tab, 143 ) 144 145 if ba.app.ui.use_toolbars: 146 first_tab = self._tab_row.tabs[tabdefs[0][0]] 147 last_tab = self._tab_row.tabs[tabdefs[-1][0]] 148 ba.widget( 149 edit=last_tab.button, 150 right_widget=ba.internal.get_special_widget('party_button'), 151 ) 152 if uiscale is ba.UIScale.SMALL: 153 bbtn = ba.internal.get_special_widget('back_button') 154 ba.widget( 155 edit=first_tab.button, up_widget=bbtn, left_widget=bbtn 156 ) 157 158 self._scroll_width = self._width - scroll_buffer_h 159 self._scroll_height = self._height - 180 160 161 # Not actually using a scroll widget anymore; just an image. 162 scroll_left = (self._width - self._scroll_width) * 0.5 163 scroll_bottom = self._height - self._scroll_height - 79 - 48 164 buffer_h = 10 165 buffer_v = 4 166 ba.imagewidget( 167 parent=self._root_widget, 168 position=(scroll_left - buffer_h, scroll_bottom - buffer_v), 169 size=( 170 self._scroll_width + 2 * buffer_h, 171 self._scroll_height + 2 * buffer_v, 172 ), 173 texture=ba.gettexture('scrollWidget'), 174 model_transparent=ba.getmodel('softEdgeOutside'), 175 ) 176 self._tab_container: ba.Widget | None = None 177 178 self._restore_state() 179 180 def _set_tab(self, tab_id: TabID) -> None: 181 # pylint: disable=too-many-locals 182 183 if self._current_tab == tab_id: 184 return 185 self._current_tab = tab_id 186 187 # Preserve our current tab between runs. 188 cfg = ba.app.config 189 cfg['Watch Tab'] = tab_id.value 190 cfg.commit() 191 192 # Update tab colors based on which is selected. 193 # tabs.update_tab_button_colors(self._tab_buttons, tab) 194 self._tab_row.update_appearance(tab_id) 195 196 if self._tab_container: 197 self._tab_container.delete() 198 scroll_left = (self._width - self._scroll_width) * 0.5 199 scroll_bottom = self._height - self._scroll_height - 79 - 48 200 201 # A place where tabs can store data to get cleared when 202 # switching to a different tab 203 self._tab_data = {} 204 205 uiscale = ba.app.ui.uiscale 206 if tab_id is self.TabID.MY_REPLAYS: 207 c_width = self._scroll_width 208 c_height = self._scroll_height - 20 209 sub_scroll_height = c_height - 63 210 self._my_replays_scroll_width = sub_scroll_width = ( 211 680 if uiscale is ba.UIScale.SMALL else 640 212 ) 213 214 self._tab_container = cnt = ba.containerwidget( 215 parent=self._root_widget, 216 position=( 217 scroll_left, 218 scroll_bottom + (self._scroll_height - c_height) * 0.5, 219 ), 220 size=(c_width, c_height), 221 background=False, 222 selection_loops_to_parent=True, 223 ) 224 225 v = c_height - 30 226 ba.textwidget( 227 parent=cnt, 228 position=(c_width * 0.5, v), 229 color=(0.6, 1.0, 0.6), 230 scale=0.7, 231 size=(0, 0), 232 maxwidth=c_width * 0.9, 233 h_align='center', 234 v_align='center', 235 text=ba.Lstr( 236 resource='replayRenameWarningText', 237 subs=[ 238 ('${REPLAY}', ba.Lstr(resource='replayNameDefaultText')) 239 ], 240 ), 241 ) 242 243 b_width = 140 if uiscale is ba.UIScale.SMALL else 178 244 b_height = ( 245 107 246 if uiscale is ba.UIScale.SMALL 247 else 142 248 if uiscale is ba.UIScale.MEDIUM 249 else 190 250 ) 251 b_space_extra = ( 252 0 253 if uiscale is ba.UIScale.SMALL 254 else -2 255 if uiscale is ba.UIScale.MEDIUM 256 else -5 257 ) 258 259 b_color = (0.6, 0.53, 0.63) 260 b_textcolor = (0.75, 0.7, 0.8) 261 btnv = ( 262 c_height 263 - ( 264 48 265 if uiscale is ba.UIScale.SMALL 266 else 45 267 if uiscale is ba.UIScale.MEDIUM 268 else 40 269 ) 270 - b_height 271 ) 272 btnh = 40 if uiscale is ba.UIScale.SMALL else 40 273 smlh = 190 if uiscale is ba.UIScale.SMALL else 225 274 tscl = 1.0 if uiscale is ba.UIScale.SMALL else 1.2 275 self._my_replays_watch_replay_button = btn1 = ba.buttonwidget( 276 parent=cnt, 277 size=(b_width, b_height), 278 position=(btnh, btnv), 279 button_type='square', 280 color=b_color, 281 textcolor=b_textcolor, 282 on_activate_call=self._on_my_replay_play_press, 283 text_scale=tscl, 284 label=ba.Lstr(resource=self._r + '.watchReplayButtonText'), 285 autoselect=True, 286 ) 287 ba.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button) 288 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 289 ba.widget( 290 edit=btn1, 291 left_widget=ba.internal.get_special_widget('back_button'), 292 ) 293 btnv -= b_height + b_space_extra 294 ba.buttonwidget( 295 parent=cnt, 296 size=(b_width, b_height), 297 position=(btnh, btnv), 298 button_type='square', 299 color=b_color, 300 textcolor=b_textcolor, 301 on_activate_call=self._on_my_replay_rename_press, 302 text_scale=tscl, 303 label=ba.Lstr(resource=self._r + '.renameReplayButtonText'), 304 autoselect=True, 305 ) 306 btnv -= b_height + b_space_extra 307 ba.buttonwidget( 308 parent=cnt, 309 size=(b_width, b_height), 310 position=(btnh, btnv), 311 button_type='square', 312 color=b_color, 313 textcolor=b_textcolor, 314 on_activate_call=self._on_my_replay_delete_press, 315 text_scale=tscl, 316 label=ba.Lstr(resource=self._r + '.deleteReplayButtonText'), 317 autoselect=True, 318 ) 319 320 v -= sub_scroll_height + 23 321 self._scrollwidget = scrlw = ba.scrollwidget( 322 parent=cnt, 323 position=(smlh, v), 324 size=(sub_scroll_width, sub_scroll_height), 325 ) 326 ba.containerwidget(edit=cnt, selected_child=scrlw) 327 self._columnwidget = ba.columnwidget( 328 parent=scrlw, left_border=10, border=2, margin=0 329 ) 330 331 ba.widget( 332 edit=scrlw, 333 autoselect=True, 334 left_widget=btn1, 335 up_widget=self._tab_row.tabs[tab_id].button, 336 ) 337 ba.widget(edit=self._tab_row.tabs[tab_id].button, down_widget=scrlw) 338 339 self._my_replay_selected = None 340 self._refresh_my_replays() 341 342 def _no_replay_selected_error(self) -> None: 343 ba.screenmessage( 344 ba.Lstr(resource=self._r + '.noReplaySelectedErrorText'), 345 color=(1, 0, 0), 346 ) 347 ba.playsound(ba.getsound('error')) 348 349 def _on_my_replay_play_press(self) -> None: 350 if self._my_replay_selected is None: 351 self._no_replay_selected_error() 352 return 353 ba.internal.increment_analytics_count('Replay watch') 354 355 def do_it() -> None: 356 try: 357 # Reset to normal speed. 358 ba.internal.set_replay_speed_exponent(0) 359 ba.internal.fade_screen(True) 360 assert self._my_replay_selected is not None 361 ba.internal.new_replay_session( 362 ba.internal.get_replays_dir() 363 + '/' 364 + self._my_replay_selected 365 ) 366 except Exception: 367 ba.print_exception('Error running replay session.') 368 369 # Drop back into a fresh main menu session 370 # in case we half-launched or something. 371 from bastd import mainmenu 372 373 ba.internal.new_host_session(mainmenu.MainMenuSession) 374 375 ba.internal.fade_screen(False, endcall=ba.Call(ba.pushcall, do_it)) 376 ba.containerwidget(edit=self._root_widget, transition='out_left') 377 378 def _on_my_replay_rename_press(self) -> None: 379 if self._my_replay_selected is None: 380 self._no_replay_selected_error() 381 return 382 c_width = 600 383 c_height = 250 384 uiscale = ba.app.ui.uiscale 385 self._my_replays_rename_window = cnt = ba.containerwidget( 386 scale=( 387 1.8 388 if uiscale is ba.UIScale.SMALL 389 else 1.55 390 if uiscale is ba.UIScale.MEDIUM 391 else 1.0 392 ), 393 size=(c_width, c_height), 394 transition='in_scale', 395 ) 396 dname = self._get_replay_display_name(self._my_replay_selected) 397 ba.textwidget( 398 parent=cnt, 399 size=(0, 0), 400 h_align='center', 401 v_align='center', 402 text=ba.Lstr( 403 resource=self._r + '.renameReplayText', 404 subs=[('${REPLAY}', dname)], 405 ), 406 maxwidth=c_width * 0.8, 407 position=(c_width * 0.5, c_height - 60), 408 ) 409 self._my_replay_rename_text = txt = ba.textwidget( 410 parent=cnt, 411 size=(c_width * 0.8, 40), 412 h_align='left', 413 v_align='center', 414 text=dname, 415 editable=True, 416 description=ba.Lstr(resource=self._r + '.replayNameText'), 417 position=(c_width * 0.1, c_height - 140), 418 autoselect=True, 419 maxwidth=c_width * 0.7, 420 max_chars=200, 421 ) 422 cbtn = ba.buttonwidget( 423 parent=cnt, 424 label=ba.Lstr(resource='cancelText'), 425 on_activate_call=ba.Call( 426 lambda c: ba.containerwidget(edit=c, transition='out_scale'), 427 cnt, 428 ), 429 size=(180, 60), 430 position=(30, 30), 431 autoselect=True, 432 ) 433 okb = ba.buttonwidget( 434 parent=cnt, 435 label=ba.Lstr(resource=self._r + '.renameText'), 436 size=(180, 60), 437 position=(c_width - 230, 30), 438 on_activate_call=ba.Call( 439 self._rename_my_replay, self._my_replay_selected 440 ), 441 autoselect=True, 442 ) 443 ba.widget(edit=cbtn, right_widget=okb) 444 ba.widget(edit=okb, left_widget=cbtn) 445 ba.textwidget(edit=txt, on_return_press_call=okb.activate) 446 ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) 447 448 def _rename_my_replay(self, replay: str) -> None: 449 new_name = None 450 try: 451 if not self._my_replay_rename_text: 452 return 453 new_name_raw = cast( 454 str, ba.textwidget(query=self._my_replay_rename_text) 455 ) 456 new_name = new_name_raw + '.brp' 457 458 # Ignore attempts to change it to what it already is 459 # (or what it looks like to the user). 460 if ( 461 replay != new_name 462 and self._get_replay_display_name(replay) != new_name_raw 463 ): 464 old_name_full = ( 465 ba.internal.get_replays_dir() + '/' + replay 466 ).encode('utf-8') 467 new_name_full = ( 468 ba.internal.get_replays_dir() + '/' + new_name 469 ).encode('utf-8') 470 # False alarm; ba.textwidget can return non-None val. 471 # pylint: disable=unsupported-membership-test 472 if os.path.exists(new_name_full): 473 ba.playsound(ba.getsound('error')) 474 ba.screenmessage( 475 ba.Lstr( 476 resource=self._r 477 + '.replayRenameErrorAlreadyExistsText' 478 ), 479 color=(1, 0, 0), 480 ) 481 elif any(char in new_name_raw for char in ['/', '\\', ':']): 482 ba.playsound(ba.getsound('error')) 483 ba.screenmessage( 484 ba.Lstr( 485 resource=self._r + '.replayRenameErrorInvalidName' 486 ), 487 color=(1, 0, 0), 488 ) 489 else: 490 ba.internal.increment_analytics_count('Replay rename') 491 os.rename(old_name_full, new_name_full) 492 self._refresh_my_replays() 493 ba.playsound(ba.getsound('gunCocking')) 494 except Exception: 495 ba.print_exception( 496 f"Error renaming replay '{replay}' to '{new_name}'." 497 ) 498 ba.playsound(ba.getsound('error')) 499 ba.screenmessage( 500 ba.Lstr(resource=self._r + '.replayRenameErrorText'), 501 color=(1, 0, 0), 502 ) 503 504 ba.containerwidget( 505 edit=self._my_replays_rename_window, transition='out_scale' 506 ) 507 508 def _on_my_replay_delete_press(self) -> None: 509 from bastd.ui import confirm 510 511 if self._my_replay_selected is None: 512 self._no_replay_selected_error() 513 return 514 confirm.ConfirmWindow( 515 ba.Lstr( 516 resource=self._r + '.deleteConfirmText', 517 subs=[ 518 ( 519 '${REPLAY}', 520 self._get_replay_display_name(self._my_replay_selected), 521 ) 522 ], 523 ), 524 ba.Call(self._delete_replay, self._my_replay_selected), 525 450, 526 150, 527 ) 528 529 def _get_replay_display_name(self, replay: str) -> str: 530 if replay.endswith('.brp'): 531 replay = replay[:-4] 532 if replay == '__lastReplay': 533 return ba.Lstr(resource='replayNameDefaultText').evaluate() 534 return replay 535 536 def _delete_replay(self, replay: str) -> None: 537 try: 538 ba.internal.increment_analytics_count('Replay delete') 539 os.remove( 540 (ba.internal.get_replays_dir() + '/' + replay).encode('utf-8') 541 ) 542 self._refresh_my_replays() 543 ba.playsound(ba.getsound('shieldDown')) 544 if replay == self._my_replay_selected: 545 self._my_replay_selected = None 546 except Exception: 547 ba.print_exception(f"Error deleting replay '{replay}'.") 548 ba.playsound(ba.getsound('error')) 549 ba.screenmessage( 550 ba.Lstr(resource=self._r + '.replayDeleteErrorText'), 551 color=(1, 0, 0), 552 ) 553 554 def _on_my_replay_select(self, replay: str) -> None: 555 self._my_replay_selected = replay 556 557 def _refresh_my_replays(self) -> None: 558 assert self._columnwidget is not None 559 for child in self._columnwidget.get_children(): 560 child.delete() 561 t_scale = 1.6 562 try: 563 names = os.listdir(ba.internal.get_replays_dir()) 564 565 # Ignore random other files in there. 566 names = [n for n in names if n.endswith('.brp')] 567 names.sort(key=lambda x: x.lower()) 568 except Exception: 569 ba.print_exception('Error listing replays dir.') 570 names = [] 571 572 assert self._my_replays_scroll_width is not None 573 assert self._my_replays_watch_replay_button is not None 574 for i, name in enumerate(names): 575 txt = ba.textwidget( 576 parent=self._columnwidget, 577 size=(self._my_replays_scroll_width / t_scale, 30), 578 selectable=True, 579 color=(1.0, 1, 0.4) 580 if name == '__lastReplay.brp' 581 else (1, 1, 1), 582 always_highlight=True, 583 on_select_call=ba.Call(self._on_my_replay_select, name), 584 on_activate_call=self._my_replays_watch_replay_button.activate, 585 text=self._get_replay_display_name(name), 586 h_align='left', 587 v_align='center', 588 corner_scale=t_scale, 589 maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93, 590 ) 591 if i == 0: 592 ba.widget( 593 edit=txt, 594 up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button, 595 ) 596 597 def _save_state(self) -> None: 598 try: 599 sel = self._root_widget.get_selected_child() 600 selected_tab_ids = [ 601 tab_id 602 for tab_id, tab in self._tab_row.tabs.items() 603 if sel == tab.button 604 ] 605 if sel == self._back_button: 606 sel_name = 'Back' 607 elif selected_tab_ids: 608 assert len(selected_tab_ids) == 1 609 sel_name = f'Tab:{selected_tab_ids[0].value}' 610 elif sel == self._tab_container: 611 sel_name = 'TabContainer' 612 else: 613 raise ValueError(f'unrecognized selection {sel}') 614 ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} 615 except Exception: 616 ba.print_exception(f'Error saving state for {self}.') 617 618 def _restore_state(self) -> None: 619 from efro.util import enum_by_value 620 621 try: 622 sel: ba.Widget | None 623 sel_name = ba.app.ui.window_states.get(type(self), {}).get( 624 'sel_name' 625 ) 626 assert isinstance(sel_name, (str, type(None))) 627 try: 628 current_tab = enum_by_value( 629 self.TabID, ba.app.config.get('Watch Tab') 630 ) 631 except ValueError: 632 current_tab = self.TabID.MY_REPLAYS 633 self._set_tab(current_tab) 634 635 if sel_name == 'Back': 636 sel = self._back_button 637 elif sel_name == 'TabContainer': 638 sel = self._tab_container 639 elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): 640 try: 641 sel_tab_id = enum_by_value( 642 self.TabID, sel_name.split(':')[-1] 643 ) 644 except ValueError: 645 sel_tab_id = self.TabID.MY_REPLAYS 646 sel = self._tab_row.tabs[sel_tab_id].button 647 else: 648 if self._tab_container is not None: 649 sel = self._tab_container 650 else: 651 sel = self._tab_row.tabs[current_tab].button 652 ba.containerwidget(edit=self._root_widget, selected_child=sel) 653 except Exception: 654 ba.print_exception(f'Error restoring state for {self}.') 655 656 def _back(self) -> None: 657 from bastd.ui.mainmenu import MainMenuWindow 658 659 self._save_state() 660 ba.containerwidget( 661 edit=self._root_widget, transition=self._transition_out 662 ) 663 ba.app.ui.set_main_menu_window( 664 MainMenuWindow(transition='in_left').get_root_widget() 665 )
Window for watching replays.
WatchWindow( transition: str | None = 'in_right', origin_widget: _ba.Widget | None = None)
28 def __init__( 29 self, 30 transition: str | None = 'in_right', 31 origin_widget: ba.Widget | None = None, 32 ): 33 # pylint: disable=too-many-locals 34 # pylint: disable=too-many-statements 35 from bastd.ui.tabs import TabRow 36 37 ba.set_analytics_screen('Watch Window') 38 scale_origin: tuple[float, float] | None 39 if origin_widget is not None: 40 self._transition_out = 'out_scale' 41 scale_origin = origin_widget.get_screen_space_center() 42 transition = 'in_scale' 43 else: 44 self._transition_out = 'out_right' 45 scale_origin = None 46 ba.app.ui.set_main_menu_location('Watch') 47 self._tab_data: dict[str, Any] = {} 48 self._my_replays_scroll_width: float | None = None 49 self._my_replays_watch_replay_button: ba.Widget | None = None 50 self._scrollwidget: ba.Widget | None = None 51 self._columnwidget: ba.Widget | None = None 52 self._my_replay_selected: str | None = None 53 self._my_replays_rename_window: ba.Widget | None = None 54 self._my_replay_rename_text: ba.Widget | None = None 55 self._r = 'watchWindow' 56 uiscale = ba.app.ui.uiscale 57 self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 58 x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 59 self._height = ( 60 578 61 if uiscale is ba.UIScale.SMALL 62 else 670 63 if uiscale is ba.UIScale.MEDIUM 64 else 800 65 ) 66 self._current_tab: WatchWindow.TabID | None = None 67 extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 68 69 super().__init__( 70 root_widget=ba.containerwidget( 71 size=(self._width, self._height + extra_top), 72 transition=transition, 73 toolbar_visibility='menu_minimal', 74 scale_origin_stack_offset=scale_origin, 75 scale=( 76 1.3 77 if uiscale is ba.UIScale.SMALL 78 else 0.97 79 if uiscale is ba.UIScale.MEDIUM 80 else 0.8 81 ), 82 stack_offset=(0, -10) 83 if uiscale is ba.UIScale.SMALL 84 else (0, 15) 85 if uiscale is ba.UIScale.MEDIUM 86 else (0, 0), 87 ) 88 ) 89 90 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 91 ba.containerwidget( 92 edit=self._root_widget, on_cancel_call=self._back 93 ) 94 self._back_button = None 95 else: 96 self._back_button = btn = ba.buttonwidget( 97 parent=self._root_widget, 98 autoselect=True, 99 position=(70 + x_inset, self._height - 74), 100 size=(140, 60), 101 scale=1.1, 102 label=ba.Lstr(resource='backText'), 103 button_type='back', 104 on_activate_call=self._back, 105 ) 106 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 107 ba.buttonwidget( 108 edit=btn, 109 button_type='backSmall', 110 size=(60, 60), 111 label=ba.charstr(ba.SpecialChar.BACK), 112 ) 113 114 ba.textwidget( 115 parent=self._root_widget, 116 position=(self._width * 0.5, self._height - 38), 117 size=(0, 0), 118 color=ba.app.ui.title_color, 119 scale=1.5, 120 h_align='center', 121 v_align='center', 122 text=ba.Lstr(resource=self._r + '.titleText'), 123 maxwidth=400, 124 ) 125 126 tabdefs = [ 127 ( 128 self.TabID.MY_REPLAYS, 129 ba.Lstr(resource=self._r + '.myReplaysText'), 130 ), 131 # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')), 132 ] 133 134 scroll_buffer_h = 130 + 2 * x_inset 135 tab_buffer_h = 750 + 2 * x_inset 136 137 self._tab_row = TabRow( 138 self._root_widget, 139 tabdefs, 140 pos=(tab_buffer_h * 0.5, self._height - 130), 141 size=(self._width - tab_buffer_h, 50), 142 on_select_call=self._set_tab, 143 ) 144 145 if ba.app.ui.use_toolbars: 146 first_tab = self._tab_row.tabs[tabdefs[0][0]] 147 last_tab = self._tab_row.tabs[tabdefs[-1][0]] 148 ba.widget( 149 edit=last_tab.button, 150 right_widget=ba.internal.get_special_widget('party_button'), 151 ) 152 if uiscale is ba.UIScale.SMALL: 153 bbtn = ba.internal.get_special_widget('back_button') 154 ba.widget( 155 edit=first_tab.button, up_widget=bbtn, left_widget=bbtn 156 ) 157 158 self._scroll_width = self._width - scroll_buffer_h 159 self._scroll_height = self._height - 180 160 161 # Not actually using a scroll widget anymore; just an image. 162 scroll_left = (self._width - self._scroll_width) * 0.5 163 scroll_bottom = self._height - self._scroll_height - 79 - 48 164 buffer_h = 10 165 buffer_v = 4 166 ba.imagewidget( 167 parent=self._root_widget, 168 position=(scroll_left - buffer_h, scroll_bottom - buffer_v), 169 size=( 170 self._scroll_width + 2 * buffer_h, 171 self._scroll_height + 2 * buffer_v, 172 ), 173 texture=ba.gettexture('scrollWidget'), 174 model_transparent=ba.getmodel('softEdgeOutside'), 175 ) 176 self._tab_container: ba.Widget | None = None 177 178 self._restore_state()
Inherited Members
- ba.ui.Window
- get_root_widget
class
WatchWindow.TabID(enum.Enum):
22 class TabID(Enum): 23 """Our available tab types.""" 24 25 MY_REPLAYS = 'my_replays' 26 TEST_TAB = 'test_tab'
Our available tab types.
Inherited Members
- enum.Enum
- name
- value