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