bauiv1lib.inbox
Provides a popup window to view achievements.
1# Released under the MIT License. See LICENSE for details. 2# 3# pylint: disable=too-many-lines 4"""Provides a popup window to view achievements.""" 5 6from __future__ import annotations 7 8import weakref 9from functools import partial 10from dataclasses import dataclass 11from typing import override, assert_never, TYPE_CHECKING 12 13from efro.util import strict_partial, pairs_from_flat 14from efro.error import CommunicationError 15import bacommon.bs 16import bauiv1 as bui 17 18if TYPE_CHECKING: 19 from typing import Callable 20 21 22class _Section: 23 def get_height(self) -> float: 24 """Return section height.""" 25 raise NotImplementedError() 26 27 def get_button_row(self) -> list[bui.Widget]: 28 """Return rows of selectable controls.""" 29 return [] 30 31 def emit(self, subcontainer: bui.Widget, y: float) -> None: 32 """Emit the section.""" 33 34 35class _TextSection(_Section): 36 37 def __init__( 38 self, 39 sub_width: float, 40 text: bui.Lstr | str, 41 *, 42 spacing_top: float = 0.0, 43 spacing_bottom: float = 0.0, 44 scale: float = 0.6, 45 color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0), 46 ) -> None: 47 self.sub_width = sub_width 48 self.spacing_top = spacing_top 49 self.spacing_bottom = spacing_bottom 50 self.color = color 51 52 # We need to bake this down since we plug its final size into 53 # our math. 54 self.textbaked = text.evaluate() if isinstance(text, bui.Lstr) else text 55 56 # Calc scale to fit width and then see what height we need at 57 # that scale. 58 t_width = max( 59 10.0, 60 bui.get_string_width(self.textbaked, suppress_warning=True) * scale, 61 ) 62 self.text_scale = scale * min(1.0, (sub_width * 0.9) / t_width) 63 64 self.text_height = ( 65 0.0 66 if not self.textbaked 67 else bui.get_string_height(self.textbaked, suppress_warning=True) 68 ) * self.text_scale 69 70 self.full_height = self.text_height + spacing_top + spacing_bottom 71 72 @override 73 def get_height(self) -> float: 74 return self.full_height 75 76 @override 77 def emit(self, subcontainer: bui.Widget, y: float) -> None: 78 bui.textwidget( 79 parent=subcontainer, 80 position=( 81 self.sub_width * 0.5, 82 y - self.spacing_top - self.text_height * 0.5, 83 ), 84 color=self.color, 85 scale=self.text_scale, 86 flatness=1.0, 87 shadow=1.0, 88 text=self.textbaked, 89 size=(0, 0), 90 h_align='center', 91 v_align='center', 92 ) 93 94 95class _ButtonSection(_Section): 96 97 def __init__( 98 self, 99 sub_width: float, 100 label: bui.Lstr | str, 101 *, 102 color: tuple[float, float, float], 103 label_color: tuple[float, float, float], 104 call: Callable[[_ButtonSection], None], 105 spacing_top: float = 0.0, 106 spacing_bottom: float = 0.0, 107 ) -> None: 108 self.sub_width = sub_width 109 self.spacing_top = spacing_top 110 self.spacing_bottom = spacing_bottom 111 self.color = color 112 self.label_color = label_color 113 self.button: bui.Widget | None = None 114 self.call = call 115 self.labelfin = label 116 self.button_width = 130 117 self.button_height = 30 118 self.full_height = self.button_height + spacing_top + spacing_bottom 119 120 @override 121 def get_height(self) -> float: 122 return self.full_height 123 124 @staticmethod 125 def weak_call(section: weakref.ref[_ButtonSection]) -> None: 126 """Call button section call if section still exists.""" 127 section_strong = section() 128 if section_strong is None: 129 return 130 131 section_strong.call(section_strong) 132 133 @override 134 def emit(self, subcontainer: bui.Widget, y: float) -> None: 135 self.button = bui.buttonwidget( 136 parent=subcontainer, 137 position=( 138 self.sub_width * 0.5 - self.button_width * 0.5, 139 y - self.spacing_top - self.button_height, 140 ), 141 autoselect=True, 142 label=self.labelfin, 143 textcolor=self.label_color, 144 text_scale=0.55, 145 size=(self.button_width, self.button_height), 146 color=self.color, 147 on_activate_call=strict_partial(self.weak_call, weakref.ref(self)), 148 ) 149 bui.widget(edit=self.button, depth_range=(0.1, 1.0)) 150 151 @override 152 def get_button_row(self) -> list[bui.Widget]: 153 """Return rows of selectable controls.""" 154 assert self.button is not None 155 return [self.button] 156 157 158class _DisplayItemsSection(_Section): 159 160 def __init__( 161 self, 162 sub_width: float, 163 items: list[bacommon.bs.DisplayItemWrapper], 164 width: float = 100.0, 165 *, 166 spacing_top: float = 0.0, 167 spacing_bottom: float = 0.0, 168 ) -> None: 169 self.display_item_width = width 170 171 # FIXME - ask for this somewhere in case it changes. 172 self.display_item_height = self.display_item_width * 0.666 173 self.items = items 174 self.sub_width = sub_width 175 self.spacing_top = spacing_top 176 self.spacing_bottom = spacing_bottom 177 self.full_height = ( 178 self.display_item_height + spacing_top + spacing_bottom 179 ) 180 181 @override 182 def get_height(self) -> float: 183 return self.full_height 184 185 @override 186 def emit(self, subcontainer: bui.Widget, y: float) -> None: 187 # pylint: disable=cyclic-import 188 from baclassic import show_display_item 189 190 xspacing = 1.1 * self.display_item_width 191 total_width = ( 192 0 if not self.items else ((len(self.items) - 1) * xspacing) 193 ) 194 x = -0.5 * total_width 195 for item in self.items: 196 show_display_item( 197 item, 198 subcontainer, 199 pos=( 200 self.sub_width * 0.5 + x, 201 y - self.spacing_top - self.display_item_height * 0.5, 202 ), 203 width=self.display_item_width, 204 ) 205 x += xspacing 206 207 208@dataclass 209class _EntryDisplay: 210 interaction_style: bacommon.bs.BasicClientUI.InteractionStyle 211 button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel 212 button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel 213 sections: list[_Section] 214 id: str 215 total_height: float 216 color: tuple[float, float, float] 217 backing: bui.Widget | None = None 218 button_positive: bui.Widget | None = None 219 button_spinner_positive: bui.Widget | None = None 220 button_negative: bui.Widget | None = None 221 button_spinner_negative: bui.Widget | None = None 222 processing_complete: bool = False 223 224 225class InboxWindow(bui.MainWindow): 226 """Popup window to show account messages.""" 227 228 def __init__( 229 self, 230 transition: str | None = 'in_right', 231 origin_widget: bui.Widget | None = None, 232 ): 233 234 assert bui.app.classic is not None 235 uiscale = bui.app.ui_v1.uiscale 236 237 self._entry_displays: list[_EntryDisplay] = [] 238 239 self._width = 800 if uiscale is bui.UIScale.SMALL else 500 240 self._height = ( 241 485 242 if uiscale is bui.UIScale.SMALL 243 else 370 if uiscale is bui.UIScale.MEDIUM else 450 244 ) 245 yoffs = -42 if uiscale is bui.UIScale.SMALL else 0 246 247 super().__init__( 248 root_widget=bui.containerwidget( 249 size=(self._width, self._height), 250 toolbar_visibility=( 251 'menu_full' if uiscale is bui.UIScale.SMALL else 'menu_full' 252 ), 253 scale=( 254 1.74 255 if uiscale is bui.UIScale.SMALL 256 else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.15 257 ), 258 stack_offset=( 259 (0, 0) 260 if uiscale is bui.UIScale.SMALL 261 else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) 262 ), 263 ), 264 transition=transition, 265 origin_widget=origin_widget, 266 ) 267 268 if uiscale is bui.UIScale.SMALL: 269 bui.containerwidget( 270 edit=self._root_widget, on_cancel_call=self.main_window_back 271 ) 272 self._back_button = None 273 else: 274 self._back_button = bui.buttonwidget( 275 parent=self._root_widget, 276 autoselect=True, 277 position=(50, self._height - 38 + yoffs), 278 size=(60, 60), 279 scale=0.6, 280 label=bui.charstr(bui.SpecialChar.BACK), 281 button_type='backSmall', 282 on_activate_call=self.main_window_back, 283 ) 284 bui.containerwidget( 285 edit=self._root_widget, cancel_button=self._back_button 286 ) 287 288 self._title_text = bui.textwidget( 289 parent=self._root_widget, 290 position=( 291 self._width * 0.5, 292 self._height 293 - (45 if uiscale is bui.UIScale.SMALL else 20) 294 + yoffs, 295 ), 296 size=(0, 0), 297 h_align='center', 298 v_align='center', 299 scale=0.6, 300 text=bui.Lstr(resource='inboxText'), 301 maxwidth=200, 302 color=bui.app.ui_v1.title_color, 303 ) 304 305 # Shows 'loading', 'no messages', etc. 306 self._infotext = bui.textwidget( 307 parent=self._root_widget, 308 position=(self._width * 0.5, self._height * 0.5), 309 maxwidth=self._width * 0.7, 310 scale=0.5, 311 flatness=1.0, 312 color=(0.4, 0.4, 0.5), 313 shadow=0.0, 314 text='', 315 size=(0, 0), 316 h_align='center', 317 v_align='center', 318 ) 319 self._loading_spinner = bui.spinnerwidget( 320 parent=self._root_widget, 321 position=(self._width * 0.5, self._height * 0.5), 322 ) 323 self._scrollwidget = bui.scrollwidget( 324 parent=self._root_widget, 325 size=( 326 self._width - 60, 327 self._height - (170 if uiscale is bui.UIScale.SMALL else 80), 328 ), 329 position=( 330 30, 331 (110 if uiscale is bui.UIScale.SMALL else 34) + yoffs, 332 ), 333 capture_arrows=True, 334 simple_culling_v=200, 335 claims_left_right=True, 336 claims_up_down=True, 337 center_small_content_horizontally=True, 338 border_opacity=0.4, 339 ) 340 bui.widget(edit=self._scrollwidget, autoselect=True) 341 if uiscale is bui.UIScale.SMALL: 342 bui.widget( 343 edit=self._scrollwidget, 344 left_widget=bui.get_special_widget('back_button'), 345 ) 346 347 bui.containerwidget( 348 edit=self._root_widget, 349 cancel_button=self._back_button, 350 single_depth=True, 351 ) 352 353 # Kick off request. 354 plus = bui.app.plus 355 if plus is None or plus.accounts.primary is None: 356 self._error(bui.Lstr(resource='notSignedInText')) 357 return 358 359 with plus.accounts.primary: 360 plus.cloud.send_message_cb( 361 bacommon.bs.InboxRequestMessage(), 362 on_response=bui.WeakCall(self._on_inbox_request_response), 363 ) 364 365 @override 366 def get_main_window_state(self) -> bui.MainWindowState: 367 # Support recreating our window for back/refresh purposes. 368 cls = type(self) 369 return bui.BasicMainWindowState( 370 create_call=lambda transition, origin_widget: cls( 371 transition=transition, origin_widget=origin_widget 372 ) 373 ) 374 375 def _error(self, errmsg: bui.Lstr | str) -> None: 376 """Put ourself in a permanent error state.""" 377 bui.spinnerwidget(edit=self._loading_spinner, visible=False) 378 bui.textwidget( 379 edit=self._infotext, 380 color=(1, 0, 0), 381 text=errmsg, 382 ) 383 384 def _on_entry_display_press( 385 self, 386 display_weak: weakref.ReferenceType[_EntryDisplay], 387 action: bacommon.bs.ClientUIAction, 388 ) -> None: 389 display = display_weak() 390 if display is None: 391 return 392 393 bui.getsound('click01').play() 394 395 self._neuter_entry_display(display) 396 397 # We currently only recognize basic entries and their possible 398 # interaction types. 399 if ( 400 display.interaction_style 401 is bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN 402 ): 403 display.processing_complete = True 404 self._close_soon_if_all_processed() 405 return 406 407 # Error if we're somehow signed out now. 408 plus = bui.app.plus 409 if plus is None or plus.accounts.primary is None: 410 bui.screenmessage( 411 bui.Lstr(resource='notSignedInText'), color=(1, 0, 0) 412 ) 413 bui.getsound('error').play() 414 return 415 416 # Ask the master-server to run our action. 417 with plus.accounts.primary: 418 plus.cloud.send_message_cb( 419 bacommon.bs.ClientUIActionMessage(display.id, action), 420 on_response=bui.WeakCall( 421 self._on_client_ui_action_response, 422 display_weak, 423 action, 424 ), 425 ) 426 427 # Tweak the UI to show that things are in motion. 428 button = ( 429 display.button_positive 430 if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE 431 else display.button_negative 432 ) 433 button_spinner = ( 434 display.button_spinner_positive 435 if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE 436 else display.button_spinner_negative 437 ) 438 if button is not None: 439 bui.buttonwidget(edit=button, label='') 440 if button_spinner is not None: 441 bui.spinnerwidget(edit=button_spinner, visible=True) 442 443 def _close_soon_if_all_processed(self) -> None: 444 bui.apptimer(0.25, bui.WeakCall(self._close_if_all_processed)) 445 446 def _close_if_all_processed(self) -> None: 447 if not all(m.processing_complete for m in self._entry_displays): 448 return 449 450 self.main_window_back() 451 452 def _neuter_entry_display(self, entry: _EntryDisplay) -> None: 453 errsound = bui.getsound('error') 454 if entry.button_positive is not None: 455 bui.buttonwidget( 456 edit=entry.button_positive, 457 color=(0.5, 0.5, 0.5), 458 textcolor=(0.4, 0.4, 0.4), 459 on_activate_call=errsound.play, 460 ) 461 if entry.button_negative is not None: 462 bui.buttonwidget( 463 edit=entry.button_negative, 464 color=(0.5, 0.5, 0.5), 465 textcolor=(0.4, 0.4, 0.4), 466 on_activate_call=errsound.play, 467 ) 468 if entry.backing is not None: 469 bui.imagewidget(edit=entry.backing, color=(0.4, 0.4, 0.4)) 470 471 def _on_client_ui_action_response( 472 self, 473 display_weak: weakref.ReferenceType[_EntryDisplay], 474 action: bacommon.bs.ClientUIAction, 475 response: bacommon.bs.ClientUIActionResponse | Exception, 476 ) -> None: 477 # pylint: disable=too-many-branches 478 display = display_weak() 479 if display is None: 480 return 481 482 assert not display.processing_complete 483 display.processing_complete = True 484 self._close_soon_if_all_processed() 485 486 # No-op if our UI is dead or on its way out. 487 if not self._root_widget or self._root_widget.transitioning_out: 488 return 489 490 # Tweak the button to show results. 491 button = ( 492 display.button_positive 493 if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE 494 else display.button_negative 495 ) 496 button_spinner = ( 497 display.button_spinner_positive 498 if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE 499 else display.button_spinner_negative 500 ) 501 # Always hide spinner at this point. 502 if button_spinner is not None: 503 bui.spinnerwidget(edit=button_spinner, visible=False) 504 505 # See if we should show an error message. 506 if isinstance(response, Exception): 507 if isinstance(response, CommunicationError): 508 error_message = bui.Lstr( 509 resource='internal.unavailableNoConnectionText' 510 ) 511 else: 512 error_message = bui.Lstr(resource='errorText') 513 elif response.error_type is not None: 514 # If error_type is set, error should be also. 515 assert response.error_message is not None 516 error_message = bui.Lstr( 517 translate=('serverResponses', response.error_message) 518 ) 519 else: 520 error_message = None 521 522 # Show error message if so. 523 if error_message is not None: 524 bui.screenmessage(error_message, color=(1, 0, 0)) 525 bui.getsound('error').play() 526 if button is not None: 527 bui.buttonwidget( 528 edit=button, label=bui.Lstr(resource='errorText') 529 ) 530 return 531 532 # Success! 533 assert not isinstance(response, Exception) 534 535 # Run any bundled effects. 536 assert bui.app.classic is not None 537 bui.app.classic.run_bs_client_effects(response.effects) 538 539 # Whee; no error. Mark as done. 540 if button is not None: 541 # If we have full unicode, just show a checkmark in all cases. 542 label: str | bui.Lstr 543 if bui.supports_unicode_display(): 544 label = '✓' 545 else: 546 label = bui.Lstr(resource='doneText') 547 bui.buttonwidget(edit=button, label=label) 548 549 def _on_inbox_request_response( 550 self, response: bacommon.bs.InboxRequestResponse | Exception 551 ) -> None: 552 # pylint: disable=too-many-locals 553 # pylint: disable=too-many-statements 554 # pylint: disable=too-many-branches 555 556 # No-op if our UI is dead or on its way out. 557 if not self._root_widget or self._root_widget.transitioning_out: 558 return 559 560 errmsg: str | bui.Lstr 561 if isinstance(response, Exception): 562 errmsg = bui.Lstr(resource='internal.unavailableNoConnectionText') 563 is_error = True 564 else: 565 is_error = response.error is not None 566 errmsg = ( 567 '' 568 if response.error is None 569 else bui.Lstr(translate=('serverResponses', response.error)) 570 ) 571 572 if is_error: 573 self._error(errmsg) 574 return 575 576 assert isinstance(response, bacommon.bs.InboxRequestResponse) 577 578 # If we got no messages, don't touch anything. This keeps 579 # keyboard control working in the empty case. 580 if not response.wrappers: 581 bui.spinnerwidget(edit=self._loading_spinner, visible=False) 582 bui.textwidget( 583 edit=self._infotext, 584 color=(0.4, 0.4, 0.5), 585 text=bui.Lstr(resource='noMessagesText'), 586 ) 587 return 588 589 bui.scrollwidget(edit=self._scrollwidget, highlight=False) 590 591 bui.spinnerwidget(edit=self._loading_spinner, visible=False) 592 bui.textwidget(edit=self._infotext, text='') 593 594 uiscale = bui.app.ui_v1.uiscale 595 596 margin_top = 0.0 if uiscale is bui.UIScale.SMALL else 10.0 597 margin_v = 0.0 if uiscale is bui.UIScale.SMALL else 5.0 598 599 # Need this to avoid the dock blocking access to buttons on our 600 # bottom message. 601 margin_bottom = 60.0 if uiscale is bui.UIScale.SMALL else 10.0 602 603 # Even though our window size varies with uiscale, we want 604 # notifications to target a fixed width. 605 sub_width = 400.0 606 sub_height = margin_top 607 608 # Construct entries for everything we'll display. 609 for i, wrapper in enumerate(response.wrappers): 610 611 # We need to flatten text here so we can measure it. 612 # textfin: str 613 color: tuple[float, float, float] 614 615 interaction_style: bacommon.bs.BasicClientUI.InteractionStyle 616 button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel 617 button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel 618 619 sections: list[_Section] = [] 620 total_height = 80.0 621 622 # Display only entries where we recognize all style/label 623 # values and ui component types. 624 if ( 625 isinstance(wrapper.ui, bacommon.bs.BasicClientUI) 626 and not wrapper.ui.contains_unknown_elements() 627 ): 628 color = (0.55, 0.5, 0.7) 629 interaction_style = wrapper.ui.interaction_style 630 button_label_positive = wrapper.ui.button_label_positive 631 button_label_negative = wrapper.ui.button_label_negative 632 633 idcls = bacommon.bs.BasicClientUIComponentTypeID 634 for component in wrapper.ui.components: 635 ctypeid = component.get_type_id() 636 section: _Section 637 638 if ctypeid is idcls.TEXT: 639 assert isinstance( 640 component, bacommon.bs.BasicClientUIComponentText 641 ) 642 section = _TextSection( 643 sub_width=sub_width, 644 text=bui.Lstr( 645 translate=('serverResponses', component.text), 646 subs=pairs_from_flat(component.subs), 647 ), 648 color=component.color, 649 scale=component.scale, 650 spacing_top=component.spacing_top, 651 spacing_bottom=component.spacing_bottom, 652 ) 653 total_height += section.get_height() 654 sections.append(section) 655 656 elif ctypeid is idcls.LINK: 657 assert isinstance( 658 component, bacommon.bs.BasicClientUIComponentLink 659 ) 660 661 def _do_open_url(url: str, sec: _ButtonSection) -> None: 662 del sec # Unused. 663 bui.open_url(url) 664 665 section = _ButtonSection( 666 sub_width=sub_width, 667 label=bui.Lstr( 668 translate=('serverResponses', component.label), 669 subs=pairs_from_flat(component.subs), 670 ), 671 color=color, 672 call=partial(_do_open_url, component.url), 673 label_color=(0.5, 0.7, 0.6), 674 spacing_top=component.spacing_top, 675 spacing_bottom=component.spacing_bottom, 676 ) 677 total_height += section.get_height() 678 sections.append(section) 679 680 elif ctypeid is idcls.DISPLAY_ITEMS: 681 assert isinstance( 682 component, 683 bacommon.bs.BasicClientUIDisplayItems, 684 ) 685 section = _DisplayItemsSection( 686 sub_width=sub_width, 687 items=component.items, 688 width=component.width, 689 spacing_top=component.spacing_top, 690 spacing_bottom=component.spacing_bottom, 691 ) 692 total_height += section.get_height() 693 sections.append(section) 694 695 elif ctypeid is idcls.BS_CLASSIC_TOURNEY_RESULT: 696 from bascenev1 import get_trophy_string 697 698 assert isinstance( 699 component, 700 bacommon.bs.BasicClientUIBsClassicTourneyResult, 701 ) 702 campaignname, levelname = component.game.split(':') 703 assert bui.app.classic is not None 704 campaign = bui.app.classic.getcampaign(campaignname) 705 706 tourney_name = bui.Lstr( 707 value='${A} ${B}', 708 subs=[ 709 ( 710 '${A}', 711 campaign.getlevel(levelname).displayname, 712 ), 713 ( 714 '${B}', 715 bui.Lstr( 716 resource='playerCountAbbreviatedText', 717 subs=[ 718 ('${COUNT}', str(component.players)) 719 ], 720 ), 721 ), 722 ], 723 ) 724 725 if component.trophy is not None: 726 trophy_prefix = ( 727 get_trophy_string(component.trophy) + ' ' 728 ) 729 else: 730 trophy_prefix = '' 731 732 section = _TextSection( 733 sub_width=sub_width, 734 # text=bui.Lstr( 735 # translate=( 736 # 'serverResponses', 737 # 'You placed #${RANK}' ' in a tournament!', 738 # # 'You placed in a tournament!', 739 # ), 740 # subs=[('${RANK}', str(component.rank))], 741 # ), 742 text=bui.Lstr( 743 value='${P}${V}', 744 subs=[ 745 ('${P}', trophy_prefix), 746 ( 747 '${V}', 748 bui.Lstr( 749 translate=( 750 'serverResponses', 751 'You placed #${RANK}' 752 ' in a tournament!', 753 # 'You placed in a tournament!', 754 ), 755 subs=[ 756 ('${RANK}', str(component.rank)) 757 ], 758 ), 759 ), 760 ], 761 ), 762 color=(1.0, 1.0, 1.0, 1.0), 763 scale=0.6, 764 ) 765 total_height += section.get_height() 766 sections.append(section) 767 768 section = _TextSection( 769 sub_width=sub_width, 770 # text=bui.Lstr( 771 # value='${P}${V}', 772 # subs=[ 773 # ('${P}', trophy_prefix), 774 # ('${V}', tourney_name), 775 # ], 776 # ), 777 text=tourney_name, 778 spacing_top=5, 779 color=(0.7, 0.7, 1.0, 1.0), 780 scale=0.7, 781 ) 782 total_height += section.get_height() 783 sections.append(section) 784 785 # rank_trophy_str = f'#{component.rank}' 786 # if component.trophy is not None: 787 # rank_trophy_str = get_trophy_string( 788 # component.trophy 789 # ) 790 # section = _TextSection( 791 # sub_width=sub_width, 792 # text=rank_trophy_str, 793 # spacing_top=10, 794 # scale=1.0, 795 # ) 796 # total_height += section.get_height() 797 # sections.append(section) 798 799 def _do_tourney_scores( 800 tournament_id: str, sec: _ButtonSection 801 ) -> None: 802 from bauiv1lib.tournamentscores import ( 803 TournamentScoresWindow, 804 ) 805 806 assert sec.button is not None 807 _ = ( 808 TournamentScoresWindow( 809 tournament_id=tournament_id, 810 position=( 811 sec.button 812 ).get_screen_space_center(), 813 ), 814 ) 815 816 section = _ButtonSection( 817 sub_width=sub_width, 818 label=bui.Lstr( 819 translate=('serverResponses', 'Final Standings') 820 ), 821 color=color, 822 call=partial( 823 _do_tourney_scores, component.tournament_id 824 ), 825 label_color=(0.5, 0.7, 0.6), 826 spacing_top=7.0, 827 spacing_bottom=0.0 if component.prizes else 7.0, 828 ) 829 total_height += section.get_height() 830 sections.append(section) 831 832 if component.prizes: 833 section = _TextSection( 834 sub_width=sub_width, 835 text=bui.Lstr( 836 translate=( 837 'serverResponses', 838 'Your prize:', 839 ) 840 ), 841 spacing_top=6, 842 color=(1.0, 1.0, 1.0, 0.4), 843 scale=0.35, 844 ) 845 total_height += section.get_height() 846 sections.append(section) 847 848 section = _DisplayItemsSection( 849 sub_width=sub_width, 850 items=component.prizes, 851 width=70.0, 852 spacing_top=0.0, 853 spacing_bottom=0.0, 854 ) 855 total_height += section.get_height() 856 sections.append(section) 857 858 elif ctypeid is idcls.UNKNOWN: 859 raise RuntimeError('Should not get here.') 860 861 else: 862 # Make sure we handle all types. 863 assert_never(ctypeid) 864 else: 865 866 # Display anything with unknown components as an 867 # 'upgrade your app to see this' message. 868 color = (0.6, 0.6, 0.6) 869 interaction_style = ( 870 bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN 871 ) 872 button_label_positive = bacommon.bs.BasicClientUI.ButtonLabel.OK 873 button_label_negative = ( 874 bacommon.bs.BasicClientUI.ButtonLabel.CANCEL 875 ) 876 877 section = _TextSection( 878 sub_width=sub_width, 879 text=bui.Lstr( 880 value='You must update the app to view this.' 881 ), 882 ) 883 total_height += section.get_height() 884 sections.append(section) 885 886 self._entry_displays.append( 887 _EntryDisplay( 888 interaction_style=interaction_style, 889 button_label_positive=button_label_positive, 890 button_label_negative=button_label_negative, 891 id=wrapper.id, 892 sections=sections, 893 total_height=total_height, 894 color=color, 895 ) 896 ) 897 sub_height += margin_v + total_height 898 899 sub_height += margin_bottom 900 901 subcontainer = bui.containerwidget( 902 id='inboxsub', 903 parent=self._scrollwidget, 904 size=(sub_width, sub_height), 905 background=False, 906 single_depth=True, 907 claims_left_right=True, 908 claims_up_down=True, 909 ) 910 911 backing_tex = bui.gettexture('buttonSquareWide') 912 913 assert bui.app.classic is not None 914 915 buttonrows: list[list[bui.Widget]] = [] 916 y = sub_height - margin_top 917 for i, _wrapper in enumerate(response.wrappers): 918 entry_display = self._entry_displays[i] 919 entry_display_weak = weakref.ref(entry_display) 920 bwidth = 140 921 bheight = 40 922 923 ysection = y - 23.0 924 925 # Backing. 926 entry_display.backing = img = bui.imagewidget( 927 parent=subcontainer, 928 position=( 929 -0.022 * sub_width, 930 y - entry_display.total_height * 1.09, 931 ), 932 texture=backing_tex, 933 size=(sub_width * 1.07, entry_display.total_height * 1.15), 934 color=entry_display.color, 935 opacity=0.9, 936 ) 937 bui.widget(edit=img, depth_range=(0, 0.1)) 938 939 # Section contents. 940 for sec in entry_display.sections: 941 sec.emit(subcontainer, ysection) 942 # Wire up any widgets created by this section. 943 sec_button_row = sec.get_button_row() 944 if sec_button_row: 945 buttonrows.append(sec_button_row) 946 ysection -= sec.get_height() 947 948 buttonrow: list[bui.Widget] = [] 949 have_negative_button = ( 950 entry_display.interaction_style 951 is ( 952 bacommon.bs.BasicClientUI 953 ).InteractionStyle.BUTTON_POSITIVE_NEGATIVE 954 ) 955 956 bpos = ( 957 ( 958 (sub_width - bwidth - 25) 959 if have_negative_button 960 else ((sub_width - bwidth) * 0.5) 961 ), 962 y - entry_display.total_height + 15.0, 963 ) 964 entry_display.button_positive = btn = bui.buttonwidget( 965 parent=subcontainer, 966 position=bpos, 967 autoselect=True, 968 size=(bwidth, bheight), 969 label=bui.app.classic.basic_client_ui_button_label_str( 970 entry_display.button_label_positive 971 ), 972 color=entry_display.color, 973 textcolor=(0, 1, 0), 974 on_activate_call=bui.WeakCall( 975 self._on_entry_display_press, 976 entry_display_weak, 977 bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE, 978 ), 979 enable_sound=False, 980 ) 981 bui.widget(edit=btn, depth_range=(0.1, 1.0)) 982 buttonrow.append(btn) 983 spinner = entry_display.button_spinner_positive = bui.spinnerwidget( 984 parent=subcontainer, 985 position=( 986 bpos[0] + 0.5 * bwidth, 987 bpos[1] + 0.5 * bheight, 988 ), 989 visible=False, 990 ) 991 bui.widget(edit=spinner, depth_range=(0.1, 1.0)) 992 993 if have_negative_button: 994 bpos = (25, y - entry_display.total_height + 15.0) 995 entry_display.button_negative = btn2 = bui.buttonwidget( 996 parent=subcontainer, 997 position=bpos, 998 autoselect=True, 999 size=(bwidth, bheight), 1000 label=bui.app.classic.basic_client_ui_button_label_str( 1001 entry_display.button_label_negative 1002 ), 1003 color=(0.85, 0.5, 0.7), 1004 textcolor=(1, 0.4, 0.4), 1005 on_activate_call=bui.WeakCall( 1006 self._on_entry_display_press, 1007 entry_display_weak, 1008 (bacommon.bs.ClientUIAction).BUTTON_PRESS_NEGATIVE, 1009 ), 1010 enable_sound=False, 1011 ) 1012 bui.widget(edit=btn2, depth_range=(0.1, 1.0)) 1013 buttonrow.append(btn2) 1014 spinner = entry_display.button_spinner_negative = ( 1015 bui.spinnerwidget( 1016 parent=subcontainer, 1017 position=( 1018 bpos[0] + 0.5 * bwidth, 1019 bpos[1] + 0.5 * bheight, 1020 ), 1021 visible=False, 1022 ) 1023 ) 1024 bui.widget(edit=spinner, depth_range=(0.1, 1.0)) 1025 1026 buttonrows.append(buttonrow) 1027 1028 y -= margin_v + entry_display.total_height 1029 1030 uiscale = bui.app.ui_v1.uiscale 1031 above_widget = ( 1032 bui.get_special_widget('back_button') 1033 if uiscale is bui.UIScale.SMALL 1034 else self._back_button 1035 ) 1036 assert above_widget is not None 1037 for i, buttons in enumerate(buttonrows): 1038 if i < len(buttonrows) - 1: 1039 below_widget = buttonrows[i + 1][0] 1040 else: 1041 below_widget = None 1042 1043 assert buttons # We should never have an empty row. 1044 for j, button in enumerate(buttons): 1045 bui.widget( 1046 edit=button, 1047 up_widget=above_widget, 1048 down_widget=below_widget, 1049 # down_widget=( 1050 # button if below_widget is None else below_widget 1051 # ), 1052 right_widget=buttons[max(j - 1, 0)], 1053 left_widget=buttons[min(j + 1, len(buttons) - 1)], 1054 ) 1055 1056 above_widget = buttons[0] 1057 1058 1059def _get_bs_classic_tourney_results_sections() -> list[_Section]: 1060 return []
class
InboxWindow(bauiv1._uitypes.MainWindow):
226class InboxWindow(bui.MainWindow): 227 """Popup window to show account messages.""" 228 229 def __init__( 230 self, 231 transition: str | None = 'in_right', 232 origin_widget: bui.Widget | None = None, 233 ): 234 235 assert bui.app.classic is not None 236 uiscale = bui.app.ui_v1.uiscale 237 238 self._entry_displays: list[_EntryDisplay] = [] 239 240 self._width = 800 if uiscale is bui.UIScale.SMALL else 500 241 self._height = ( 242 485 243 if uiscale is bui.UIScale.SMALL 244 else 370 if uiscale is bui.UIScale.MEDIUM else 450 245 ) 246 yoffs = -42 if uiscale is bui.UIScale.SMALL else 0 247 248 super().__init__( 249 root_widget=bui.containerwidget( 250 size=(self._width, self._height), 251 toolbar_visibility=( 252 'menu_full' if uiscale is bui.UIScale.SMALL else 'menu_full' 253 ), 254 scale=( 255 1.74 256 if uiscale is bui.UIScale.SMALL 257 else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.15 258 ), 259 stack_offset=( 260 (0, 0) 261 if uiscale is bui.UIScale.SMALL 262 else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) 263 ), 264 ), 265 transition=transition, 266 origin_widget=origin_widget, 267 ) 268 269 if uiscale is bui.UIScale.SMALL: 270 bui.containerwidget( 271 edit=self._root_widget, on_cancel_call=self.main_window_back 272 ) 273 self._back_button = None 274 else: 275 self._back_button = bui.buttonwidget( 276 parent=self._root_widget, 277 autoselect=True, 278 position=(50, self._height - 38 + yoffs), 279 size=(60, 60), 280 scale=0.6, 281 label=bui.charstr(bui.SpecialChar.BACK), 282 button_type='backSmall', 283 on_activate_call=self.main_window_back, 284 ) 285 bui.containerwidget( 286 edit=self._root_widget, cancel_button=self._back_button 287 ) 288 289 self._title_text = bui.textwidget( 290 parent=self._root_widget, 291 position=( 292 self._width * 0.5, 293 self._height 294 - (45 if uiscale is bui.UIScale.SMALL else 20) 295 + yoffs, 296 ), 297 size=(0, 0), 298 h_align='center', 299 v_align='center', 300 scale=0.6, 301 text=bui.Lstr(resource='inboxText'), 302 maxwidth=200, 303 color=bui.app.ui_v1.title_color, 304 ) 305 306 # Shows 'loading', 'no messages', etc. 307 self._infotext = bui.textwidget( 308 parent=self._root_widget, 309 position=(self._width * 0.5, self._height * 0.5), 310 maxwidth=self._width * 0.7, 311 scale=0.5, 312 flatness=1.0, 313 color=(0.4, 0.4, 0.5), 314 shadow=0.0, 315 text='', 316 size=(0, 0), 317 h_align='center', 318 v_align='center', 319 ) 320 self._loading_spinner = bui.spinnerwidget( 321 parent=self._root_widget, 322 position=(self._width * 0.5, self._height * 0.5), 323 ) 324 self._scrollwidget = bui.scrollwidget( 325 parent=self._root_widget, 326 size=( 327 self._width - 60, 328 self._height - (170 if uiscale is bui.UIScale.SMALL else 80), 329 ), 330 position=( 331 30, 332 (110 if uiscale is bui.UIScale.SMALL else 34) + yoffs, 333 ), 334 capture_arrows=True, 335 simple_culling_v=200, 336 claims_left_right=True, 337 claims_up_down=True, 338 center_small_content_horizontally=True, 339 border_opacity=0.4, 340 ) 341 bui.widget(edit=self._scrollwidget, autoselect=True) 342 if uiscale is bui.UIScale.SMALL: 343 bui.widget( 344 edit=self._scrollwidget, 345 left_widget=bui.get_special_widget('back_button'), 346 ) 347 348 bui.containerwidget( 349 edit=self._root_widget, 350 cancel_button=self._back_button, 351 single_depth=True, 352 ) 353 354 # Kick off request. 355 plus = bui.app.plus 356 if plus is None or plus.accounts.primary is None: 357 self._error(bui.Lstr(resource='notSignedInText')) 358 return 359 360 with plus.accounts.primary: 361 plus.cloud.send_message_cb( 362 bacommon.bs.InboxRequestMessage(), 363 on_response=bui.WeakCall(self._on_inbox_request_response), 364 ) 365 366 @override 367 def get_main_window_state(self) -> bui.MainWindowState: 368 # Support recreating our window for back/refresh purposes. 369 cls = type(self) 370 return bui.BasicMainWindowState( 371 create_call=lambda transition, origin_widget: cls( 372 transition=transition, origin_widget=origin_widget 373 ) 374 ) 375 376 def _error(self, errmsg: bui.Lstr | str) -> None: 377 """Put ourself in a permanent error state.""" 378 bui.spinnerwidget(edit=self._loading_spinner, visible=False) 379 bui.textwidget( 380 edit=self._infotext, 381 color=(1, 0, 0), 382 text=errmsg, 383 ) 384 385 def _on_entry_display_press( 386 self, 387 display_weak: weakref.ReferenceType[_EntryDisplay], 388 action: bacommon.bs.ClientUIAction, 389 ) -> None: 390 display = display_weak() 391 if display is None: 392 return 393 394 bui.getsound('click01').play() 395 396 self._neuter_entry_display(display) 397 398 # We currently only recognize basic entries and their possible 399 # interaction types. 400 if ( 401 display.interaction_style 402 is bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN 403 ): 404 display.processing_complete = True 405 self._close_soon_if_all_processed() 406 return 407 408 # Error if we're somehow signed out now. 409 plus = bui.app.plus 410 if plus is None or plus.accounts.primary is None: 411 bui.screenmessage( 412 bui.Lstr(resource='notSignedInText'), color=(1, 0, 0) 413 ) 414 bui.getsound('error').play() 415 return 416 417 # Ask the master-server to run our action. 418 with plus.accounts.primary: 419 plus.cloud.send_message_cb( 420 bacommon.bs.ClientUIActionMessage(display.id, action), 421 on_response=bui.WeakCall( 422 self._on_client_ui_action_response, 423 display_weak, 424 action, 425 ), 426 ) 427 428 # Tweak the UI to show that things are in motion. 429 button = ( 430 display.button_positive 431 if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE 432 else display.button_negative 433 ) 434 button_spinner = ( 435 display.button_spinner_positive 436 if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE 437 else display.button_spinner_negative 438 ) 439 if button is not None: 440 bui.buttonwidget(edit=button, label='') 441 if button_spinner is not None: 442 bui.spinnerwidget(edit=button_spinner, visible=True) 443 444 def _close_soon_if_all_processed(self) -> None: 445 bui.apptimer(0.25, bui.WeakCall(self._close_if_all_processed)) 446 447 def _close_if_all_processed(self) -> None: 448 if not all(m.processing_complete for m in self._entry_displays): 449 return 450 451 self.main_window_back() 452 453 def _neuter_entry_display(self, entry: _EntryDisplay) -> None: 454 errsound = bui.getsound('error') 455 if entry.button_positive is not None: 456 bui.buttonwidget( 457 edit=entry.button_positive, 458 color=(0.5, 0.5, 0.5), 459 textcolor=(0.4, 0.4, 0.4), 460 on_activate_call=errsound.play, 461 ) 462 if entry.button_negative is not None: 463 bui.buttonwidget( 464 edit=entry.button_negative, 465 color=(0.5, 0.5, 0.5), 466 textcolor=(0.4, 0.4, 0.4), 467 on_activate_call=errsound.play, 468 ) 469 if entry.backing is not None: 470 bui.imagewidget(edit=entry.backing, color=(0.4, 0.4, 0.4)) 471 472 def _on_client_ui_action_response( 473 self, 474 display_weak: weakref.ReferenceType[_EntryDisplay], 475 action: bacommon.bs.ClientUIAction, 476 response: bacommon.bs.ClientUIActionResponse | Exception, 477 ) -> None: 478 # pylint: disable=too-many-branches 479 display = display_weak() 480 if display is None: 481 return 482 483 assert not display.processing_complete 484 display.processing_complete = True 485 self._close_soon_if_all_processed() 486 487 # No-op if our UI is dead or on its way out. 488 if not self._root_widget or self._root_widget.transitioning_out: 489 return 490 491 # Tweak the button to show results. 492 button = ( 493 display.button_positive 494 if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE 495 else display.button_negative 496 ) 497 button_spinner = ( 498 display.button_spinner_positive 499 if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE 500 else display.button_spinner_negative 501 ) 502 # Always hide spinner at this point. 503 if button_spinner is not None: 504 bui.spinnerwidget(edit=button_spinner, visible=False) 505 506 # See if we should show an error message. 507 if isinstance(response, Exception): 508 if isinstance(response, CommunicationError): 509 error_message = bui.Lstr( 510 resource='internal.unavailableNoConnectionText' 511 ) 512 else: 513 error_message = bui.Lstr(resource='errorText') 514 elif response.error_type is not None: 515 # If error_type is set, error should be also. 516 assert response.error_message is not None 517 error_message = bui.Lstr( 518 translate=('serverResponses', response.error_message) 519 ) 520 else: 521 error_message = None 522 523 # Show error message if so. 524 if error_message is not None: 525 bui.screenmessage(error_message, color=(1, 0, 0)) 526 bui.getsound('error').play() 527 if button is not None: 528 bui.buttonwidget( 529 edit=button, label=bui.Lstr(resource='errorText') 530 ) 531 return 532 533 # Success! 534 assert not isinstance(response, Exception) 535 536 # Run any bundled effects. 537 assert bui.app.classic is not None 538 bui.app.classic.run_bs_client_effects(response.effects) 539 540 # Whee; no error. Mark as done. 541 if button is not None: 542 # If we have full unicode, just show a checkmark in all cases. 543 label: str | bui.Lstr 544 if bui.supports_unicode_display(): 545 label = '✓' 546 else: 547 label = bui.Lstr(resource='doneText') 548 bui.buttonwidget(edit=button, label=label) 549 550 def _on_inbox_request_response( 551 self, response: bacommon.bs.InboxRequestResponse | Exception 552 ) -> None: 553 # pylint: disable=too-many-locals 554 # pylint: disable=too-many-statements 555 # pylint: disable=too-many-branches 556 557 # No-op if our UI is dead or on its way out. 558 if not self._root_widget or self._root_widget.transitioning_out: 559 return 560 561 errmsg: str | bui.Lstr 562 if isinstance(response, Exception): 563 errmsg = bui.Lstr(resource='internal.unavailableNoConnectionText') 564 is_error = True 565 else: 566 is_error = response.error is not None 567 errmsg = ( 568 '' 569 if response.error is None 570 else bui.Lstr(translate=('serverResponses', response.error)) 571 ) 572 573 if is_error: 574 self._error(errmsg) 575 return 576 577 assert isinstance(response, bacommon.bs.InboxRequestResponse) 578 579 # If we got no messages, don't touch anything. This keeps 580 # keyboard control working in the empty case. 581 if not response.wrappers: 582 bui.spinnerwidget(edit=self._loading_spinner, visible=False) 583 bui.textwidget( 584 edit=self._infotext, 585 color=(0.4, 0.4, 0.5), 586 text=bui.Lstr(resource='noMessagesText'), 587 ) 588 return 589 590 bui.scrollwidget(edit=self._scrollwidget, highlight=False) 591 592 bui.spinnerwidget(edit=self._loading_spinner, visible=False) 593 bui.textwidget(edit=self._infotext, text='') 594 595 uiscale = bui.app.ui_v1.uiscale 596 597 margin_top = 0.0 if uiscale is bui.UIScale.SMALL else 10.0 598 margin_v = 0.0 if uiscale is bui.UIScale.SMALL else 5.0 599 600 # Need this to avoid the dock blocking access to buttons on our 601 # bottom message. 602 margin_bottom = 60.0 if uiscale is bui.UIScale.SMALL else 10.0 603 604 # Even though our window size varies with uiscale, we want 605 # notifications to target a fixed width. 606 sub_width = 400.0 607 sub_height = margin_top 608 609 # Construct entries for everything we'll display. 610 for i, wrapper in enumerate(response.wrappers): 611 612 # We need to flatten text here so we can measure it. 613 # textfin: str 614 color: tuple[float, float, float] 615 616 interaction_style: bacommon.bs.BasicClientUI.InteractionStyle 617 button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel 618 button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel 619 620 sections: list[_Section] = [] 621 total_height = 80.0 622 623 # Display only entries where we recognize all style/label 624 # values and ui component types. 625 if ( 626 isinstance(wrapper.ui, bacommon.bs.BasicClientUI) 627 and not wrapper.ui.contains_unknown_elements() 628 ): 629 color = (0.55, 0.5, 0.7) 630 interaction_style = wrapper.ui.interaction_style 631 button_label_positive = wrapper.ui.button_label_positive 632 button_label_negative = wrapper.ui.button_label_negative 633 634 idcls = bacommon.bs.BasicClientUIComponentTypeID 635 for component in wrapper.ui.components: 636 ctypeid = component.get_type_id() 637 section: _Section 638 639 if ctypeid is idcls.TEXT: 640 assert isinstance( 641 component, bacommon.bs.BasicClientUIComponentText 642 ) 643 section = _TextSection( 644 sub_width=sub_width, 645 text=bui.Lstr( 646 translate=('serverResponses', component.text), 647 subs=pairs_from_flat(component.subs), 648 ), 649 color=component.color, 650 scale=component.scale, 651 spacing_top=component.spacing_top, 652 spacing_bottom=component.spacing_bottom, 653 ) 654 total_height += section.get_height() 655 sections.append(section) 656 657 elif ctypeid is idcls.LINK: 658 assert isinstance( 659 component, bacommon.bs.BasicClientUIComponentLink 660 ) 661 662 def _do_open_url(url: str, sec: _ButtonSection) -> None: 663 del sec # Unused. 664 bui.open_url(url) 665 666 section = _ButtonSection( 667 sub_width=sub_width, 668 label=bui.Lstr( 669 translate=('serverResponses', component.label), 670 subs=pairs_from_flat(component.subs), 671 ), 672 color=color, 673 call=partial(_do_open_url, component.url), 674 label_color=(0.5, 0.7, 0.6), 675 spacing_top=component.spacing_top, 676 spacing_bottom=component.spacing_bottom, 677 ) 678 total_height += section.get_height() 679 sections.append(section) 680 681 elif ctypeid is idcls.DISPLAY_ITEMS: 682 assert isinstance( 683 component, 684 bacommon.bs.BasicClientUIDisplayItems, 685 ) 686 section = _DisplayItemsSection( 687 sub_width=sub_width, 688 items=component.items, 689 width=component.width, 690 spacing_top=component.spacing_top, 691 spacing_bottom=component.spacing_bottom, 692 ) 693 total_height += section.get_height() 694 sections.append(section) 695 696 elif ctypeid is idcls.BS_CLASSIC_TOURNEY_RESULT: 697 from bascenev1 import get_trophy_string 698 699 assert isinstance( 700 component, 701 bacommon.bs.BasicClientUIBsClassicTourneyResult, 702 ) 703 campaignname, levelname = component.game.split(':') 704 assert bui.app.classic is not None 705 campaign = bui.app.classic.getcampaign(campaignname) 706 707 tourney_name = bui.Lstr( 708 value='${A} ${B}', 709 subs=[ 710 ( 711 '${A}', 712 campaign.getlevel(levelname).displayname, 713 ), 714 ( 715 '${B}', 716 bui.Lstr( 717 resource='playerCountAbbreviatedText', 718 subs=[ 719 ('${COUNT}', str(component.players)) 720 ], 721 ), 722 ), 723 ], 724 ) 725 726 if component.trophy is not None: 727 trophy_prefix = ( 728 get_trophy_string(component.trophy) + ' ' 729 ) 730 else: 731 trophy_prefix = '' 732 733 section = _TextSection( 734 sub_width=sub_width, 735 # text=bui.Lstr( 736 # translate=( 737 # 'serverResponses', 738 # 'You placed #${RANK}' ' in a tournament!', 739 # # 'You placed in a tournament!', 740 # ), 741 # subs=[('${RANK}', str(component.rank))], 742 # ), 743 text=bui.Lstr( 744 value='${P}${V}', 745 subs=[ 746 ('${P}', trophy_prefix), 747 ( 748 '${V}', 749 bui.Lstr( 750 translate=( 751 'serverResponses', 752 'You placed #${RANK}' 753 ' in a tournament!', 754 # 'You placed in a tournament!', 755 ), 756 subs=[ 757 ('${RANK}', str(component.rank)) 758 ], 759 ), 760 ), 761 ], 762 ), 763 color=(1.0, 1.0, 1.0, 1.0), 764 scale=0.6, 765 ) 766 total_height += section.get_height() 767 sections.append(section) 768 769 section = _TextSection( 770 sub_width=sub_width, 771 # text=bui.Lstr( 772 # value='${P}${V}', 773 # subs=[ 774 # ('${P}', trophy_prefix), 775 # ('${V}', tourney_name), 776 # ], 777 # ), 778 text=tourney_name, 779 spacing_top=5, 780 color=(0.7, 0.7, 1.0, 1.0), 781 scale=0.7, 782 ) 783 total_height += section.get_height() 784 sections.append(section) 785 786 # rank_trophy_str = f'#{component.rank}' 787 # if component.trophy is not None: 788 # rank_trophy_str = get_trophy_string( 789 # component.trophy 790 # ) 791 # section = _TextSection( 792 # sub_width=sub_width, 793 # text=rank_trophy_str, 794 # spacing_top=10, 795 # scale=1.0, 796 # ) 797 # total_height += section.get_height() 798 # sections.append(section) 799 800 def _do_tourney_scores( 801 tournament_id: str, sec: _ButtonSection 802 ) -> None: 803 from bauiv1lib.tournamentscores import ( 804 TournamentScoresWindow, 805 ) 806 807 assert sec.button is not None 808 _ = ( 809 TournamentScoresWindow( 810 tournament_id=tournament_id, 811 position=( 812 sec.button 813 ).get_screen_space_center(), 814 ), 815 ) 816 817 section = _ButtonSection( 818 sub_width=sub_width, 819 label=bui.Lstr( 820 translate=('serverResponses', 'Final Standings') 821 ), 822 color=color, 823 call=partial( 824 _do_tourney_scores, component.tournament_id 825 ), 826 label_color=(0.5, 0.7, 0.6), 827 spacing_top=7.0, 828 spacing_bottom=0.0 if component.prizes else 7.0, 829 ) 830 total_height += section.get_height() 831 sections.append(section) 832 833 if component.prizes: 834 section = _TextSection( 835 sub_width=sub_width, 836 text=bui.Lstr( 837 translate=( 838 'serverResponses', 839 'Your prize:', 840 ) 841 ), 842 spacing_top=6, 843 color=(1.0, 1.0, 1.0, 0.4), 844 scale=0.35, 845 ) 846 total_height += section.get_height() 847 sections.append(section) 848 849 section = _DisplayItemsSection( 850 sub_width=sub_width, 851 items=component.prizes, 852 width=70.0, 853 spacing_top=0.0, 854 spacing_bottom=0.0, 855 ) 856 total_height += section.get_height() 857 sections.append(section) 858 859 elif ctypeid is idcls.UNKNOWN: 860 raise RuntimeError('Should not get here.') 861 862 else: 863 # Make sure we handle all types. 864 assert_never(ctypeid) 865 else: 866 867 # Display anything with unknown components as an 868 # 'upgrade your app to see this' message. 869 color = (0.6, 0.6, 0.6) 870 interaction_style = ( 871 bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN 872 ) 873 button_label_positive = bacommon.bs.BasicClientUI.ButtonLabel.OK 874 button_label_negative = ( 875 bacommon.bs.BasicClientUI.ButtonLabel.CANCEL 876 ) 877 878 section = _TextSection( 879 sub_width=sub_width, 880 text=bui.Lstr( 881 value='You must update the app to view this.' 882 ), 883 ) 884 total_height += section.get_height() 885 sections.append(section) 886 887 self._entry_displays.append( 888 _EntryDisplay( 889 interaction_style=interaction_style, 890 button_label_positive=button_label_positive, 891 button_label_negative=button_label_negative, 892 id=wrapper.id, 893 sections=sections, 894 total_height=total_height, 895 color=color, 896 ) 897 ) 898 sub_height += margin_v + total_height 899 900 sub_height += margin_bottom 901 902 subcontainer = bui.containerwidget( 903 id='inboxsub', 904 parent=self._scrollwidget, 905 size=(sub_width, sub_height), 906 background=False, 907 single_depth=True, 908 claims_left_right=True, 909 claims_up_down=True, 910 ) 911 912 backing_tex = bui.gettexture('buttonSquareWide') 913 914 assert bui.app.classic is not None 915 916 buttonrows: list[list[bui.Widget]] = [] 917 y = sub_height - margin_top 918 for i, _wrapper in enumerate(response.wrappers): 919 entry_display = self._entry_displays[i] 920 entry_display_weak = weakref.ref(entry_display) 921 bwidth = 140 922 bheight = 40 923 924 ysection = y - 23.0 925 926 # Backing. 927 entry_display.backing = img = bui.imagewidget( 928 parent=subcontainer, 929 position=( 930 -0.022 * sub_width, 931 y - entry_display.total_height * 1.09, 932 ), 933 texture=backing_tex, 934 size=(sub_width * 1.07, entry_display.total_height * 1.15), 935 color=entry_display.color, 936 opacity=0.9, 937 ) 938 bui.widget(edit=img, depth_range=(0, 0.1)) 939 940 # Section contents. 941 for sec in entry_display.sections: 942 sec.emit(subcontainer, ysection) 943 # Wire up any widgets created by this section. 944 sec_button_row = sec.get_button_row() 945 if sec_button_row: 946 buttonrows.append(sec_button_row) 947 ysection -= sec.get_height() 948 949 buttonrow: list[bui.Widget] = [] 950 have_negative_button = ( 951 entry_display.interaction_style 952 is ( 953 bacommon.bs.BasicClientUI 954 ).InteractionStyle.BUTTON_POSITIVE_NEGATIVE 955 ) 956 957 bpos = ( 958 ( 959 (sub_width - bwidth - 25) 960 if have_negative_button 961 else ((sub_width - bwidth) * 0.5) 962 ), 963 y - entry_display.total_height + 15.0, 964 ) 965 entry_display.button_positive = btn = bui.buttonwidget( 966 parent=subcontainer, 967 position=bpos, 968 autoselect=True, 969 size=(bwidth, bheight), 970 label=bui.app.classic.basic_client_ui_button_label_str( 971 entry_display.button_label_positive 972 ), 973 color=entry_display.color, 974 textcolor=(0, 1, 0), 975 on_activate_call=bui.WeakCall( 976 self._on_entry_display_press, 977 entry_display_weak, 978 bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE, 979 ), 980 enable_sound=False, 981 ) 982 bui.widget(edit=btn, depth_range=(0.1, 1.0)) 983 buttonrow.append(btn) 984 spinner = entry_display.button_spinner_positive = bui.spinnerwidget( 985 parent=subcontainer, 986 position=( 987 bpos[0] + 0.5 * bwidth, 988 bpos[1] + 0.5 * bheight, 989 ), 990 visible=False, 991 ) 992 bui.widget(edit=spinner, depth_range=(0.1, 1.0)) 993 994 if have_negative_button: 995 bpos = (25, y - entry_display.total_height + 15.0) 996 entry_display.button_negative = btn2 = bui.buttonwidget( 997 parent=subcontainer, 998 position=bpos, 999 autoselect=True, 1000 size=(bwidth, bheight), 1001 label=bui.app.classic.basic_client_ui_button_label_str( 1002 entry_display.button_label_negative 1003 ), 1004 color=(0.85, 0.5, 0.7), 1005 textcolor=(1, 0.4, 0.4), 1006 on_activate_call=bui.WeakCall( 1007 self._on_entry_display_press, 1008 entry_display_weak, 1009 (bacommon.bs.ClientUIAction).BUTTON_PRESS_NEGATIVE, 1010 ), 1011 enable_sound=False, 1012 ) 1013 bui.widget(edit=btn2, depth_range=(0.1, 1.0)) 1014 buttonrow.append(btn2) 1015 spinner = entry_display.button_spinner_negative = ( 1016 bui.spinnerwidget( 1017 parent=subcontainer, 1018 position=( 1019 bpos[0] + 0.5 * bwidth, 1020 bpos[1] + 0.5 * bheight, 1021 ), 1022 visible=False, 1023 ) 1024 ) 1025 bui.widget(edit=spinner, depth_range=(0.1, 1.0)) 1026 1027 buttonrows.append(buttonrow) 1028 1029 y -= margin_v + entry_display.total_height 1030 1031 uiscale = bui.app.ui_v1.uiscale 1032 above_widget = ( 1033 bui.get_special_widget('back_button') 1034 if uiscale is bui.UIScale.SMALL 1035 else self._back_button 1036 ) 1037 assert above_widget is not None 1038 for i, buttons in enumerate(buttonrows): 1039 if i < len(buttonrows) - 1: 1040 below_widget = buttonrows[i + 1][0] 1041 else: 1042 below_widget = None 1043 1044 assert buttons # We should never have an empty row. 1045 for j, button in enumerate(buttons): 1046 bui.widget( 1047 edit=button, 1048 up_widget=above_widget, 1049 down_widget=below_widget, 1050 # down_widget=( 1051 # button if below_widget is None else below_widget 1052 # ), 1053 right_widget=buttons[max(j - 1, 0)], 1054 left_widget=buttons[min(j + 1, len(buttons) - 1)], 1055 ) 1056 1057 above_widget = buttons[0]
Popup window to show account messages.
InboxWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
229 def __init__( 230 self, 231 transition: str | None = 'in_right', 232 origin_widget: bui.Widget | None = None, 233 ): 234 235 assert bui.app.classic is not None 236 uiscale = bui.app.ui_v1.uiscale 237 238 self._entry_displays: list[_EntryDisplay] = [] 239 240 self._width = 800 if uiscale is bui.UIScale.SMALL else 500 241 self._height = ( 242 485 243 if uiscale is bui.UIScale.SMALL 244 else 370 if uiscale is bui.UIScale.MEDIUM else 450 245 ) 246 yoffs = -42 if uiscale is bui.UIScale.SMALL else 0 247 248 super().__init__( 249 root_widget=bui.containerwidget( 250 size=(self._width, self._height), 251 toolbar_visibility=( 252 'menu_full' if uiscale is bui.UIScale.SMALL else 'menu_full' 253 ), 254 scale=( 255 1.74 256 if uiscale is bui.UIScale.SMALL 257 else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.15 258 ), 259 stack_offset=( 260 (0, 0) 261 if uiscale is bui.UIScale.SMALL 262 else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) 263 ), 264 ), 265 transition=transition, 266 origin_widget=origin_widget, 267 ) 268 269 if uiscale is bui.UIScale.SMALL: 270 bui.containerwidget( 271 edit=self._root_widget, on_cancel_call=self.main_window_back 272 ) 273 self._back_button = None 274 else: 275 self._back_button = bui.buttonwidget( 276 parent=self._root_widget, 277 autoselect=True, 278 position=(50, self._height - 38 + yoffs), 279 size=(60, 60), 280 scale=0.6, 281 label=bui.charstr(bui.SpecialChar.BACK), 282 button_type='backSmall', 283 on_activate_call=self.main_window_back, 284 ) 285 bui.containerwidget( 286 edit=self._root_widget, cancel_button=self._back_button 287 ) 288 289 self._title_text = bui.textwidget( 290 parent=self._root_widget, 291 position=( 292 self._width * 0.5, 293 self._height 294 - (45 if uiscale is bui.UIScale.SMALL else 20) 295 + yoffs, 296 ), 297 size=(0, 0), 298 h_align='center', 299 v_align='center', 300 scale=0.6, 301 text=bui.Lstr(resource='inboxText'), 302 maxwidth=200, 303 color=bui.app.ui_v1.title_color, 304 ) 305 306 # Shows 'loading', 'no messages', etc. 307 self._infotext = bui.textwidget( 308 parent=self._root_widget, 309 position=(self._width * 0.5, self._height * 0.5), 310 maxwidth=self._width * 0.7, 311 scale=0.5, 312 flatness=1.0, 313 color=(0.4, 0.4, 0.5), 314 shadow=0.0, 315 text='', 316 size=(0, 0), 317 h_align='center', 318 v_align='center', 319 ) 320 self._loading_spinner = bui.spinnerwidget( 321 parent=self._root_widget, 322 position=(self._width * 0.5, self._height * 0.5), 323 ) 324 self._scrollwidget = bui.scrollwidget( 325 parent=self._root_widget, 326 size=( 327 self._width - 60, 328 self._height - (170 if uiscale is bui.UIScale.SMALL else 80), 329 ), 330 position=( 331 30, 332 (110 if uiscale is bui.UIScale.SMALL else 34) + yoffs, 333 ), 334 capture_arrows=True, 335 simple_culling_v=200, 336 claims_left_right=True, 337 claims_up_down=True, 338 center_small_content_horizontally=True, 339 border_opacity=0.4, 340 ) 341 bui.widget(edit=self._scrollwidget, autoselect=True) 342 if uiscale is bui.UIScale.SMALL: 343 bui.widget( 344 edit=self._scrollwidget, 345 left_widget=bui.get_special_widget('back_button'), 346 ) 347 348 bui.containerwidget( 349 edit=self._root_widget, 350 cancel_button=self._back_button, 351 single_depth=True, 352 ) 353 354 # Kick off request. 355 plus = bui.app.plus 356 if plus is None or plus.accounts.primary is None: 357 self._error(bui.Lstr(resource='notSignedInText')) 358 return 359 360 with plus.accounts.primary: 361 plus.cloud.send_message_cb( 362 bacommon.bs.InboxRequestMessage(), 363 on_response=bui.WeakCall(self._on_inbox_request_response), 364 )
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.
366 @override 367 def get_main_window_state(self) -> bui.MainWindowState: 368 # Support recreating our window for back/refresh purposes. 369 cls = type(self) 370 return bui.BasicMainWindowState( 371 create_call=lambda transition, origin_widget: cls( 372 transition=transition, origin_widget=origin_widget 373 ) 374 )
Return a WindowState to recreate this window, if supported.