Source code for bauiv1lib.inbox

# Released under the MIT License. See LICENSE for details.
#
"""Provides a popup window to view achievements."""

from __future__ import annotations

import weakref
from dataclasses import dataclass
from typing import override

from efro.error import CommunicationError
import bacommon.cloud
import bauiv1 as bui

# Messages with format versions higher than this will show up as
# 'app needs to be updated to view this'
SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION = 1


@dataclass
class _MessageEntry:
    type: bacommon.cloud.BSInboxEntryType
    id: str
    height: float
    text_height: float
    scale: float
    text: str
    color: tuple[float, float, float]
    backing: bui.Widget | None = None
    button_positive: bui.Widget | None = None
    button_negative: bui.Widget | None = None
    message_text: bui.Widget | None = None
    processing_complete: bool = False


[docs] class InboxWindow(bui.MainWindow): """Popup window to show account messages.""" def __init__( self, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale self._message_entries: list[_MessageEntry] = [] self._width = 600 if uiscale is bui.UIScale.SMALL else 450 self._height = ( 380 if uiscale is bui.UIScale.SMALL else 370 if uiscale is bui.UIScale.MEDIUM else 450 ) yoffs = -45 if uiscale is bui.UIScale.SMALL else 0 super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), toolbar_visibility=( 'menu_full' if uiscale is bui.UIScale.SMALL else 'menu_full' ), scale=( 2.3 if uiscale is bui.UIScale.SMALL else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 ), stack_offset=( (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) ), ), transition=transition, origin_widget=origin_widget, ) if uiscale is bui.UIScale.SMALL: bui.containerwidget( edit=self._root_widget, on_cancel_call=self.main_window_back ) self._back_button = None else: self._back_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(50, self._height - 38 + yoffs), size=(60, 60), scale=0.6, label=bui.charstr(bui.SpecialChar.BACK), button_type='backSmall', on_activate_call=self.main_window_back, ) bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) self._title_text = bui.textwidget( parent=self._root_widget, position=( self._width * 0.5, self._height - (27 if uiscale is bui.UIScale.SMALL else 20) + yoffs, ), size=(0, 0), h_align='center', v_align='center', scale=0.6, text=bui.Lstr(resource='inboxText'), maxwidth=200, color=bui.app.ui_v1.title_color, ) # Shows 'loading', 'no messages', etc. self._infotext = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5), maxwidth=self._width * 0.7, scale=0.5, flatness=1.0, color=(0.4, 0.4, 0.5), shadow=0.0, text=bui.Lstr(resource='loadingText'), size=(0, 0), h_align='center', v_align='center', ) self._scrollwidget = bui.scrollwidget( parent=self._root_widget, size=( self._width - 60, self._height - (170 if uiscale is bui.UIScale.SMALL else 70), ), position=( 30, (133 if uiscale is bui.UIScale.SMALL else 30) + yoffs, ), capture_arrows=True, simple_culling_v=200, claims_left_right=True, claims_up_down=True, ) bui.widget(edit=self._scrollwidget, autoselect=True) if uiscale is bui.UIScale.SMALL: bui.widget( edit=self._scrollwidget, left_widget=bui.get_special_widget('back_button'), ) bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button, single_depth=True, ) # Kick off request. plus = bui.app.plus if plus is None or plus.accounts.primary is None: self._error(bui.Lstr(resource='notSignedInText')) return with plus.accounts.primary: plus.cloud.send_message_cb( bacommon.cloud.BSInboxRequestMessage(), on_response=bui.WeakCall(self._on_inbox_request_response), )
[docs] @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. cls = type(self) return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget ) )
def _error(self, errmsg: bui.Lstr | str) -> None: """Put ourself in a permanent error state.""" bui.textwidget( edit=self._infotext, color=(1, 0, 0), text=errmsg, ) def _on_message_entry_press( self, entry_weak: weakref.ReferenceType[_MessageEntry], process_type: bacommon.cloud.BSInboxEntryProcessType, ) -> None: entry = entry_weak() if entry is None: return self._neuter_message_entry(entry) # We don't do anything for invalid messages. if entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN: entry.processing_complete = True self._close_soon_if_all_processed() return # Error if we're somehow signed out now. plus = bui.app.plus if plus is None or plus.accounts.primary is None: bui.screenmessage( bui.Lstr(resource='notSignedInText'), color=(1, 0, 0) ) bui.getsound('error').play() return # Message the master-server to process the entry. with plus.accounts.primary: plus.cloud.send_message_cb( bacommon.cloud.BSInboxEntryProcessMessage( entry.id, process_type ), on_response=bui.WeakCall( self._on_inbox_entry_process_response, entry_weak, process_type, ), ) # Tweak the button to show this is in progress. button = ( entry.button_positive if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE else entry.button_negative ) if button is not None: bui.buttonwidget(edit=button, label='...') def _close_soon_if_all_processed(self) -> None: bui.apptimer(0.25, bui.WeakCall(self._close_if_all_processed)) def _close_if_all_processed(self) -> None: if not all(m.processing_complete for m in self._message_entries): return self.main_window_back() def _neuter_message_entry(self, entry: _MessageEntry) -> None: errsound = bui.getsound('error') if entry.button_positive is not None: bui.buttonwidget( edit=entry.button_positive, color=(0.5, 0.5, 0.5), textcolor=(0.4, 0.4, 0.4), on_activate_call=errsound.play, ) if entry.button_negative is not None: bui.buttonwidget( edit=entry.button_negative, color=(0.5, 0.5, 0.5), textcolor=(0.4, 0.4, 0.4), on_activate_call=errsound.play, ) if entry.backing is not None: bui.imagewidget(edit=entry.backing, color=(0.4, 0.4, 0.4)) if entry.message_text is not None: bui.textwidget(edit=entry.message_text, color=(0.5, 0.5, 0.5, 0.5)) def _on_inbox_entry_process_response( self, entry_weak: weakref.ReferenceType[_MessageEntry], process_type: bacommon.cloud.BSInboxEntryProcessType, response: bacommon.cloud.BSInboxEntryProcessResponse | Exception, ) -> None: # pylint: disable=too-many-branches entry = entry_weak() if entry is None: return assert not entry.processing_complete entry.processing_complete = True self._close_soon_if_all_processed() # No-op if our UI is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return # Tweak the button to show results. button = ( entry.button_positive if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE else entry.button_negative ) # See if we should show an error message. if isinstance(response, Exception): if isinstance(response, CommunicationError): error_message = bui.Lstr( resource='internal.unavailableNoConnectionText' ) else: error_message = bui.Lstr(resource='errorText') elif response.error is not None: error_message = bui.Lstr( translate=('serverResponses', response.error) ) else: error_message = None # Show error message if so. if error_message is not None: bui.screenmessage(error_message, color=(1, 0, 0)) bui.getsound('error').play() if button is not None: bui.buttonwidget( edit=button, label=bui.Lstr(resource='errorText') ) return # Whee; no error. Mark as done. if button is not None: # If we have full unicode, just show a checkmark in all cases. label: str | bui.Lstr if bui.supports_unicode_display(): label = '✓' else: # For positive claim buttons, say 'success'. # Otherwise default to 'done.' if ( entry.type in { bacommon.cloud.BSInboxEntryType.CLAIM, bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD, } and process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE ): label = bui.Lstr(resource='successText') else: label = bui.Lstr(resource='doneText') bui.buttonwidget(edit=button, label=label) def _on_inbox_request_response( self, response: bacommon.cloud.BSInboxRequestResponse | Exception ) -> None: # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=too-many-branches # No-op if our UI is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return errmsg: str | bui.Lstr if isinstance(response, Exception): errmsg = bui.Lstr(resource='internal.unavailableNoConnectionText') is_error = True else: is_error = response.error is not None errmsg = ( '' if response.error is None else bui.Lstr(translate=('serverResponses', response.error)) ) if is_error: self._error(errmsg) return assert isinstance(response, bacommon.cloud.BSInboxRequestResponse) # If we got no messages, don't touch anything. This keeps # keyboard control working in the empty case. if not response.entries: bui.textwidget( edit=self._infotext, color=(0.4, 0.4, 0.5), text=bui.Lstr(resource='noMessagesText'), ) return bui.textwidget(edit=self._infotext, text='') sub_width = self._width - 90 sub_height = 0.0 # Run the math on row heights/etc. for i, entry in enumerate(response.entries): # We need to flatten text here so we can measure it. textfin: str color: tuple[float, float, float] # Messages with either newer formatting or unrecognized # types show up as 'upgrade your app to see this'. if ( entry.format_version > SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION or entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN ): textfin = bui.Lstr( translate=( 'serverResponses', 'You must update the app to view this.', ) ).evaluate() color = (0.6, 0.6, 0.6) else: # Translate raw response and apply any replacements. textfin = bui.Lstr( translate=('serverResponses', entry.message) ).evaluate() assert len(entry.subs) % 2 == 0 # Should always be even. for j in range(0, len(entry.subs) - 1, 2): textfin = textfin.replace(entry.subs[j], entry.subs[j + 1]) color = (0.55, 0.5, 0.7) # Calc scale to fit width and then see what height we need # at that scale. t_width = max( 10.0, bui.get_string_width(textfin, suppress_warning=True) ) scale = min(0.6, (sub_width * 0.9) / t_width) t_height = ( max(10.0, bui.get_string_height(textfin, suppress_warning=True)) * scale ) entry_height = 90.0 + t_height self._message_entries.append( _MessageEntry( type=entry.type, id=entry.id, height=entry_height, text_height=t_height, scale=scale, text=textfin, color=color, ) ) sub_height += entry_height subcontainer = bui.containerwidget( id='inboxsub', parent=self._scrollwidget, size=(sub_width, sub_height), background=False, single_depth=True, claims_left_right=True, claims_up_down=True, ) backing_tex = bui.gettexture('buttonSquareWide') buttonrows: list[list[bui.Widget]] = [] y = sub_height for i, _entry in enumerate(response.entries): message_entry = self._message_entries[i] message_entry_weak = weakref.ref(message_entry) bwidth = 140 bheight = 40 # Backing. message_entry.backing = img = bui.imagewidget( parent=subcontainer, position=(-0.022 * sub_width, y - message_entry.height * 1.09), texture=backing_tex, size=(sub_width * 1.07, message_entry.height * 1.15), color=message_entry.color, opacity=0.9, ) bui.widget(edit=img, depth_range=(0, 0.1)) buttonrow: list[bui.Widget] = [] have_negative_button = ( message_entry.type is bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD ) message_entry.button_positive = btn = bui.buttonwidget( parent=subcontainer, position=( ( (sub_width - bwidth - 25) if have_negative_button else ((sub_width - bwidth) * 0.5) ), y - message_entry.height + 15.0, ), size=(bwidth, bheight), label=bui.Lstr( resource=( 'claimText' if message_entry.type in { bacommon.cloud.BSInboxEntryType.CLAIM, bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD, } else 'okText' ) ), color=message_entry.color, textcolor=(0, 1, 0), on_activate_call=bui.WeakCall( self._on_message_entry_press, message_entry_weak, bacommon.cloud.BSInboxEntryProcessType.POSITIVE, ), ) bui.widget(edit=btn, depth_range=(0.1, 1.0)) buttonrow.append(btn) if have_negative_button: message_entry.button_negative = btn2 = bui.buttonwidget( parent=subcontainer, position=(25, y - message_entry.height + 15.0), size=(bwidth, bheight), label=bui.Lstr(resource='discardText'), color=(0.85, 0.5, 0.7), textcolor=(1, 0.4, 0.4), on_activate_call=bui.WeakCall( self._on_message_entry_press, message_entry_weak, bacommon.cloud.BSInboxEntryProcessType.NEGATIVE, ), ) bui.widget(edit=btn2, depth_range=(0.1, 1.0)) buttonrow.append(btn2) buttonrows.append(buttonrow) message_entry.message_text = bui.textwidget( parent=subcontainer, position=( sub_width * 0.5, y - message_entry.text_height * 0.5 - 23.0, ), scale=message_entry.scale, flatness=1.0, shadow=0.0, text=message_entry.text, size=(0, 0), h_align='center', v_align='center', ) y -= message_entry.height uiscale = bui.app.ui_v1.uiscale above_widget = ( bui.get_special_widget('back_button') if uiscale is bui.UIScale.SMALL else self._back_button ) assert above_widget is not None for i, buttons in enumerate(buttonrows): if i < len(buttonrows) - 1: below_widget = buttonrows[i + 1][0] else: below_widget = None assert buttons # We should never have an empty row. for j, button in enumerate(buttons): bui.widget( edit=button, up_widget=above_widget, down_widget=( button if below_widget is None else below_widget ), right_widget=buttons[max(j - 1, 0)], left_widget=buttons[min(j + 1, len(buttons) - 1)], ) above_widget = buttons[0]