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