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.74 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_y = 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, 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_y), capture_arrows=True, simple_culling_v=200, claims_left_right=True, claims_up_down=True, 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 []