Source code for bauiv1lib.inbox

# Released under the MIT License. See LICENSE for details.
#
# pylint: disable=too-many-lines
"""Provides a popup window to view achievements."""

from __future__ import annotations

import weakref
from functools import partial
from dataclasses import dataclass
from typing import override, assert_never, TYPE_CHECKING

from efro.util import strict_partial, pairs_from_flat
from efro.error import CommunicationError
import bacommon.bs
import bauiv1 as bui

if TYPE_CHECKING:
    import datetime
    from typing import Callable


class _Section:
    def get_height(self) -> float:
        """Return section height."""
        raise NotImplementedError()

    def get_button_row(self) -> list[bui.Widget]:
        """Return rows of selectable controls."""
        return []

    def emit(self, subcontainer: bui.Widget, y: float) -> None:
        """Emit the section."""


class _TextSection(_Section):

    def __init__(
        self,
        *,
        sub_width: float,
        text: bui.Lstr | str,
        spacing_top: float = 0.0,
        spacing_bottom: float = 0.0,
        scale: float = 0.6,
        color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
    ) -> None:
        self.sub_width = sub_width
        self.spacing_top = spacing_top
        self.spacing_bottom = spacing_bottom
        self.color = color

        # We need to bake this down since we plug its final size into
        # our math.
        self.textbaked = text.evaluate() if isinstance(text, bui.Lstr) else text

        # Calc scale to fit width and then see what height we need at
        # that scale.
        t_width = max(
            10.0,
            bui.get_string_width(self.textbaked, suppress_warning=True) * scale,
        )
        self.text_scale = scale * min(1.0, (sub_width * 0.9) / t_width)

        self.text_height = (
            0.0
            if not self.textbaked
            else bui.get_string_height(self.textbaked, suppress_warning=True)
        ) * self.text_scale

        self.full_height = self.text_height + spacing_top + spacing_bottom

    @override
    def get_height(self) -> float:
        return self.full_height

    @override
    def emit(self, subcontainer: bui.Widget, y: float) -> None:
        bui.textwidget(
            parent=subcontainer,
            position=(
                self.sub_width * 0.5,
                y - self.spacing_top - self.text_height * 0.5,
            ),
            color=self.color,
            scale=self.text_scale,
            flatness=1.0,
            shadow=1.0,
            text=self.textbaked,
            size=(0, 0),
            h_align='center',
            v_align='center',
        )


class _ButtonSection(_Section):

    def __init__(
        self,
        *,
        sub_width: float,
        label: bui.Lstr | str,
        color: tuple[float, float, float],
        label_color: tuple[float, float, float],
        call: Callable[[_ButtonSection], None],
        spacing_top: float = 0.0,
        spacing_bottom: float = 0.0,
    ) -> None:
        self.sub_width = sub_width
        self.spacing_top = spacing_top
        self.spacing_bottom = spacing_bottom
        self.color = color
        self.label_color = label_color
        self.button: bui.Widget | None = None
        self.call = call
        self.labelfin = label
        self.button_width = 130
        self.button_height = 30
        self.full_height = self.button_height + spacing_top + spacing_bottom

    @override
    def get_height(self) -> float:
        return self.full_height

    @staticmethod
    def weak_call(section: weakref.ref[_ButtonSection]) -> None:
        """Call button section call if section still exists."""
        section_strong = section()
        if section_strong is None:
            return

        section_strong.call(section_strong)

    @override
    def emit(self, subcontainer: bui.Widget, y: float) -> None:
        self.button = bui.buttonwidget(
            parent=subcontainer,
            position=(
                self.sub_width * 0.5 - self.button_width * 0.5,
                y - self.spacing_top - self.button_height,
            ),
            autoselect=True,
            label=self.labelfin,
            textcolor=self.label_color,
            text_scale=0.55,
            size=(self.button_width, self.button_height),
            color=self.color,
            on_activate_call=strict_partial(self.weak_call, weakref.ref(self)),
        )
        bui.widget(edit=self.button, depth_range=(0.1, 1.0))

    @override
    def get_button_row(self) -> list[bui.Widget]:
        """Return rows of selectable controls."""
        assert self.button is not None
        return [self.button]


class _DisplayItemsSection(_Section):

    def __init__(
        self,
        *,
        sub_width: float,
        items: list[bacommon.bs.DisplayItemWrapper],
        width: float = 100.0,
        spacing_top: float = 0.0,
        spacing_bottom: float = 0.0,
    ) -> None:
        self.display_item_width = width

        # FIXME - ask for this somewhere in case it changes.
        self.display_item_height = self.display_item_width * 0.666
        self.items = items
        self.sub_width = sub_width
        self.spacing_top = spacing_top
        self.spacing_bottom = spacing_bottom
        self.full_height = (
            self.display_item_height + spacing_top + spacing_bottom
        )

    @override
    def get_height(self) -> float:
        return self.full_height

    @override
    def emit(self, subcontainer: bui.Widget, y: float) -> None:
        # pylint: disable=cyclic-import
        from baclassic import show_display_item

        xspacing = 1.1 * self.display_item_width
        total_width = (
            0 if not self.items else ((len(self.items) - 1) * xspacing)
        )
        x = -0.5 * total_width
        for item in self.items:
            show_display_item(
                item,
                subcontainer,
                pos=(
                    self.sub_width * 0.5 + x,
                    y - self.spacing_top - self.display_item_height * 0.5,
                ),
                width=self.display_item_width,
            )
            x += xspacing


class _ExpireTimeSection(_Section):

    def __init__(
        self,
        *,
        sub_width: float,
        time: datetime.datetime,
        spacing_top: float = 0.0,
        spacing_bottom: float = 0.0,
    ) -> None:
        self.time = time
        self.sub_width = sub_width
        self.spacing_top = spacing_top
        self.spacing_bottom = spacing_bottom
        self.color = (1.0, 0.0, 1.0)
        self._timer: bui.AppTimer | None = None
        self._widget: bui.Widget | None = None
        self.text_scale = 0.4
        self.text_height = 30.0 * self.text_scale
        self.full_height = self.text_height + spacing_top + spacing_bottom

    @override
    def get_height(self) -> float:
        return self.full_height

    def _update(self) -> None:
        if not self._widget:
            return

        now = bui.utc_now_cloud()

        val: bui.Lstr
        if now < self.time:
            color = (1.0, 1.0, 1.0, 0.3)
            val = bui.Lstr(
                resource='expiresInText',
                subs=[
                    (
                        '${T}',
                        bui.timestring(
                            (self.time - now).total_seconds(), centi=False
                        ),
                    ),
                ],
            )
        else:
            color = (1.0, 0.3, 0.3, 0.5)
            val = bui.Lstr(
                resource='expiredAgoText',
                subs=[
                    (
                        '${T}',
                        bui.timestring(
                            (now - self.time).total_seconds(), centi=False
                        ),
                    ),
                ],
            )
        bui.textwidget(edit=self._widget, text=val, color=color)

    @override
    def emit(self, subcontainer: bui.Widget, y: float) -> None:
        self._widget = bui.textwidget(
            parent=subcontainer,
            position=(
                self.sub_width * 0.5,
                y - self.spacing_top - self.text_height * 0.5,
            ),
            color=self.color,
            scale=self.text_scale,
            flatness=1.0,
            shadow=1.0,
            text='',
            maxwidth=self.sub_width * 0.7,
            size=(0, 0),
            h_align='center',
            v_align='center',
        )
        self._timer = bui.AppTimer(1.0, bui.WeakCall(self._update), repeat=True)
        self._update()


@dataclass
class _EntryDisplay:
    interaction_style: bacommon.bs.BasicClientUI.InteractionStyle
    button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel
    button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel
    sections: list[_Section]
    id: str
    total_height: float
    color: tuple[float, float, float]
    backing: bui.Widget | None = None
    button_positive: bui.Widget | None = None
    button_spinner_positive: bui.Widget | None = None
    button_negative: bui.Widget | None = None
    button_spinner_negative: 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._entry_displays: list[_EntryDisplay] = [] self._width = 900 if uiscale is bui.UIScale.SMALL else 500 self._height = ( 600 if uiscale is bui.UIScale.SMALL else 460 if uiscale is bui.UIScale.MEDIUM else 600 ) # Do some fancy math to fill all available screen area up to the # size of our backing container. This lets us fit to the exact # screen shape at small ui scale. screensize = bui.get_virtual_screen_size() scale = ( 1.9 if uiscale is bui.UIScale.SMALL else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 ) # Calc screen size in our local container space and clamp to a # bit smaller than our container size. target_width = min(self._width - 60, screensize[0] / scale) target_height = min(self._height - 70, screensize[1] / scale) # To get top/left coords, go to the center of our window and offset # by half the width/height of our target area. yoffs = 0.5 * self._height + 0.5 * target_height + 30.0 scroll_width = target_width scroll_height = target_height - 31 scroll_bottom = yoffs - 59 - scroll_height 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=scale, ), transition=transition, origin_widget=origin_widget, # We're affected by screen size only at small ui-scale. refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, ) 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, yoffs - 48), 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, yoffs - (45 if uiscale is bui.UIScale.SMALL else 30), ), size=(0, 0), h_align='center', v_align='center', scale=0.6 if uiscale is bui.UIScale.SMALL else 0.8, 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='', size=(0, 0), h_align='center', v_align='center', ) self._loading_spinner = bui.spinnerwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5), style='bomb', size=48, ) self._scrollwidget = bui.scrollwidget( parent=self._root_widget, size=(scroll_width, scroll_height), position=(self._width * 0.5 - scroll_width * 0.5, scroll_bottom), capture_arrows=True, simple_culling_v=200, claims_left_right=True, claims_up_down=True, # Centering messages vertically looks natural with # fullscreen backing but weird in a window. center_small_content=uiscale is bui.UIScale.SMALL, center_small_content_horizontally=True, border_opacity=0.4, ) 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.bs.InboxRequestMessage(), 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.spinnerwidget(edit=self._loading_spinner, visible=False) bui.textwidget( edit=self._infotext, color=(1, 0, 0), text=errmsg, ) def _on_entry_display_press( self, display_weak: weakref.ReferenceType[_EntryDisplay], action: bacommon.bs.ClientUIAction, ) -> None: display = display_weak() if display is None: return bui.getsound('click01').play() self._neuter_entry_display(display) # We currently only recognize basic entries and their possible # interaction types. if ( display.interaction_style is bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN ): display.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 # Ask the master-server to run our action. with plus.accounts.primary: plus.cloud.send_message_cb( bacommon.bs.ClientUIActionMessage(display.id, action), on_response=bui.WeakCall( self._on_client_ui_action_response, display_weak, action, ), ) # Tweak the UI to show that things are in motion. button = ( display.button_positive if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE else display.button_negative ) button_spinner = ( display.button_spinner_positive if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE else display.button_spinner_negative ) if button is not None: bui.buttonwidget(edit=button, label='') if button_spinner is not None: bui.spinnerwidget(edit=button_spinner, visible=True) 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._entry_displays): return self.main_window_back() def _neuter_entry_display(self, entry: _EntryDisplay) -> 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)) def _on_client_ui_action_response( self, display_weak: weakref.ReferenceType[_EntryDisplay], action: bacommon.bs.ClientUIAction, response: bacommon.bs.ClientUIActionResponse | Exception, ) -> None: # pylint: disable=too-many-branches display = display_weak() if display is None: return assert not display.processing_complete display.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 = ( display.button_positive if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE else display.button_negative ) button_spinner = ( display.button_spinner_positive if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE else display.button_spinner_negative ) # Always hide spinner at this point. if button_spinner is not None: bui.spinnerwidget(edit=button_spinner, visible=False) # 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_type is not None: # If error_type is set, error should be also. assert response.error_message is not None error_message = bui.Lstr( translate=('serverResponses', response.error_message) ) 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 # Success! assert not isinstance(response, Exception) # Run any bundled effects. assert bui.app.classic is not None bui.app.classic.run_bs_client_effects(response.effects) # 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: label = bui.Lstr(resource='doneText') bui.buttonwidget(edit=button, label=label) def _on_inbox_request_response( self, response: bacommon.bs.InboxRequestResponse | 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.bs.InboxRequestResponse) # If we got no messages, don't touch anything. This keeps # keyboard control working in the empty case. if not response.wrappers: bui.spinnerwidget(edit=self._loading_spinner, visible=False) bui.textwidget( edit=self._infotext, color=(0.4, 0.4, 0.5), text=bui.Lstr(resource='noMessagesText'), ) return bui.scrollwidget(edit=self._scrollwidget, highlight=False) bui.spinnerwidget(edit=self._loading_spinner, visible=False) bui.textwidget(edit=self._infotext, text='') uiscale = bui.app.ui_v1.uiscale margin_top = 0.0 if uiscale is bui.UIScale.SMALL else 10.0 margin_v = 0.0 if uiscale is bui.UIScale.SMALL else 5.0 # Need this to avoid the dock blocking access to buttons on our # bottom message. margin_bottom = 60.0 if uiscale is bui.UIScale.SMALL else 10.0 # Even though our window size varies with uiscale, we want # notifications to target a fixed width. sub_width = 400.0 sub_height = margin_top # Construct entries for everything we'll display. for i, wrapper in enumerate(response.wrappers): # We need to flatten text here so we can measure it. # textfin: str color: tuple[float, float, float] interaction_style: bacommon.bs.BasicClientUI.InteractionStyle button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel sections: list[_Section] = [] total_height = 80.0 # Display only entries where we recognize all style/label # values and ui component types. if ( isinstance(wrapper.ui, bacommon.bs.BasicClientUI) and not wrapper.ui.contains_unknown_elements() ): color = (0.55, 0.5, 0.7) interaction_style = wrapper.ui.interaction_style button_label_positive = wrapper.ui.button_label_positive button_label_negative = wrapper.ui.button_label_negative idcls = bacommon.bs.BasicClientUIComponentTypeID for component in wrapper.ui.components: ctypeid = component.get_type_id() section: _Section if ctypeid is idcls.TEXT: assert isinstance( component, bacommon.bs.BasicClientUIComponentText ) section = _TextSection( sub_width=sub_width, text=bui.Lstr( translate=('serverResponses', component.text), subs=pairs_from_flat(component.subs), ), color=component.color, scale=component.scale, spacing_top=component.spacing_top, spacing_bottom=component.spacing_bottom, ) total_height += section.get_height() sections.append(section) elif ctypeid is idcls.LINK: assert isinstance( component, bacommon.bs.BasicClientUIComponentLink ) def _do_open_url(url: str, sec: _ButtonSection) -> None: del sec # Unused. bui.open_url(url) section = _ButtonSection( sub_width=sub_width, label=bui.Lstr( translate=('serverResponses', component.label), subs=pairs_from_flat(component.subs), ), color=color, call=partial(_do_open_url, component.url), label_color=(0.5, 0.7, 0.6), spacing_top=component.spacing_top, spacing_bottom=component.spacing_bottom, ) total_height += section.get_height() sections.append(section) elif ctypeid is idcls.DISPLAY_ITEMS: assert isinstance( component, bacommon.bs.BasicClientUIDisplayItems, ) section = _DisplayItemsSection( sub_width=sub_width, items=component.items, width=component.width, spacing_top=component.spacing_top, spacing_bottom=component.spacing_bottom, ) total_height += section.get_height() sections.append(section) elif ctypeid is idcls.BS_CLASSIC_TOURNEY_RESULT: from bascenev1 import get_trophy_string assert isinstance( component, bacommon.bs.BasicClientUIBsClassicTourneyResult, ) campaignname, levelname = component.game.split(':') assert bui.app.classic is not None campaign = bui.app.classic.getcampaign(campaignname) tourney_name = bui.Lstr( value='${A} ${B}', subs=[ ( '${A}', campaign.getlevel(levelname).displayname, ), ( '${B}', bui.Lstr( resource='playerCountAbbreviatedText', subs=[ ('${COUNT}', str(component.players)) ], ), ), ], ) if component.trophy is not None: trophy_prefix = ( get_trophy_string(component.trophy) + ' ' ) else: trophy_prefix = '' section = _TextSection( sub_width=sub_width, text=bui.Lstr( value='${P}${V}', subs=[ ('${P}', trophy_prefix), ( '${V}', bui.Lstr( translate=( 'serverResponses', 'You placed #${RANK}' ' in a tournament!', ), subs=[ ('${RANK}', str(component.rank)) ], ), ), ], ), color=(1.0, 1.0, 1.0, 1.0), scale=0.6, ) total_height += section.get_height() sections.append(section) section = _TextSection( sub_width=sub_width, text=tourney_name, spacing_top=5, color=(0.7, 0.7, 1.0, 1.0), scale=0.7, ) total_height += section.get_height() sections.append(section) def _do_tourney_scores( tournament_id: str, sec: _ButtonSection ) -> None: from bauiv1lib.tournamentscores import ( TournamentScoresWindow, ) assert sec.button is not None _ = ( TournamentScoresWindow( tournament_id=tournament_id, position=( sec.button ).get_screen_space_center(), ), ) section = _ButtonSection( sub_width=sub_width, label=bui.Lstr( resource='tournamentFinalStandingsText' ), color=color, call=partial( _do_tourney_scores, component.tournament_id ), label_color=(0.5, 0.7, 0.6), spacing_top=7.0, spacing_bottom=0.0 if component.prizes else 7.0, ) total_height += section.get_height() sections.append(section) if component.prizes: section = _TextSection( sub_width=sub_width, text=bui.Lstr(resource='yourPrizeText'), spacing_top=6, color=(1.0, 1.0, 1.0, 0.4), scale=0.35, ) total_height += section.get_height() sections.append(section) section = _DisplayItemsSection( sub_width=sub_width, items=component.prizes, width=70.0, spacing_top=0.0, spacing_bottom=0.0, ) total_height += section.get_height() sections.append(section) elif ctypeid is idcls.EXPIRE_TIME: assert isinstance( component, bacommon.bs.BasicClientUIExpireTime ) section = _ExpireTimeSection( sub_width=sub_width, time=component.time, spacing_top=component.spacing_top, spacing_bottom=component.spacing_bottom, ) total_height += section.get_height() sections.append(section) elif ctypeid is idcls.UNKNOWN: raise RuntimeError('Should not get here.') else: # Make sure we handle all types. assert_never(ctypeid) else: # Display anything with unknown components as an # 'upgrade your app to see this' message. color = (0.6, 0.6, 0.6) interaction_style = ( bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN ) button_label_positive = bacommon.bs.BasicClientUI.ButtonLabel.OK button_label_negative = ( bacommon.bs.BasicClientUI.ButtonLabel.CANCEL ) section = _TextSection( sub_width=sub_width, text=bui.Lstr( value='You must update the app to view this.' ), ) total_height += section.get_height() sections.append(section) self._entry_displays.append( _EntryDisplay( interaction_style=interaction_style, button_label_positive=button_label_positive, button_label_negative=button_label_negative, id=wrapper.id, sections=sections, total_height=total_height, color=color, ) ) sub_height += margin_v + total_height sub_height += margin_bottom 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') assert bui.app.classic is not None buttonrows: list[list[bui.Widget]] = [] y = sub_height - margin_top for i, _wrapper in enumerate(response.wrappers): entry_display = self._entry_displays[i] entry_display_weak = weakref.ref(entry_display) bwidth = 140 bheight = 40 ysection = y - 23.0 # Backing. entry_display.backing = img = bui.imagewidget( parent=subcontainer, position=( -0.022 * sub_width, y - entry_display.total_height * 1.09, ), texture=backing_tex, size=(sub_width * 1.07, entry_display.total_height * 1.15), color=entry_display.color, opacity=0.9, ) bui.widget(edit=img, depth_range=(0, 0.1)) # Section contents. for sec in entry_display.sections: sec.emit(subcontainer, ysection) # Wire up any widgets created by this section. sec_button_row = sec.get_button_row() if sec_button_row: buttonrows.append(sec_button_row) ysection -= sec.get_height() buttonrow: list[bui.Widget] = [] have_negative_button = ( entry_display.interaction_style is ( bacommon.bs.BasicClientUI ).InteractionStyle.BUTTON_POSITIVE_NEGATIVE ) bpos = ( ( (sub_width - bwidth - 25) if have_negative_button else ((sub_width - bwidth) * 0.5) ), y - entry_display.total_height + 15.0, ) entry_display.button_positive = btn = bui.buttonwidget( parent=subcontainer, position=bpos, autoselect=True, size=(bwidth, bheight), label=bui.app.classic.basic_client_ui_button_label_str( entry_display.button_label_positive ), color=entry_display.color, textcolor=(0, 1, 0), on_activate_call=bui.WeakCall( self._on_entry_display_press, entry_display_weak, bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE, ), enable_sound=False, ) bui.widget(edit=btn, depth_range=(0.1, 1.0)) buttonrow.append(btn) spinner = entry_display.button_spinner_positive = bui.spinnerwidget( parent=subcontainer, position=( bpos[0] + 0.5 * bwidth, bpos[1] + 0.5 * bheight, ), visible=False, ) bui.widget(edit=spinner, depth_range=(0.1, 1.0)) if have_negative_button: bpos = (25, y - entry_display.total_height + 15.0) entry_display.button_negative = btn2 = bui.buttonwidget( parent=subcontainer, position=bpos, autoselect=True, size=(bwidth, bheight), label=bui.app.classic.basic_client_ui_button_label_str( entry_display.button_label_negative ), color=(0.85, 0.5, 0.7), textcolor=(1, 0.4, 0.4), on_activate_call=bui.WeakCall( self._on_entry_display_press, entry_display_weak, (bacommon.bs.ClientUIAction).BUTTON_PRESS_NEGATIVE, ), enable_sound=False, ) bui.widget(edit=btn2, depth_range=(0.1, 1.0)) buttonrow.append(btn2) spinner = entry_display.button_spinner_negative = ( bui.spinnerwidget( parent=subcontainer, position=( bpos[0] + 0.5 * bwidth, bpos[1] + 0.5 * bheight, ), visible=False, ) ) bui.widget(edit=spinner, depth_range=(0.1, 1.0)) buttonrows.append(buttonrow) y -= margin_v + entry_display.total_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=below_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]
def _get_bs_classic_tourney_results_sections() -> list[_Section]: return [] # Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum