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