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 def do_it() -> None: 359 try: 360 # Reset to normal speed. 361 bs.set_replay_speed_exponent(0) 362 bui.fade_screen(True) 363 assert self._my_replay_selected is not None 364 bs.new_replay_session( 365 f'{bui.get_replays_dir()}/{self._my_replay_selected}' 366 ) 367 except Exception: 368 logging.exception('Error running replay session.') 369 370 # Drop back into a fresh main menu session 371 # in case we half-launched or something. 372 from bascenev1lib import mainmenu 373 374 bs.new_host_session(mainmenu.MainMenuSession) 375 376 bui.fade_screen(False, endcall=bui.Call(bui.pushcall, do_it)) 377 bui.containerwidget(edit=self._root_widget, transition='out_left') 378 379 def _on_my_replay_rename_press(self) -> None: 380 if self._my_replay_selected is None: 381 self._no_replay_selected_error() 382 return 383 c_width = 600 384 c_height = 250 385 assert bui.app.classic is not None 386 uiscale = bui.app.ui_v1.uiscale 387 self._my_replays_rename_window = cnt = bui.containerwidget( 388 scale=( 389 1.8 390 if uiscale is bui.UIScale.SMALL 391 else 1.55 if uiscale is bui.UIScale.MEDIUM else 1.0 392 ), 393 size=(c_width, c_height), 394 transition='in_scale', 395 ) 396 dname = self._get_replay_display_name(self._my_replay_selected) 397 bui.textwidget( 398 parent=cnt, 399 size=(0, 0), 400 h_align='center', 401 v_align='center', 402 text=bui.Lstr( 403 resource=f'{self._r}.renameReplayText', 404 subs=[('${REPLAY}', dname)], 405 ), 406 maxwidth=c_width * 0.8, 407 position=(c_width * 0.5, c_height - 60), 408 ) 409 self._my_replay_rename_text = txt = bui.textwidget( 410 parent=cnt, 411 size=(c_width * 0.8, 40), 412 h_align='left', 413 v_align='center', 414 text=dname, 415 editable=True, 416 description=bui.Lstr(resource=f'{self._r}.replayNameText'), 417 position=(c_width * 0.1, c_height - 140), 418 autoselect=True, 419 maxwidth=c_width * 0.7, 420 max_chars=200, 421 ) 422 cbtn = bui.buttonwidget( 423 parent=cnt, 424 label=bui.Lstr(resource='cancelText'), 425 on_activate_call=bui.Call( 426 lambda c: bui.containerwidget(edit=c, transition='out_scale'), 427 cnt, 428 ), 429 size=(180, 60), 430 position=(30, 30), 431 autoselect=True, 432 ) 433 okb = bui.buttonwidget( 434 parent=cnt, 435 label=bui.Lstr(resource=f'{self._r}.renameText'), 436 size=(180, 60), 437 position=(c_width - 230, 30), 438 on_activate_call=bui.Call( 439 self._rename_my_replay, self._my_replay_selected 440 ), 441 autoselect=True, 442 ) 443 bui.widget(edit=cbtn, right_widget=okb) 444 bui.widget(edit=okb, left_widget=cbtn) 445 bui.textwidget(edit=txt, on_return_press_call=okb.activate) 446 bui.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) 447 448 def _rename_my_replay(self, replay: str) -> None: 449 new_name = None 450 try: 451 if not self._my_replay_rename_text: 452 return 453 new_name_raw = cast( 454 str, bui.textwidget(query=self._my_replay_rename_text) 455 ) 456 new_name = new_name_raw + '.brp' 457 458 # Ignore attempts to change it to what it already is 459 # (or what it looks like to the user). 460 if ( 461 replay != new_name 462 and self._get_replay_display_name(replay) != new_name_raw 463 ): 464 old_name_full = (bui.get_replays_dir() + '/' + replay).encode( 465 'utf-8' 466 ) 467 new_name_full = (bui.get_replays_dir() + '/' + new_name).encode( 468 'utf-8' 469 ) 470 # False alarm; bui.textwidget can return non-None val. 471 # pylint: disable=unsupported-membership-test 472 if os.path.exists(new_name_full): 473 bui.getsound('error').play() 474 bui.screenmessage( 475 bui.Lstr( 476 resource=self._r 477 + '.replayRenameErrorAlreadyExistsText' 478 ), 479 color=(1, 0, 0), 480 ) 481 elif any(char in new_name_raw for char in ['/', '\\', ':']): 482 bui.getsound('error').play() 483 bui.screenmessage( 484 bui.Lstr( 485 resource=f'{self._r}.replayRenameErrorInvalidName' 486 ), 487 color=(1, 0, 0), 488 ) 489 else: 490 bui.increment_analytics_count('Replay rename') 491 os.rename(old_name_full, new_name_full) 492 self._refresh_my_replays() 493 bui.getsound('gunCocking').play() 494 except Exception: 495 logging.exception( 496 "Error renaming replay '%s' to '%s'.", replay, new_name 497 ) 498 bui.getsound('error').play() 499 bui.screenmessage( 500 bui.Lstr(resource=f'{self._r}.replayRenameErrorText'), 501 color=(1, 0, 0), 502 ) 503 504 bui.containerwidget( 505 edit=self._my_replays_rename_window, transition='out_scale' 506 ) 507 508 def _on_my_replay_delete_press(self) -> None: 509 from bauiv1lib import confirm 510 511 if self._my_replay_selected is None: 512 self._no_replay_selected_error() 513 return 514 confirm.ConfirmWindow( 515 bui.Lstr( 516 resource=f'{self._r}.deleteConfirmText', 517 subs=[ 518 ( 519 '${REPLAY}', 520 self._get_replay_display_name(self._my_replay_selected), 521 ) 522 ], 523 ), 524 bui.Call(self._delete_replay, self._my_replay_selected), 525 450, 526 150, 527 ) 528 529 def _get_replay_display_name(self, replay: str) -> str: 530 if replay.endswith('.brp'): 531 replay = replay[:-4] 532 if replay == '__lastReplay': 533 return bui.Lstr(resource='replayNameDefaultText').evaluate() 534 return replay 535 536 def _delete_replay(self, replay: str) -> None: 537 try: 538 bui.increment_analytics_count('Replay delete') 539 os.remove((bui.get_replays_dir() + '/' + replay).encode('utf-8')) 540 self._refresh_my_replays() 541 bui.getsound('shieldDown').play() 542 if replay == self._my_replay_selected: 543 self._my_replay_selected = None 544 except Exception: 545 logging.exception("Error deleting replay '%s'.", replay) 546 bui.getsound('error').play() 547 bui.screenmessage( 548 bui.Lstr(resource=f'{self._r}.replayDeleteErrorText'), 549 color=(1, 0, 0), 550 ) 551 552 def _on_my_replay_select(self, replay: str) -> None: 553 self._my_replay_selected = replay 554 555 def _refresh_my_replays(self) -> None: 556 assert self._columnwidget is not None 557 for child in self._columnwidget.get_children(): 558 child.delete() 559 t_scale = 1.6 560 try: 561 names = os.listdir(bui.get_replays_dir()) 562 563 # Ignore random other files in there. 564 names = [n for n in names if n.endswith('.brp')] 565 names.sort(key=lambda x: x.lower()) 566 except Exception: 567 logging.exception('Error listing replays dir.') 568 names = [] 569 570 assert self._my_replays_scroll_width is not None 571 assert self._my_replays_watch_replay_button is not None 572 for i, name in enumerate(names): 573 txt = bui.textwidget( 574 parent=self._columnwidget, 575 size=(self._my_replays_scroll_width / t_scale, 30), 576 selectable=True, 577 color=( 578 (1.0, 1, 0.4) if name == '__lastReplay.brp' else (1, 1, 1) 579 ), 580 always_highlight=True, 581 on_select_call=bui.Call(self._on_my_replay_select, name), 582 on_activate_call=self._my_replays_watch_replay_button.activate, 583 text=self._get_replay_display_name(name), 584 h_align='left', 585 v_align='center', 586 corner_scale=t_scale, 587 maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93, 588 ) 589 if i == 0: 590 bui.widget( 591 edit=txt, 592 up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button, 593 ) 594 self._my_replay_selected = name 595 596 def _save_state(self) -> None: 597 try: 598 sel = self._root_widget.get_selected_child() 599 selected_tab_ids = [ 600 tab_id 601 for tab_id, tab in self._tab_row.tabs.items() 602 if sel == tab.button 603 ] 604 if sel == self._back_button: 605 sel_name = 'Back' 606 elif selected_tab_ids: 607 assert len(selected_tab_ids) == 1 608 sel_name = f'Tab:{selected_tab_ids[0].value}' 609 elif sel == self._tab_container: 610 sel_name = 'TabContainer' 611 else: 612 raise ValueError(f'unrecognized selection {sel}') 613 assert bui.app.classic is not None 614 bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} 615 except Exception: 616 logging.exception('Error saving state for %s.', self) 617 618 def _restore_state(self) -> None: 619 try: 620 sel: bui.Widget | None 621 assert bui.app.classic is not None 622 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 623 'sel_name' 624 ) 625 assert isinstance(sel_name, (str, type(None))) 626 try: 627 current_tab = self.TabID(bui.app.config.get('Watch Tab')) 628 except ValueError: 629 current_tab = self.TabID.MY_REPLAYS 630 self._set_tab(current_tab) 631 632 if sel_name == 'Back': 633 sel = self._back_button 634 elif sel_name == 'TabContainer': 635 sel = self._tab_container 636 elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): 637 try: 638 sel_tab_id = self.TabID(sel_name.split(':')[-1]) 639 except ValueError: 640 sel_tab_id = self.TabID.MY_REPLAYS 641 sel = self._tab_row.tabs[sel_tab_id].button 642 else: 643 if self._tab_container is not None: 644 sel = self._tab_container 645 else: 646 sel = self._tab_row.tabs[current_tab].button 647 bui.containerwidget(edit=self._root_widget, selected_child=sel) 648 except Exception: 649 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 def do_it() -> None: 360 try: 361 # Reset to normal speed. 362 bs.set_replay_speed_exponent(0) 363 bui.fade_screen(True) 364 assert self._my_replay_selected is not None 365 bs.new_replay_session( 366 f'{bui.get_replays_dir()}/{self._my_replay_selected}' 367 ) 368 except Exception: 369 logging.exception('Error running replay session.') 370 371 # Drop back into a fresh main menu session 372 # in case we half-launched or something. 373 from bascenev1lib import mainmenu 374 375 bs.new_host_session(mainmenu.MainMenuSession) 376 377 bui.fade_screen(False, endcall=bui.Call(bui.pushcall, do_it)) 378 bui.containerwidget(edit=self._root_widget, transition='out_left') 379 380 def _on_my_replay_rename_press(self) -> None: 381 if self._my_replay_selected is None: 382 self._no_replay_selected_error() 383 return 384 c_width = 600 385 c_height = 250 386 assert bui.app.classic is not None 387 uiscale = bui.app.ui_v1.uiscale 388 self._my_replays_rename_window = cnt = bui.containerwidget( 389 scale=( 390 1.8 391 if uiscale is bui.UIScale.SMALL 392 else 1.55 if uiscale is bui.UIScale.MEDIUM else 1.0 393 ), 394 size=(c_width, c_height), 395 transition='in_scale', 396 ) 397 dname = self._get_replay_display_name(self._my_replay_selected) 398 bui.textwidget( 399 parent=cnt, 400 size=(0, 0), 401 h_align='center', 402 v_align='center', 403 text=bui.Lstr( 404 resource=f'{self._r}.renameReplayText', 405 subs=[('${REPLAY}', dname)], 406 ), 407 maxwidth=c_width * 0.8, 408 position=(c_width * 0.5, c_height - 60), 409 ) 410 self._my_replay_rename_text = txt = bui.textwidget( 411 parent=cnt, 412 size=(c_width * 0.8, 40), 413 h_align='left', 414 v_align='center', 415 text=dname, 416 editable=True, 417 description=bui.Lstr(resource=f'{self._r}.replayNameText'), 418 position=(c_width * 0.1, c_height - 140), 419 autoselect=True, 420 maxwidth=c_width * 0.7, 421 max_chars=200, 422 ) 423 cbtn = bui.buttonwidget( 424 parent=cnt, 425 label=bui.Lstr(resource='cancelText'), 426 on_activate_call=bui.Call( 427 lambda c: bui.containerwidget(edit=c, transition='out_scale'), 428 cnt, 429 ), 430 size=(180, 60), 431 position=(30, 30), 432 autoselect=True, 433 ) 434 okb = bui.buttonwidget( 435 parent=cnt, 436 label=bui.Lstr(resource=f'{self._r}.renameText'), 437 size=(180, 60), 438 position=(c_width - 230, 30), 439 on_activate_call=bui.Call( 440 self._rename_my_replay, self._my_replay_selected 441 ), 442 autoselect=True, 443 ) 444 bui.widget(edit=cbtn, right_widget=okb) 445 bui.widget(edit=okb, left_widget=cbtn) 446 bui.textwidget(edit=txt, on_return_press_call=okb.activate) 447 bui.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) 448 449 def _rename_my_replay(self, replay: str) -> None: 450 new_name = None 451 try: 452 if not self._my_replay_rename_text: 453 return 454 new_name_raw = cast( 455 str, bui.textwidget(query=self._my_replay_rename_text) 456 ) 457 new_name = new_name_raw + '.brp' 458 459 # Ignore attempts to change it to what it already is 460 # (or what it looks like to the user). 461 if ( 462 replay != new_name 463 and self._get_replay_display_name(replay) != new_name_raw 464 ): 465 old_name_full = (bui.get_replays_dir() + '/' + replay).encode( 466 'utf-8' 467 ) 468 new_name_full = (bui.get_replays_dir() + '/' + new_name).encode( 469 'utf-8' 470 ) 471 # False alarm; bui.textwidget can return non-None val. 472 # pylint: disable=unsupported-membership-test 473 if os.path.exists(new_name_full): 474 bui.getsound('error').play() 475 bui.screenmessage( 476 bui.Lstr( 477 resource=self._r 478 + '.replayRenameErrorAlreadyExistsText' 479 ), 480 color=(1, 0, 0), 481 ) 482 elif any(char in new_name_raw for char in ['/', '\\', ':']): 483 bui.getsound('error').play() 484 bui.screenmessage( 485 bui.Lstr( 486 resource=f'{self._r}.replayRenameErrorInvalidName' 487 ), 488 color=(1, 0, 0), 489 ) 490 else: 491 bui.increment_analytics_count('Replay rename') 492 os.rename(old_name_full, new_name_full) 493 self._refresh_my_replays() 494 bui.getsound('gunCocking').play() 495 except Exception: 496 logging.exception( 497 "Error renaming replay '%s' to '%s'.", replay, new_name 498 ) 499 bui.getsound('error').play() 500 bui.screenmessage( 501 bui.Lstr(resource=f'{self._r}.replayRenameErrorText'), 502 color=(1, 0, 0), 503 ) 504 505 bui.containerwidget( 506 edit=self._my_replays_rename_window, transition='out_scale' 507 ) 508 509 def _on_my_replay_delete_press(self) -> None: 510 from bauiv1lib import confirm 511 512 if self._my_replay_selected is None: 513 self._no_replay_selected_error() 514 return 515 confirm.ConfirmWindow( 516 bui.Lstr( 517 resource=f'{self._r}.deleteConfirmText', 518 subs=[ 519 ( 520 '${REPLAY}', 521 self._get_replay_display_name(self._my_replay_selected), 522 ) 523 ], 524 ), 525 bui.Call(self._delete_replay, self._my_replay_selected), 526 450, 527 150, 528 ) 529 530 def _get_replay_display_name(self, replay: str) -> str: 531 if replay.endswith('.brp'): 532 replay = replay[:-4] 533 if replay == '__lastReplay': 534 return bui.Lstr(resource='replayNameDefaultText').evaluate() 535 return replay 536 537 def _delete_replay(self, replay: str) -> None: 538 try: 539 bui.increment_analytics_count('Replay delete') 540 os.remove((bui.get_replays_dir() + '/' + replay).encode('utf-8')) 541 self._refresh_my_replays() 542 bui.getsound('shieldDown').play() 543 if replay == self._my_replay_selected: 544 self._my_replay_selected = None 545 except Exception: 546 logging.exception("Error deleting replay '%s'.", replay) 547 bui.getsound('error').play() 548 bui.screenmessage( 549 bui.Lstr(resource=f'{self._r}.replayDeleteErrorText'), 550 color=(1, 0, 0), 551 ) 552 553 def _on_my_replay_select(self, replay: str) -> None: 554 self._my_replay_selected = replay 555 556 def _refresh_my_replays(self) -> None: 557 assert self._columnwidget is not None 558 for child in self._columnwidget.get_children(): 559 child.delete() 560 t_scale = 1.6 561 try: 562 names = os.listdir(bui.get_replays_dir()) 563 564 # Ignore random other files in there. 565 names = [n for n in names if n.endswith('.brp')] 566 names.sort(key=lambda x: x.lower()) 567 except Exception: 568 logging.exception('Error listing replays dir.') 569 names = [] 570 571 assert self._my_replays_scroll_width is not None 572 assert self._my_replays_watch_replay_button is not None 573 for i, name in enumerate(names): 574 txt = bui.textwidget( 575 parent=self._columnwidget, 576 size=(self._my_replays_scroll_width / t_scale, 30), 577 selectable=True, 578 color=( 579 (1.0, 1, 0.4) if name == '__lastReplay.brp' else (1, 1, 1) 580 ), 581 always_highlight=True, 582 on_select_call=bui.Call(self._on_my_replay_select, name), 583 on_activate_call=self._my_replays_watch_replay_button.activate, 584 text=self._get_replay_display_name(name), 585 h_align='left', 586 v_align='center', 587 corner_scale=t_scale, 588 maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93, 589 ) 590 if i == 0: 591 bui.widget( 592 edit=txt, 593 up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button, 594 ) 595 self._my_replay_selected = name 596 597 def _save_state(self) -> None: 598 try: 599 sel = self._root_widget.get_selected_child() 600 selected_tab_ids = [ 601 tab_id 602 for tab_id, tab in self._tab_row.tabs.items() 603 if sel == tab.button 604 ] 605 if sel == self._back_button: 606 sel_name = 'Back' 607 elif selected_tab_ids: 608 assert len(selected_tab_ids) == 1 609 sel_name = f'Tab:{selected_tab_ids[0].value}' 610 elif sel == self._tab_container: 611 sel_name = 'TabContainer' 612 else: 613 raise ValueError(f'unrecognized selection {sel}') 614 assert bui.app.classic is not None 615 bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} 616 except Exception: 617 logging.exception('Error saving state for %s.', self) 618 619 def _restore_state(self) -> None: 620 try: 621 sel: bui.Widget | None 622 assert bui.app.classic is not None 623 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 624 'sel_name' 625 ) 626 assert isinstance(sel_name, (str, type(None))) 627 try: 628 current_tab = self.TabID(bui.app.config.get('Watch Tab')) 629 except ValueError: 630 current_tab = self.TabID.MY_REPLAYS 631 self._set_tab(current_tab) 632 633 if sel_name == 'Back': 634 sel = self._back_button 635 elif sel_name == 'TabContainer': 636 sel = self._tab_container 637 elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): 638 try: 639 sel_tab_id = self.TabID(sel_name.split(':')[-1]) 640 except ValueError: 641 sel_tab_id = self.TabID.MY_REPLAYS 642 sel = self._tab_row.tabs[sel_tab_id].button 643 else: 644 if self._tab_container is not None: 645 sel = self._tab_container 646 else: 647 sel = self._tab_row.tabs[current_tab].button 648 bui.containerwidget(edit=self._root_widget, selected_child=sel) 649 except Exception: 650 logging.exception('Error restoring state for %s.', self)
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.
@override
def
on_main_window_close(self) -> None:
Called before transitioning out a main window.
A good opportunity to save window state/etc.
Inherited Members
- bauiv1._uitypes.MainWindow
- main_window_back_state
- main_window_is_top_level
- main_window_close
- main_window_has_control
- main_window_back
- main_window_replace
- 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