bauiv1lib.inbox
Provides a popup window to view achievements.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides a popup window to view achievements.""" 4 5from __future__ import annotations 6 7import weakref 8from dataclasses import dataclass 9from typing import override 10 11from efro.error import CommunicationError 12import bacommon.cloud 13import bauiv1 as bui 14 15# Messages with format versions higher than this will show up as 16# 'app needs to be updated to view this' 17SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION = 1 18 19 20@dataclass 21class _MessageEntry: 22 type: bacommon.cloud.BSInboxEntryType 23 id: str 24 height: float 25 text_height: float 26 scale: float 27 text: str 28 color: tuple[float, float, float] 29 backing: bui.Widget | None = None 30 button_positive: bui.Widget | None = None 31 button_negative: bui.Widget | None = None 32 message_text: bui.Widget | None = None 33 processing_complete: bool = False 34 35 36class InboxWindow(bui.MainWindow): 37 """Popup window to show account messages.""" 38 39 def __init__( 40 self, 41 transition: str | None = 'in_right', 42 origin_widget: bui.Widget | None = None, 43 ): 44 45 assert bui.app.classic is not None 46 uiscale = bui.app.ui_v1.uiscale 47 48 self._message_entries: list[_MessageEntry] = [] 49 50 self._width = 600 if uiscale is bui.UIScale.SMALL else 450 51 self._height = ( 52 375 53 if uiscale is bui.UIScale.SMALL 54 else 370 if uiscale is bui.UIScale.MEDIUM else 450 55 ) 56 yoffs = -47 if uiscale is bui.UIScale.SMALL else 0 57 58 super().__init__( 59 root_widget=bui.containerwidget( 60 size=(self._width, self._height), 61 toolbar_visibility=( 62 'menu_full' if uiscale is bui.UIScale.SMALL else 'menu_full' 63 ), 64 scale=( 65 2.3 66 if uiscale is bui.UIScale.SMALL 67 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 68 ), 69 stack_offset=( 70 (0, 0) 71 if uiscale is bui.UIScale.SMALL 72 else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) 73 ), 74 ), 75 transition=transition, 76 origin_widget=origin_widget, 77 ) 78 79 if uiscale is bui.UIScale.SMALL: 80 bui.containerwidget( 81 edit=self._root_widget, on_cancel_call=self.main_window_back 82 ) 83 self._back_button = None 84 else: 85 self._back_button = bui.buttonwidget( 86 parent=self._root_widget, 87 autoselect=True, 88 position=(50, self._height - 38 + yoffs), 89 size=(60, 60), 90 scale=0.6, 91 label=bui.charstr(bui.SpecialChar.BACK), 92 button_type='backSmall', 93 on_activate_call=self.main_window_back, 94 ) 95 bui.containerwidget( 96 edit=self._root_widget, cancel_button=self._back_button 97 ) 98 99 self._title_text = bui.textwidget( 100 parent=self._root_widget, 101 position=( 102 self._width * 0.5, 103 self._height 104 - (27 if uiscale is bui.UIScale.SMALL else 20) 105 + yoffs, 106 ), 107 size=(0, 0), 108 h_align='center', 109 v_align='center', 110 scale=0.6, 111 text=bui.Lstr(resource='inboxText'), 112 maxwidth=200, 113 color=bui.app.ui_v1.title_color, 114 ) 115 116 # Shows 'loading', 'no messages', etc. 117 self._infotext = bui.textwidget( 118 parent=self._root_widget, 119 position=(self._width * 0.5, self._height * 0.5), 120 maxwidth=self._width * 0.7, 121 scale=0.5, 122 flatness=1.0, 123 color=(0.4, 0.4, 0.5), 124 shadow=0.0, 125 text=bui.Lstr(resource='loadingText'), 126 size=(0, 0), 127 h_align='center', 128 v_align='center', 129 ) 130 self._scrollwidget = bui.scrollwidget( 131 parent=self._root_widget, 132 size=( 133 self._width - 60, 134 self._height - (170 if uiscale is bui.UIScale.SMALL else 70), 135 ), 136 position=( 137 30, 138 (133 if uiscale is bui.UIScale.SMALL else 30) + yoffs, 139 ), 140 capture_arrows=True, 141 simple_culling_v=200, 142 claims_left_right=True, 143 claims_up_down=True, 144 ) 145 bui.widget(edit=self._scrollwidget, autoselect=True) 146 if uiscale is bui.UIScale.SMALL: 147 bui.widget( 148 edit=self._scrollwidget, 149 left_widget=bui.get_special_widget('back_button'), 150 ) 151 152 bui.containerwidget( 153 edit=self._root_widget, 154 cancel_button=self._back_button, 155 single_depth=True, 156 ) 157 158 # Kick off request. 159 plus = bui.app.plus 160 if plus is None or plus.accounts.primary is None: 161 self._error(bui.Lstr(resource='notSignedInText')) 162 return 163 164 with plus.accounts.primary: 165 plus.cloud.send_message_cb( 166 bacommon.cloud.BSInboxRequestMessage(), 167 on_response=bui.WeakCall(self._on_inbox_request_response), 168 ) 169 170 @override 171 def get_main_window_state(self) -> bui.MainWindowState: 172 # Support recreating our window for back/refresh purposes. 173 cls = type(self) 174 return bui.BasicMainWindowState( 175 create_call=lambda transition, origin_widget: cls( 176 transition=transition, origin_widget=origin_widget 177 ) 178 ) 179 180 def _error(self, errmsg: bui.Lstr | str) -> None: 181 """Put ourself in a permanent error state.""" 182 bui.textwidget( 183 edit=self._infotext, 184 color=(1, 0, 0), 185 text=errmsg, 186 ) 187 188 def _on_message_entry_press( 189 self, 190 entry_weak: weakref.ReferenceType[_MessageEntry], 191 process_type: bacommon.cloud.BSInboxEntryProcessType, 192 ) -> None: 193 entry = entry_weak() 194 if entry is None: 195 return 196 197 self._neuter_message_entry(entry) 198 199 # We don't do anything for invalid messages. 200 if entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN: 201 entry.processing_complete = True 202 self._close_soon_if_all_processed() 203 return 204 205 # Error if we're somehow signed out now. 206 plus = bui.app.plus 207 if plus is None or plus.accounts.primary is None: 208 bui.screenmessage( 209 bui.Lstr(resource='notSignedInText'), color=(1, 0, 0) 210 ) 211 bui.getsound('error').play() 212 return 213 214 # Message the master-server to process the entry. 215 with plus.accounts.primary: 216 plus.cloud.send_message_cb( 217 bacommon.cloud.BSInboxEntryProcessMessage( 218 entry.id, process_type 219 ), 220 on_response=bui.WeakCall( 221 self._on_inbox_entry_process_response, 222 entry_weak, 223 process_type, 224 ), 225 ) 226 227 # Tweak the button to show this is in progress. 228 button = ( 229 entry.button_positive 230 if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE 231 else entry.button_negative 232 ) 233 if button is not None: 234 bui.buttonwidget(edit=button, label='...') 235 236 def _close_soon_if_all_processed(self) -> None: 237 bui.apptimer(0.25, bui.WeakCall(self._close_if_all_processed)) 238 239 def _close_if_all_processed(self) -> None: 240 if not all(m.processing_complete for m in self._message_entries): 241 return 242 243 self.main_window_back() 244 245 def _neuter_message_entry(self, entry: _MessageEntry) -> None: 246 errsound = bui.getsound('error') 247 if entry.button_positive is not None: 248 bui.buttonwidget( 249 edit=entry.button_positive, 250 color=(0.5, 0.5, 0.5), 251 textcolor=(0.4, 0.4, 0.4), 252 on_activate_call=errsound.play, 253 ) 254 if entry.button_negative is not None: 255 bui.buttonwidget( 256 edit=entry.button_negative, 257 color=(0.5, 0.5, 0.5), 258 textcolor=(0.4, 0.4, 0.4), 259 on_activate_call=errsound.play, 260 ) 261 if entry.backing is not None: 262 bui.imagewidget(edit=entry.backing, color=(0.4, 0.4, 0.4)) 263 if entry.message_text is not None: 264 bui.textwidget(edit=entry.message_text, color=(0.5, 0.5, 0.5, 0.5)) 265 266 def _on_inbox_entry_process_response( 267 self, 268 entry_weak: weakref.ReferenceType[_MessageEntry], 269 process_type: bacommon.cloud.BSInboxEntryProcessType, 270 response: bacommon.cloud.BSInboxEntryProcessResponse | Exception, 271 ) -> None: 272 # pylint: disable=too-many-branches 273 entry = entry_weak() 274 if entry is None: 275 return 276 277 assert not entry.processing_complete 278 entry.processing_complete = True 279 self._close_soon_if_all_processed() 280 281 # No-op if our UI is dead or on its way out. 282 if not self._root_widget or self._root_widget.transitioning_out: 283 return 284 285 # Tweak the button to show results. 286 button = ( 287 entry.button_positive 288 if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE 289 else entry.button_negative 290 ) 291 292 # See if we should show an error message. 293 if isinstance(response, Exception): 294 if isinstance(response, CommunicationError): 295 error_message = bui.Lstr( 296 resource='internal.unavailableNoConnectionText' 297 ) 298 else: 299 error_message = bui.Lstr(resource='errorText') 300 elif response.error is not None: 301 error_message = bui.Lstr( 302 translate=('serverResponses', response.error) 303 ) 304 else: 305 error_message = None 306 307 # Show error message if so. 308 if error_message is not None: 309 bui.screenmessage(error_message, color=(1, 0, 0)) 310 bui.getsound('error').play() 311 if button is not None: 312 bui.buttonwidget( 313 edit=button, label=bui.Lstr(resource='errorText') 314 ) 315 return 316 317 # Whee; no error. Mark as done. 318 if button is not None: 319 # If we have full unicode, just show a checkmark in all cases. 320 label: str | bui.Lstr 321 if bui.supports_unicode_display(): 322 label = '✓' 323 else: 324 # For positive claim buttons, say 'success'. 325 # Otherwise default to 'done.' 326 if ( 327 entry.type 328 in { 329 bacommon.cloud.BSInboxEntryType.CLAIM, 330 bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD, 331 } 332 and process_type 333 is bacommon.cloud.BSInboxEntryProcessType.POSITIVE 334 ): 335 label = bui.Lstr(resource='successText') 336 else: 337 label = bui.Lstr(resource='doneText') 338 bui.buttonwidget(edit=button, label=label) 339 340 def _on_inbox_request_response( 341 self, response: bacommon.cloud.BSInboxRequestResponse | Exception 342 ) -> None: 343 # pylint: disable=too-many-locals 344 # pylint: disable=too-many-statements 345 # pylint: disable=too-many-branches 346 347 # No-op if our UI is dead or on its way out. 348 if not self._root_widget or self._root_widget.transitioning_out: 349 return 350 351 errmsg: str | bui.Lstr 352 if isinstance(response, Exception): 353 errmsg = bui.Lstr(resource='internal.unavailableNoConnectionText') 354 is_error = True 355 else: 356 is_error = response.error is not None 357 errmsg = ( 358 '' 359 if response.error is None 360 else bui.Lstr(translate=('serverResponses', response.error)) 361 ) 362 363 if is_error: 364 self._error(errmsg) 365 return 366 367 assert isinstance(response, bacommon.cloud.BSInboxRequestResponse) 368 369 # If we got no messages, don't touch anything. This keeps 370 # keyboard control working in the empty case. 371 if not response.entries: 372 bui.textwidget( 373 edit=self._infotext, 374 color=(0.4, 0.4, 0.5), 375 text=bui.Lstr(resource='noMessagesText'), 376 ) 377 return 378 379 bui.textwidget(edit=self._infotext, text='') 380 381 sub_width = self._width - 90 382 sub_height = 0.0 383 384 # Run the math on row heights/etc. 385 for i, entry in enumerate(response.entries): 386 # We need to flatten text here so we can measure it. 387 textfin: str 388 color: tuple[float, float, float] 389 390 # Messages with either newer formatting or unrecognized 391 # types show up as 'upgrade your app to see this'. 392 if ( 393 entry.format_version > SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION 394 or entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN 395 ): 396 textfin = bui.Lstr( 397 translate=( 398 'serverResponses', 399 'You must update the app to view this.', 400 ) 401 ).evaluate() 402 color = (0.6, 0.6, 0.6) 403 else: 404 # Translate raw response and apply any replacements. 405 textfin = bui.Lstr( 406 translate=('serverResponses', entry.message) 407 ).evaluate() 408 assert len(entry.subs) % 2 == 0 # Should always be even. 409 for j in range(0, len(entry.subs) - 1, 2): 410 textfin = textfin.replace(entry.subs[j], entry.subs[j + 1]) 411 color = (0.55, 0.5, 0.7) 412 413 # Calc scale to fit width and then see what height we need 414 # at that scale. 415 t_width = max( 416 10.0, bui.get_string_width(textfin, suppress_warning=True) 417 ) 418 scale = min(0.6, (sub_width * 0.9) / t_width) 419 t_height = ( 420 max(10.0, bui.get_string_height(textfin, suppress_warning=True)) 421 * scale 422 ) 423 entry_height = 90.0 + t_height 424 self._message_entries.append( 425 _MessageEntry( 426 type=entry.type, 427 id=entry.id, 428 height=entry_height, 429 text_height=t_height, 430 scale=scale, 431 text=textfin, 432 color=color, 433 ) 434 ) 435 sub_height += entry_height 436 437 subcontainer = bui.containerwidget( 438 id='inboxsub', 439 parent=self._scrollwidget, 440 size=(sub_width, sub_height), 441 background=False, 442 single_depth=True, 443 claims_left_right=True, 444 claims_up_down=True, 445 ) 446 447 backing_tex = bui.gettexture('buttonSquareWide') 448 449 buttonrows: list[list[bui.Widget]] = [] 450 y = sub_height 451 for i, _entry in enumerate(response.entries): 452 message_entry = self._message_entries[i] 453 message_entry_weak = weakref.ref(message_entry) 454 bwidth = 140 455 bheight = 40 456 457 # Backing. 458 message_entry.backing = img = bui.imagewidget( 459 parent=subcontainer, 460 position=(-0.022 * sub_width, y - message_entry.height * 1.09), 461 texture=backing_tex, 462 size=(sub_width * 1.07, message_entry.height * 1.15), 463 color=message_entry.color, 464 opacity=0.9, 465 ) 466 bui.widget(edit=img, depth_range=(0, 0.1)) 467 468 buttonrow: list[bui.Widget] = [] 469 have_negative_button = ( 470 message_entry.type 471 is bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD 472 ) 473 474 message_entry.button_positive = btn = bui.buttonwidget( 475 parent=subcontainer, 476 position=( 477 ( 478 (sub_width - bwidth - 25) 479 if have_negative_button 480 else ((sub_width - bwidth) * 0.5) 481 ), 482 y - message_entry.height + 15.0, 483 ), 484 size=(bwidth, bheight), 485 label=bui.Lstr( 486 resource=( 487 'claimText' 488 if message_entry.type 489 in { 490 bacommon.cloud.BSInboxEntryType.CLAIM, 491 bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD, 492 } 493 else 'okText' 494 ) 495 ), 496 color=message_entry.color, 497 textcolor=(0, 1, 0), 498 on_activate_call=bui.WeakCall( 499 self._on_message_entry_press, 500 message_entry_weak, 501 bacommon.cloud.BSInboxEntryProcessType.POSITIVE, 502 ), 503 ) 504 bui.widget(edit=btn, depth_range=(0.1, 1.0)) 505 buttonrow.append(btn) 506 507 if have_negative_button: 508 message_entry.button_negative = btn2 = bui.buttonwidget( 509 parent=subcontainer, 510 position=(25, y - message_entry.height + 15.0), 511 size=(bwidth, bheight), 512 label=bui.Lstr(resource='discardText'), 513 color=(0.85, 0.5, 0.7), 514 textcolor=(1, 0.4, 0.4), 515 on_activate_call=bui.WeakCall( 516 self._on_message_entry_press, 517 message_entry_weak, 518 bacommon.cloud.BSInboxEntryProcessType.NEGATIVE, 519 ), 520 ) 521 bui.widget(edit=btn2, depth_range=(0.1, 1.0)) 522 buttonrow.append(btn2) 523 524 buttonrows.append(buttonrow) 525 526 message_entry.message_text = bui.textwidget( 527 parent=subcontainer, 528 position=( 529 sub_width * 0.5, 530 y - message_entry.text_height * 0.5 - 23.0, 531 ), 532 scale=message_entry.scale, 533 flatness=1.0, 534 shadow=0.0, 535 text=message_entry.text, 536 size=(0, 0), 537 h_align='center', 538 v_align='center', 539 ) 540 y -= message_entry.height 541 542 uiscale = bui.app.ui_v1.uiscale 543 above_widget = ( 544 bui.get_special_widget('back_button') 545 if uiscale is bui.UIScale.SMALL 546 else self._back_button 547 ) 548 assert above_widget is not None 549 for i, buttons in enumerate(buttonrows): 550 if i < len(buttonrows) - 1: 551 below_widget = buttonrows[i + 1][0] 552 else: 553 below_widget = None 554 555 assert buttons # We should never have an empty row. 556 for j, button in enumerate(buttons): 557 bui.widget( 558 edit=button, 559 up_widget=above_widget, 560 down_widget=( 561 button if below_widget is None else below_widget 562 ), 563 right_widget=buttons[max(j - 1, 0)], 564 left_widget=buttons[min(j + 1, len(buttons) - 1)], 565 ) 566 567 above_widget = buttons[0]
SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION =
1
class
InboxWindow(bauiv1._uitypes.MainWindow):
37class InboxWindow(bui.MainWindow): 38 """Popup window to show account messages.""" 39 40 def __init__( 41 self, 42 transition: str | None = 'in_right', 43 origin_widget: bui.Widget | None = None, 44 ): 45 46 assert bui.app.classic is not None 47 uiscale = bui.app.ui_v1.uiscale 48 49 self._message_entries: list[_MessageEntry] = [] 50 51 self._width = 600 if uiscale is bui.UIScale.SMALL else 450 52 self._height = ( 53 375 54 if uiscale is bui.UIScale.SMALL 55 else 370 if uiscale is bui.UIScale.MEDIUM else 450 56 ) 57 yoffs = -47 if uiscale is bui.UIScale.SMALL else 0 58 59 super().__init__( 60 root_widget=bui.containerwidget( 61 size=(self._width, self._height), 62 toolbar_visibility=( 63 'menu_full' if uiscale is bui.UIScale.SMALL else 'menu_full' 64 ), 65 scale=( 66 2.3 67 if uiscale is bui.UIScale.SMALL 68 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 69 ), 70 stack_offset=( 71 (0, 0) 72 if uiscale is bui.UIScale.SMALL 73 else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) 74 ), 75 ), 76 transition=transition, 77 origin_widget=origin_widget, 78 ) 79 80 if uiscale is bui.UIScale.SMALL: 81 bui.containerwidget( 82 edit=self._root_widget, on_cancel_call=self.main_window_back 83 ) 84 self._back_button = None 85 else: 86 self._back_button = bui.buttonwidget( 87 parent=self._root_widget, 88 autoselect=True, 89 position=(50, self._height - 38 + yoffs), 90 size=(60, 60), 91 scale=0.6, 92 label=bui.charstr(bui.SpecialChar.BACK), 93 button_type='backSmall', 94 on_activate_call=self.main_window_back, 95 ) 96 bui.containerwidget( 97 edit=self._root_widget, cancel_button=self._back_button 98 ) 99 100 self._title_text = bui.textwidget( 101 parent=self._root_widget, 102 position=( 103 self._width * 0.5, 104 self._height 105 - (27 if uiscale is bui.UIScale.SMALL else 20) 106 + yoffs, 107 ), 108 size=(0, 0), 109 h_align='center', 110 v_align='center', 111 scale=0.6, 112 text=bui.Lstr(resource='inboxText'), 113 maxwidth=200, 114 color=bui.app.ui_v1.title_color, 115 ) 116 117 # Shows 'loading', 'no messages', etc. 118 self._infotext = bui.textwidget( 119 parent=self._root_widget, 120 position=(self._width * 0.5, self._height * 0.5), 121 maxwidth=self._width * 0.7, 122 scale=0.5, 123 flatness=1.0, 124 color=(0.4, 0.4, 0.5), 125 shadow=0.0, 126 text=bui.Lstr(resource='loadingText'), 127 size=(0, 0), 128 h_align='center', 129 v_align='center', 130 ) 131 self._scrollwidget = bui.scrollwidget( 132 parent=self._root_widget, 133 size=( 134 self._width - 60, 135 self._height - (170 if uiscale is bui.UIScale.SMALL else 70), 136 ), 137 position=( 138 30, 139 (133 if uiscale is bui.UIScale.SMALL else 30) + yoffs, 140 ), 141 capture_arrows=True, 142 simple_culling_v=200, 143 claims_left_right=True, 144 claims_up_down=True, 145 ) 146 bui.widget(edit=self._scrollwidget, autoselect=True) 147 if uiscale is bui.UIScale.SMALL: 148 bui.widget( 149 edit=self._scrollwidget, 150 left_widget=bui.get_special_widget('back_button'), 151 ) 152 153 bui.containerwidget( 154 edit=self._root_widget, 155 cancel_button=self._back_button, 156 single_depth=True, 157 ) 158 159 # Kick off request. 160 plus = bui.app.plus 161 if plus is None or plus.accounts.primary is None: 162 self._error(bui.Lstr(resource='notSignedInText')) 163 return 164 165 with plus.accounts.primary: 166 plus.cloud.send_message_cb( 167 bacommon.cloud.BSInboxRequestMessage(), 168 on_response=bui.WeakCall(self._on_inbox_request_response), 169 ) 170 171 @override 172 def get_main_window_state(self) -> bui.MainWindowState: 173 # Support recreating our window for back/refresh purposes. 174 cls = type(self) 175 return bui.BasicMainWindowState( 176 create_call=lambda transition, origin_widget: cls( 177 transition=transition, origin_widget=origin_widget 178 ) 179 ) 180 181 def _error(self, errmsg: bui.Lstr | str) -> None: 182 """Put ourself in a permanent error state.""" 183 bui.textwidget( 184 edit=self._infotext, 185 color=(1, 0, 0), 186 text=errmsg, 187 ) 188 189 def _on_message_entry_press( 190 self, 191 entry_weak: weakref.ReferenceType[_MessageEntry], 192 process_type: bacommon.cloud.BSInboxEntryProcessType, 193 ) -> None: 194 entry = entry_weak() 195 if entry is None: 196 return 197 198 self._neuter_message_entry(entry) 199 200 # We don't do anything for invalid messages. 201 if entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN: 202 entry.processing_complete = True 203 self._close_soon_if_all_processed() 204 return 205 206 # Error if we're somehow signed out now. 207 plus = bui.app.plus 208 if plus is None or plus.accounts.primary is None: 209 bui.screenmessage( 210 bui.Lstr(resource='notSignedInText'), color=(1, 0, 0) 211 ) 212 bui.getsound('error').play() 213 return 214 215 # Message the master-server to process the entry. 216 with plus.accounts.primary: 217 plus.cloud.send_message_cb( 218 bacommon.cloud.BSInboxEntryProcessMessage( 219 entry.id, process_type 220 ), 221 on_response=bui.WeakCall( 222 self._on_inbox_entry_process_response, 223 entry_weak, 224 process_type, 225 ), 226 ) 227 228 # Tweak the button to show this is in progress. 229 button = ( 230 entry.button_positive 231 if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE 232 else entry.button_negative 233 ) 234 if button is not None: 235 bui.buttonwidget(edit=button, label='...') 236 237 def _close_soon_if_all_processed(self) -> None: 238 bui.apptimer(0.25, bui.WeakCall(self._close_if_all_processed)) 239 240 def _close_if_all_processed(self) -> None: 241 if not all(m.processing_complete for m in self._message_entries): 242 return 243 244 self.main_window_back() 245 246 def _neuter_message_entry(self, entry: _MessageEntry) -> None: 247 errsound = bui.getsound('error') 248 if entry.button_positive is not None: 249 bui.buttonwidget( 250 edit=entry.button_positive, 251 color=(0.5, 0.5, 0.5), 252 textcolor=(0.4, 0.4, 0.4), 253 on_activate_call=errsound.play, 254 ) 255 if entry.button_negative is not None: 256 bui.buttonwidget( 257 edit=entry.button_negative, 258 color=(0.5, 0.5, 0.5), 259 textcolor=(0.4, 0.4, 0.4), 260 on_activate_call=errsound.play, 261 ) 262 if entry.backing is not None: 263 bui.imagewidget(edit=entry.backing, color=(0.4, 0.4, 0.4)) 264 if entry.message_text is not None: 265 bui.textwidget(edit=entry.message_text, color=(0.5, 0.5, 0.5, 0.5)) 266 267 def _on_inbox_entry_process_response( 268 self, 269 entry_weak: weakref.ReferenceType[_MessageEntry], 270 process_type: bacommon.cloud.BSInboxEntryProcessType, 271 response: bacommon.cloud.BSInboxEntryProcessResponse | Exception, 272 ) -> None: 273 # pylint: disable=too-many-branches 274 entry = entry_weak() 275 if entry is None: 276 return 277 278 assert not entry.processing_complete 279 entry.processing_complete = True 280 self._close_soon_if_all_processed() 281 282 # No-op if our UI is dead or on its way out. 283 if not self._root_widget or self._root_widget.transitioning_out: 284 return 285 286 # Tweak the button to show results. 287 button = ( 288 entry.button_positive 289 if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE 290 else entry.button_negative 291 ) 292 293 # See if we should show an error message. 294 if isinstance(response, Exception): 295 if isinstance(response, CommunicationError): 296 error_message = bui.Lstr( 297 resource='internal.unavailableNoConnectionText' 298 ) 299 else: 300 error_message = bui.Lstr(resource='errorText') 301 elif response.error is not None: 302 error_message = bui.Lstr( 303 translate=('serverResponses', response.error) 304 ) 305 else: 306 error_message = None 307 308 # Show error message if so. 309 if error_message is not None: 310 bui.screenmessage(error_message, color=(1, 0, 0)) 311 bui.getsound('error').play() 312 if button is not None: 313 bui.buttonwidget( 314 edit=button, label=bui.Lstr(resource='errorText') 315 ) 316 return 317 318 # Whee; no error. Mark as done. 319 if button is not None: 320 # If we have full unicode, just show a checkmark in all cases. 321 label: str | bui.Lstr 322 if bui.supports_unicode_display(): 323 label = '✓' 324 else: 325 # For positive claim buttons, say 'success'. 326 # Otherwise default to 'done.' 327 if ( 328 entry.type 329 in { 330 bacommon.cloud.BSInboxEntryType.CLAIM, 331 bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD, 332 } 333 and process_type 334 is bacommon.cloud.BSInboxEntryProcessType.POSITIVE 335 ): 336 label = bui.Lstr(resource='successText') 337 else: 338 label = bui.Lstr(resource='doneText') 339 bui.buttonwidget(edit=button, label=label) 340 341 def _on_inbox_request_response( 342 self, response: bacommon.cloud.BSInboxRequestResponse | Exception 343 ) -> None: 344 # pylint: disable=too-many-locals 345 # pylint: disable=too-many-statements 346 # pylint: disable=too-many-branches 347 348 # No-op if our UI is dead or on its way out. 349 if not self._root_widget or self._root_widget.transitioning_out: 350 return 351 352 errmsg: str | bui.Lstr 353 if isinstance(response, Exception): 354 errmsg = bui.Lstr(resource='internal.unavailableNoConnectionText') 355 is_error = True 356 else: 357 is_error = response.error is not None 358 errmsg = ( 359 '' 360 if response.error is None 361 else bui.Lstr(translate=('serverResponses', response.error)) 362 ) 363 364 if is_error: 365 self._error(errmsg) 366 return 367 368 assert isinstance(response, bacommon.cloud.BSInboxRequestResponse) 369 370 # If we got no messages, don't touch anything. This keeps 371 # keyboard control working in the empty case. 372 if not response.entries: 373 bui.textwidget( 374 edit=self._infotext, 375 color=(0.4, 0.4, 0.5), 376 text=bui.Lstr(resource='noMessagesText'), 377 ) 378 return 379 380 bui.textwidget(edit=self._infotext, text='') 381 382 sub_width = self._width - 90 383 sub_height = 0.0 384 385 # Run the math on row heights/etc. 386 for i, entry in enumerate(response.entries): 387 # We need to flatten text here so we can measure it. 388 textfin: str 389 color: tuple[float, float, float] 390 391 # Messages with either newer formatting or unrecognized 392 # types show up as 'upgrade your app to see this'. 393 if ( 394 entry.format_version > SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION 395 or entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN 396 ): 397 textfin = bui.Lstr( 398 translate=( 399 'serverResponses', 400 'You must update the app to view this.', 401 ) 402 ).evaluate() 403 color = (0.6, 0.6, 0.6) 404 else: 405 # Translate raw response and apply any replacements. 406 textfin = bui.Lstr( 407 translate=('serverResponses', entry.message) 408 ).evaluate() 409 assert len(entry.subs) % 2 == 0 # Should always be even. 410 for j in range(0, len(entry.subs) - 1, 2): 411 textfin = textfin.replace(entry.subs[j], entry.subs[j + 1]) 412 color = (0.55, 0.5, 0.7) 413 414 # Calc scale to fit width and then see what height we need 415 # at that scale. 416 t_width = max( 417 10.0, bui.get_string_width(textfin, suppress_warning=True) 418 ) 419 scale = min(0.6, (sub_width * 0.9) / t_width) 420 t_height = ( 421 max(10.0, bui.get_string_height(textfin, suppress_warning=True)) 422 * scale 423 ) 424 entry_height = 90.0 + t_height 425 self._message_entries.append( 426 _MessageEntry( 427 type=entry.type, 428 id=entry.id, 429 height=entry_height, 430 text_height=t_height, 431 scale=scale, 432 text=textfin, 433 color=color, 434 ) 435 ) 436 sub_height += entry_height 437 438 subcontainer = bui.containerwidget( 439 id='inboxsub', 440 parent=self._scrollwidget, 441 size=(sub_width, sub_height), 442 background=False, 443 single_depth=True, 444 claims_left_right=True, 445 claims_up_down=True, 446 ) 447 448 backing_tex = bui.gettexture('buttonSquareWide') 449 450 buttonrows: list[list[bui.Widget]] = [] 451 y = sub_height 452 for i, _entry in enumerate(response.entries): 453 message_entry = self._message_entries[i] 454 message_entry_weak = weakref.ref(message_entry) 455 bwidth = 140 456 bheight = 40 457 458 # Backing. 459 message_entry.backing = img = bui.imagewidget( 460 parent=subcontainer, 461 position=(-0.022 * sub_width, y - message_entry.height * 1.09), 462 texture=backing_tex, 463 size=(sub_width * 1.07, message_entry.height * 1.15), 464 color=message_entry.color, 465 opacity=0.9, 466 ) 467 bui.widget(edit=img, depth_range=(0, 0.1)) 468 469 buttonrow: list[bui.Widget] = [] 470 have_negative_button = ( 471 message_entry.type 472 is bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD 473 ) 474 475 message_entry.button_positive = btn = bui.buttonwidget( 476 parent=subcontainer, 477 position=( 478 ( 479 (sub_width - bwidth - 25) 480 if have_negative_button 481 else ((sub_width - bwidth) * 0.5) 482 ), 483 y - message_entry.height + 15.0, 484 ), 485 size=(bwidth, bheight), 486 label=bui.Lstr( 487 resource=( 488 'claimText' 489 if message_entry.type 490 in { 491 bacommon.cloud.BSInboxEntryType.CLAIM, 492 bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD, 493 } 494 else 'okText' 495 ) 496 ), 497 color=message_entry.color, 498 textcolor=(0, 1, 0), 499 on_activate_call=bui.WeakCall( 500 self._on_message_entry_press, 501 message_entry_weak, 502 bacommon.cloud.BSInboxEntryProcessType.POSITIVE, 503 ), 504 ) 505 bui.widget(edit=btn, depth_range=(0.1, 1.0)) 506 buttonrow.append(btn) 507 508 if have_negative_button: 509 message_entry.button_negative = btn2 = bui.buttonwidget( 510 parent=subcontainer, 511 position=(25, y - message_entry.height + 15.0), 512 size=(bwidth, bheight), 513 label=bui.Lstr(resource='discardText'), 514 color=(0.85, 0.5, 0.7), 515 textcolor=(1, 0.4, 0.4), 516 on_activate_call=bui.WeakCall( 517 self._on_message_entry_press, 518 message_entry_weak, 519 bacommon.cloud.BSInboxEntryProcessType.NEGATIVE, 520 ), 521 ) 522 bui.widget(edit=btn2, depth_range=(0.1, 1.0)) 523 buttonrow.append(btn2) 524 525 buttonrows.append(buttonrow) 526 527 message_entry.message_text = bui.textwidget( 528 parent=subcontainer, 529 position=( 530 sub_width * 0.5, 531 y - message_entry.text_height * 0.5 - 23.0, 532 ), 533 scale=message_entry.scale, 534 flatness=1.0, 535 shadow=0.0, 536 text=message_entry.text, 537 size=(0, 0), 538 h_align='center', 539 v_align='center', 540 ) 541 y -= message_entry.height 542 543 uiscale = bui.app.ui_v1.uiscale 544 above_widget = ( 545 bui.get_special_widget('back_button') 546 if uiscale is bui.UIScale.SMALL 547 else self._back_button 548 ) 549 assert above_widget is not None 550 for i, buttons in enumerate(buttonrows): 551 if i < len(buttonrows) - 1: 552 below_widget = buttonrows[i + 1][0] 553 else: 554 below_widget = None 555 556 assert buttons # We should never have an empty row. 557 for j, button in enumerate(buttons): 558 bui.widget( 559 edit=button, 560 up_widget=above_widget, 561 down_widget=( 562 button if below_widget is None else below_widget 563 ), 564 right_widget=buttons[max(j - 1, 0)], 565 left_widget=buttons[min(j + 1, len(buttons) - 1)], 566 ) 567 568 above_widget = buttons[0]
Popup window to show account messages.
InboxWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
40 def __init__( 41 self, 42 transition: str | None = 'in_right', 43 origin_widget: bui.Widget | None = None, 44 ): 45 46 assert bui.app.classic is not None 47 uiscale = bui.app.ui_v1.uiscale 48 49 self._message_entries: list[_MessageEntry] = [] 50 51 self._width = 600 if uiscale is bui.UIScale.SMALL else 450 52 self._height = ( 53 375 54 if uiscale is bui.UIScale.SMALL 55 else 370 if uiscale is bui.UIScale.MEDIUM else 450 56 ) 57 yoffs = -47 if uiscale is bui.UIScale.SMALL else 0 58 59 super().__init__( 60 root_widget=bui.containerwidget( 61 size=(self._width, self._height), 62 toolbar_visibility=( 63 'menu_full' if uiscale is bui.UIScale.SMALL else 'menu_full' 64 ), 65 scale=( 66 2.3 67 if uiscale is bui.UIScale.SMALL 68 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 69 ), 70 stack_offset=( 71 (0, 0) 72 if uiscale is bui.UIScale.SMALL 73 else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) 74 ), 75 ), 76 transition=transition, 77 origin_widget=origin_widget, 78 ) 79 80 if uiscale is bui.UIScale.SMALL: 81 bui.containerwidget( 82 edit=self._root_widget, on_cancel_call=self.main_window_back 83 ) 84 self._back_button = None 85 else: 86 self._back_button = bui.buttonwidget( 87 parent=self._root_widget, 88 autoselect=True, 89 position=(50, self._height - 38 + yoffs), 90 size=(60, 60), 91 scale=0.6, 92 label=bui.charstr(bui.SpecialChar.BACK), 93 button_type='backSmall', 94 on_activate_call=self.main_window_back, 95 ) 96 bui.containerwidget( 97 edit=self._root_widget, cancel_button=self._back_button 98 ) 99 100 self._title_text = bui.textwidget( 101 parent=self._root_widget, 102 position=( 103 self._width * 0.5, 104 self._height 105 - (27 if uiscale is bui.UIScale.SMALL else 20) 106 + yoffs, 107 ), 108 size=(0, 0), 109 h_align='center', 110 v_align='center', 111 scale=0.6, 112 text=bui.Lstr(resource='inboxText'), 113 maxwidth=200, 114 color=bui.app.ui_v1.title_color, 115 ) 116 117 # Shows 'loading', 'no messages', etc. 118 self._infotext = bui.textwidget( 119 parent=self._root_widget, 120 position=(self._width * 0.5, self._height * 0.5), 121 maxwidth=self._width * 0.7, 122 scale=0.5, 123 flatness=1.0, 124 color=(0.4, 0.4, 0.5), 125 shadow=0.0, 126 text=bui.Lstr(resource='loadingText'), 127 size=(0, 0), 128 h_align='center', 129 v_align='center', 130 ) 131 self._scrollwidget = bui.scrollwidget( 132 parent=self._root_widget, 133 size=( 134 self._width - 60, 135 self._height - (170 if uiscale is bui.UIScale.SMALL else 70), 136 ), 137 position=( 138 30, 139 (133 if uiscale is bui.UIScale.SMALL else 30) + yoffs, 140 ), 141 capture_arrows=True, 142 simple_culling_v=200, 143 claims_left_right=True, 144 claims_up_down=True, 145 ) 146 bui.widget(edit=self._scrollwidget, autoselect=True) 147 if uiscale is bui.UIScale.SMALL: 148 bui.widget( 149 edit=self._scrollwidget, 150 left_widget=bui.get_special_widget('back_button'), 151 ) 152 153 bui.containerwidget( 154 edit=self._root_widget, 155 cancel_button=self._back_button, 156 single_depth=True, 157 ) 158 159 # Kick off request. 160 plus = bui.app.plus 161 if plus is None or plus.accounts.primary is None: 162 self._error(bui.Lstr(resource='notSignedInText')) 163 return 164 165 with plus.accounts.primary: 166 plus.cloud.send_message_cb( 167 bacommon.cloud.BSInboxRequestMessage(), 168 on_response=bui.WeakCall(self._on_inbox_request_response), 169 )
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.
171 @override 172 def get_main_window_state(self) -> bui.MainWindowState: 173 # Support recreating our window for back/refresh purposes. 174 cls = type(self) 175 return bui.BasicMainWindowState( 176 create_call=lambda transition, origin_widget: cls( 177 transition=transition, origin_widget=origin_widget 178 ) 179 )
Return a WindowState to recreate this window, if supported.