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