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 opacity=0.4, 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)
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 opacity=0.4, 165 ) 166 self._tab_container: bui.Widget | None = None 167 168 self._restore_state() 169 170 @override 171 def get_main_window_state(self) -> bui.MainWindowState: 172 # Support recreating our window for back/refresh purposes. 173 cls = type(self) 174 return bui.BasicMainWindowState( 175 create_call=lambda transition, origin_widget: cls( 176 transition=transition, origin_widget=origin_widget 177 ) 178 ) 179 180 @override 181 def on_main_window_close(self) -> None: 182 self._save_state() 183 184 def _set_tab(self, tab_id: TabID) -> None: 185 # pylint: disable=too-many-locals 186 187 if self._current_tab == tab_id: 188 return 189 self._current_tab = tab_id 190 191 # Preserve our current tab between runs. 192 cfg = bui.app.config 193 cfg['Watch Tab'] = tab_id.value 194 cfg.commit() 195 196 # Update tab colors based on which is selected. 197 # tabs.update_tab_button_colors(self._tab_buttons, tab) 198 self._tab_row.update_appearance(tab_id) 199 200 if self._tab_container: 201 self._tab_container.delete() 202 scroll_left = (self._width - self._scroll_width) * 0.5 203 scroll_bottom = self._height - self._scroll_height - 79 - 48 204 205 # A place where tabs can store data to get cleared when 206 # switching to a different tab 207 self._tab_data = {} 208 209 assert bui.app.classic is not None 210 uiscale = bui.app.ui_v1.uiscale 211 if tab_id is self.TabID.MY_REPLAYS: 212 c_width = self._scroll_width 213 c_height = self._scroll_height - 20 214 sub_scroll_height = c_height - 63 215 self._my_replays_scroll_width = sub_scroll_width = ( 216 680 if uiscale is bui.UIScale.SMALL else 640 217 ) 218 219 self._tab_container = cnt = bui.containerwidget( 220 parent=self._root_widget, 221 position=( 222 scroll_left, 223 scroll_bottom + (self._scroll_height - c_height) * 0.5, 224 ), 225 size=(c_width, c_height), 226 background=False, 227 selection_loops_to_parent=True, 228 ) 229 230 v = c_height - 30 231 bui.textwidget( 232 parent=cnt, 233 position=(c_width * 0.5, v), 234 color=(0.6, 1.0, 0.6), 235 scale=0.7, 236 size=(0, 0), 237 maxwidth=c_width * 0.9, 238 h_align='center', 239 v_align='center', 240 text=bui.Lstr( 241 resource='replayRenameWarningText', 242 subs=[ 243 ( 244 '${REPLAY}', 245 bui.Lstr(resource='replayNameDefaultText'), 246 ) 247 ], 248 ), 249 ) 250 251 b_width = 140 if uiscale is bui.UIScale.SMALL else 178 252 b_height = ( 253 107 254 if uiscale is bui.UIScale.SMALL 255 else 142 if uiscale is bui.UIScale.MEDIUM else 190 256 ) 257 b_space_extra = ( 258 0 259 if uiscale is bui.UIScale.SMALL 260 else -2 if uiscale is bui.UIScale.MEDIUM else -5 261 ) 262 263 b_color = (0.6, 0.53, 0.63) 264 b_textcolor = (0.75, 0.7, 0.8) 265 btnv = ( 266 c_height 267 - ( 268 48 269 if uiscale is bui.UIScale.SMALL 270 else 45 if uiscale is bui.UIScale.MEDIUM else 40 271 ) 272 - b_height 273 ) 274 btnh = 40 if uiscale is bui.UIScale.SMALL else 40 275 smlh = 190 if uiscale is bui.UIScale.SMALL else 225 276 tscl = 1.0 if uiscale is bui.UIScale.SMALL else 1.2 277 self._my_replays_watch_replay_button = btn1 = bui.buttonwidget( 278 parent=cnt, 279 size=(b_width, b_height), 280 position=(btnh, btnv), 281 button_type='square', 282 color=b_color, 283 textcolor=b_textcolor, 284 on_activate_call=self._on_my_replay_play_press, 285 text_scale=tscl, 286 label=bui.Lstr(resource=f'{self._r}.watchReplayButtonText'), 287 autoselect=True, 288 ) 289 bui.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button) 290 assert bui.app.classic is not None 291 if uiscale is bui.UIScale.SMALL: 292 bui.widget( 293 edit=btn1, 294 left_widget=bui.get_special_widget('back_button'), 295 ) 296 btnv -= b_height + b_space_extra 297 bui.buttonwidget( 298 parent=cnt, 299 size=(b_width, b_height), 300 position=(btnh, btnv), 301 button_type='square', 302 color=b_color, 303 textcolor=b_textcolor, 304 on_activate_call=self._on_my_replay_rename_press, 305 text_scale=tscl, 306 label=bui.Lstr(resource=f'{self._r}.renameReplayButtonText'), 307 autoselect=True, 308 ) 309 btnv -= b_height + b_space_extra 310 bui.buttonwidget( 311 parent=cnt, 312 size=(b_width, b_height), 313 position=(btnh, btnv), 314 button_type='square', 315 color=b_color, 316 textcolor=b_textcolor, 317 on_activate_call=self._on_my_replay_delete_press, 318 text_scale=tscl, 319 label=bui.Lstr(resource=f'{self._r}.deleteReplayButtonText'), 320 autoselect=True, 321 ) 322 323 v -= sub_scroll_height + 23 324 self._scrollwidget = scrlw = bui.scrollwidget( 325 parent=cnt, 326 position=(smlh, v), 327 size=(sub_scroll_width, sub_scroll_height), 328 ) 329 bui.containerwidget(edit=cnt, selected_child=scrlw) 330 self._columnwidget = bui.columnwidget( 331 parent=scrlw, left_border=10, border=2, margin=0 332 ) 333 334 bui.widget( 335 edit=scrlw, 336 autoselect=True, 337 left_widget=btn1, 338 up_widget=self._tab_row.tabs[tab_id].button, 339 ) 340 bui.widget( 341 edit=self._tab_row.tabs[tab_id].button, down_widget=scrlw 342 ) 343 344 self._my_replay_selected = None 345 self._refresh_my_replays() 346 347 def _no_replay_selected_error(self) -> None: 348 bui.screenmessage( 349 bui.Lstr(resource=f'{self._r}.noReplaySelectedErrorText'), 350 color=(1, 0, 0), 351 ) 352 bui.getsound('error').play() 353 354 def _on_my_replay_play_press(self) -> None: 355 if self._my_replay_selected is None: 356 self._no_replay_selected_error() 357 return 358 bui.increment_analytics_count('Replay watch') 359 360 # Save our place in the UI so we return there when done. 361 if bui.app.classic is not None: 362 bui.app.classic.save_ui_state() 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 f'{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 if uiscale is bui.UIScale.MEDIUM else 1.0 398 ), 399 size=(c_width, c_height), 400 transition='in_scale', 401 ) 402 dname = self._get_replay_display_name(self._my_replay_selected) 403 bui.textwidget( 404 parent=cnt, 405 size=(0, 0), 406 h_align='center', 407 v_align='center', 408 text=bui.Lstr( 409 resource=f'{self._r}.renameReplayText', 410 subs=[('${REPLAY}', dname)], 411 ), 412 maxwidth=c_width * 0.8, 413 position=(c_width * 0.5, c_height - 60), 414 ) 415 self._my_replay_rename_text = txt = bui.textwidget( 416 parent=cnt, 417 size=(c_width * 0.8, 40), 418 h_align='left', 419 v_align='center', 420 text=dname, 421 editable=True, 422 description=bui.Lstr(resource=f'{self._r}.replayNameText'), 423 position=(c_width * 0.1, c_height - 140), 424 autoselect=True, 425 maxwidth=c_width * 0.7, 426 max_chars=200, 427 ) 428 cbtn = bui.buttonwidget( 429 parent=cnt, 430 label=bui.Lstr(resource='cancelText'), 431 on_activate_call=bui.Call( 432 lambda c: bui.containerwidget(edit=c, transition='out_scale'), 433 cnt, 434 ), 435 size=(180, 60), 436 position=(30, 30), 437 autoselect=True, 438 ) 439 okb = bui.buttonwidget( 440 parent=cnt, 441 label=bui.Lstr(resource=f'{self._r}.renameText'), 442 size=(180, 60), 443 position=(c_width - 230, 30), 444 on_activate_call=bui.Call( 445 self._rename_my_replay, self._my_replay_selected 446 ), 447 autoselect=True, 448 ) 449 bui.widget(edit=cbtn, right_widget=okb) 450 bui.widget(edit=okb, left_widget=cbtn) 451 bui.textwidget(edit=txt, on_return_press_call=okb.activate) 452 bui.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) 453 454 def _rename_my_replay(self, replay: str) -> None: 455 new_name = None 456 try: 457 if not self._my_replay_rename_text: 458 return 459 new_name_raw = cast( 460 str, bui.textwidget(query=self._my_replay_rename_text) 461 ) 462 new_name = new_name_raw + '.brp' 463 464 # Ignore attempts to change it to what it already is 465 # (or what it looks like to the user). 466 if ( 467 replay != new_name 468 and self._get_replay_display_name(replay) != new_name_raw 469 ): 470 old_name_full = (bui.get_replays_dir() + '/' + replay).encode( 471 'utf-8' 472 ) 473 new_name_full = (bui.get_replays_dir() + '/' + new_name).encode( 474 'utf-8' 475 ) 476 # False alarm; bui.textwidget can return non-None val. 477 # pylint: disable=unsupported-membership-test 478 if os.path.exists(new_name_full): 479 bui.getsound('error').play() 480 bui.screenmessage( 481 bui.Lstr( 482 resource=self._r 483 + '.replayRenameErrorAlreadyExistsText' 484 ), 485 color=(1, 0, 0), 486 ) 487 elif any(char in new_name_raw for char in ['/', '\\', ':']): 488 bui.getsound('error').play() 489 bui.screenmessage( 490 bui.Lstr( 491 resource=f'{self._r}.replayRenameErrorInvalidName' 492 ), 493 color=(1, 0, 0), 494 ) 495 else: 496 bui.increment_analytics_count('Replay rename') 497 os.rename(old_name_full, new_name_full) 498 self._refresh_my_replays() 499 bui.getsound('gunCocking').play() 500 except Exception: 501 logging.exception( 502 "Error renaming replay '%s' to '%s'.", replay, new_name 503 ) 504 bui.getsound('error').play() 505 bui.screenmessage( 506 bui.Lstr(resource=f'{self._r}.replayRenameErrorText'), 507 color=(1, 0, 0), 508 ) 509 510 bui.containerwidget( 511 edit=self._my_replays_rename_window, transition='out_scale' 512 ) 513 514 def _on_my_replay_delete_press(self) -> None: 515 from bauiv1lib import confirm 516 517 if self._my_replay_selected is None: 518 self._no_replay_selected_error() 519 return 520 confirm.ConfirmWindow( 521 bui.Lstr( 522 resource=f'{self._r}.deleteConfirmText', 523 subs=[ 524 ( 525 '${REPLAY}', 526 self._get_replay_display_name(self._my_replay_selected), 527 ) 528 ], 529 ), 530 bui.Call(self._delete_replay, self._my_replay_selected), 531 450, 532 150, 533 ) 534 535 def _get_replay_display_name(self, replay: str) -> str: 536 if replay.endswith('.brp'): 537 replay = replay[:-4] 538 if replay == '__lastReplay': 539 return bui.Lstr(resource='replayNameDefaultText').evaluate() 540 return replay 541 542 def _delete_replay(self, replay: str) -> None: 543 try: 544 bui.increment_analytics_count('Replay delete') 545 os.remove((bui.get_replays_dir() + '/' + replay).encode('utf-8')) 546 self._refresh_my_replays() 547 bui.getsound('shieldDown').play() 548 if replay == self._my_replay_selected: 549 self._my_replay_selected = None 550 except Exception: 551 logging.exception("Error deleting replay '%s'.", replay) 552 bui.getsound('error').play() 553 bui.screenmessage( 554 bui.Lstr(resource=f'{self._r}.replayDeleteErrorText'), 555 color=(1, 0, 0), 556 ) 557 558 def _on_my_replay_select(self, replay: str) -> None: 559 self._my_replay_selected = replay 560 561 def _refresh_my_replays(self) -> None: 562 assert self._columnwidget is not None 563 for child in self._columnwidget.get_children(): 564 child.delete() 565 t_scale = 1.6 566 try: 567 names = os.listdir(bui.get_replays_dir()) 568 569 # Ignore random other files in there. 570 names = [n for n in names if n.endswith('.brp')] 571 names.sort(key=lambda x: x.lower()) 572 except Exception: 573 logging.exception('Error listing replays dir.') 574 names = [] 575 576 assert self._my_replays_scroll_width is not None 577 assert self._my_replays_watch_replay_button is not None 578 for i, name in enumerate(names): 579 txt = bui.textwidget( 580 parent=self._columnwidget, 581 size=(self._my_replays_scroll_width / t_scale, 30), 582 selectable=True, 583 color=( 584 (1.0, 1, 0.4) if name == '__lastReplay.brp' else (1, 1, 1) 585 ), 586 always_highlight=True, 587 on_select_call=bui.Call(self._on_my_replay_select, name), 588 on_activate_call=self._my_replays_watch_replay_button.activate, 589 text=self._get_replay_display_name(name), 590 h_align='left', 591 v_align='center', 592 corner_scale=t_scale, 593 maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93, 594 ) 595 if i == 0: 596 bui.widget( 597 edit=txt, 598 up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button, 599 ) 600 self._my_replay_selected = name 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 try: 626 sel: bui.Widget | None 627 assert bui.app.classic is not None 628 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 629 'sel_name' 630 ) 631 assert isinstance(sel_name, (str, type(None))) 632 try: 633 current_tab = self.TabID(bui.app.config.get('Watch Tab')) 634 except ValueError: 635 current_tab = self.TabID.MY_REPLAYS 636 self._set_tab(current_tab) 637 638 if sel_name == 'Back': 639 sel = self._back_button 640 elif sel_name == 'TabContainer': 641 sel = self._tab_container 642 elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): 643 try: 644 sel_tab_id = self.TabID(sel_name.split(':')[-1]) 645 except ValueError: 646 sel_tab_id = self.TabID.MY_REPLAYS 647 sel = self._tab_row.tabs[sel_tab_id].button 648 else: 649 if self._tab_container is not None: 650 sel = self._tab_container 651 else: 652 sel = self._tab_row.tabs[current_tab].button 653 bui.containerwidget(edit=self._root_widget, selected_child=sel) 654 except Exception: 655 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 opacity=0.4, 165 ) 166 self._tab_container: bui.Widget | None = None 167 168 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.
170 @override 171 def get_main_window_state(self) -> bui.MainWindowState: 172 # Support recreating our window for back/refresh purposes. 173 cls = type(self) 174 return bui.BasicMainWindowState( 175 create_call=lambda transition, origin_widget: cls( 176 transition=transition, origin_widget=origin_widget 177 ) 178 )
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.