# Released under the MIT License. See LICENSE for details.
#
"""Provides help related ui."""
from __future__ import annotations
from typing import override, TYPE_CHECKING
from efro.util import asserttype
import bacommon.docui.v1 as dui1
import bauiv1 as bui
from bauiv1lib.docui import DocUIController
if TYPE_CHECKING:
from typing import Any
from bacommon.docui import DocUIRequest, DocUIResponse
from bauiv1lib.docui import DocUILocalAction, DocUIWindow
[docs]
class InventoryUIController(DocUIController):
"""DocUI setup for inventory."""
def __init__(self, player_profiles_only: bool = False) -> None:
self._next_selected_profile: str | None = None
self._player_profiles_only = player_profiles_only
[docs]
@override
def fulfill_request(self, request: DocUIRequest) -> DocUIResponse:
response: DocUIResponse
# If we only want player profiles, we can skip the whole cloud
# request bit.
if self._player_profiles_only:
response = dui1.Response(
page=dui1.Page(
title='{"r":"inventoryText"}',
title_is_lstr=True,
rows=[],
)
)
else:
# *Most* of our inventory comes from the cloud - we just supply
# profiles ourself so it works offline.
response = self.fulfill_request_cloud(request, 'classicinventory')
assert isinstance(request, dui1.Request)
assert isinstance(response, dui1.Response)
if request.path != '/':
return response
signed_in = (
bui.app.plus is not None
and bui.app.plus.accounts.primary is not None
)
# If anything went wrong, replace the error page they sent us with
# a minimal 'most stuff is only available online' page.
inv_only_signin_t = '{"r":"inventoryOnlyAvailableSignedInText"}'
inv_only_online_t = '{"r":"inventoryOnlyAvailableOnlineText"}'
if response.status is not dui1.ResponseStatus.SUCCESS:
response = dui1.Response(
page=dui1.Page(
title='{"r":"inventoryText"}',
title_is_lstr=True,
rows=[
dui1.ButtonRow(
center_content=True,
buttons=[
dui1.Button(
(
inv_only_signin_t
if not signed_in
else inv_only_online_t
),
label_is_lstr=True,
texture='white',
size=(600, 100),
color=(1, 1, 1, 0.0),
label_scale=0.7,
label_color=(1, 0.4, 0.4, 0.8),
)
],
),
],
),
)
# Now add in our profiles, which we handle locally so it is
# available offline.
response.page.rows = [
dui1.ButtonRow(
title='{"r":"playerProfilesWindow.titleText"}',
title_is_lstr=True,
subtitle='{"r":"playerProfilesWindow.explanationText"}',
subtitle_is_lstr=True,
button_spacing=15,
buttons=self._get_profile_buttons(),
),
dui1.ButtonRow(
spacing_top=-15,
spacing_bottom=15,
padding_left=13,
buttons=[
dui1.Button(
'{"r":"editProfileWindow.titleNewText"}',
dui1.Local(
default_sound=False,
immediate_local_action='new_profile',
),
label_is_lstr=True,
style=dui1.ButtonStyle.MEDIUM,
size=(200, 60),
scale=0.8,
color=(0.6, 0.5, 0.8, 1.0),
label_color=(1, 1, 1, 1),
),
],
),
] + response.page.rows
return response
[docs]
@override
def local_action(self, action: DocUILocalAction) -> None:
if action.name == 'new_profile':
self._new_profile(action)
elif action.name == 'edit_profile':
self._edit_profile(action)
else:
bui.screenmessage(
f'Invalid local-action "{action.name}".', color=(1, 0, 0)
)
bui.getsound('error').play()
[docs]
@override
def restore_window_shared_state(
self, window: DocUIWindow, state: dict
) -> None:
"""Called when a window shared state is being restored."""
if not isinstance(window.request, dui1.Request):
return
# If desired, set the profile button that will be selected in
# the new window. We do this when coming back from creating a
# new profile/etc.
if (
window.request.path == '/'
and self._next_selected_profile is not None
):
state['selection'] = f'$(WIN)|profile.{self._next_selected_profile}'
# Only do this once (return to normal selection save/restore
# after).
self._next_selected_profile = None
def _on_profile_save(self, name: str) -> None:
# An editor we launched tells us it saved a profile.
# Have this one selected when we go back to the listing.
self._next_selected_profile = name
bui.pushcall(self._notify_profiles_changed)
def _on_profile_delete(self, name: str) -> None:
# An editor we launched tells us it deleted a profile.
# Ask the inventory list to select/show the profile right before
# the one we're deleting.
profiles = bui.app.config.get('Player Profiles', {})
items = list(profiles.items())
items.sort(key=lambda x: asserttype(x[0], str).lower())
namelower = name.lower()
prevname = items[0][0] if items else None
for item in items:
if item[0].lower() < namelower:
prevname = item[0]
else:
break
if prevname is not None:
self._next_selected_profile = prevname
self._notify_profiles_changed()
def _notify_profiles_changed(self) -> None:
import bascenev1 as bs
# If there's a team-chooser in existence, tell it the profile-list
# has probably changed.
session = bs.get_foreground_host_session()
if session is not None:
session.handlemessage(bs.PlayerProfilesChangedMessage())
def _get_profile_buttons(self) -> list[dui1.Button]:
# pylint: disable=too-many-locals
plus = bui.app.plus
assert plus is not None
classic = bui.app.classic
assert classic is not None
buttons: list[dui1.Button] = []
profiles = bui.app.config.get('Player Profiles', {})
items = list(profiles.items())
items.sort(key=lambda x: asserttype(x[0], str).lower())
account_name: str | None
if plus.get_v1_account_state() == 'signed_in':
account_name = plus.get_v1_account_display_string()
else:
account_name = None
spaz_appearances = classic.spaz_appearances
spaz_appearance_default = spaz_appearances['Spaz']
for p_name, p_info in items:
if p_name == '__account__' and account_name is None:
continue
color, highlight = classic.get_player_profile_colors(p_name)
tval = (
account_name
if p_name == '__account__'
else classic.get_player_profile_icon(p_name) + p_name
)
assert tval is not None
tcolor: Any = bui.safecolor(color, 0.4) + (1.0,)
assert len(tcolor) == 4
appearance = spaz_appearances.get(p_info['character'])
if appearance is None:
appearance = spaz_appearance_default
buttons.append(
dui1.Button(
texture='white',
size=(145, 175),
action=dui1.Local(
default_sound=False,
immediate_local_action='edit_profile',
immediate_local_action_args={'profile': p_name},
),
# color=(0.6, 0.5, 0.7, 1.0),
color=(1, 1, 1, 0.0),
widget_id=f'profile.{p_name}',
decorations=[
dui1.Image(
appearance.icon_texture,
position=(0, 15),
size=(140, 140),
mask_texture='characterIconMask',
tint_texture=appearance.icon_mask_texture,
tint_color=color,
tint2_color=highlight,
),
dui1.Text(
tval,
position=(0, -75),
size=(130, 40),
flatness=1.0,
shadow=1.0,
color=tcolor,
),
],
)
)
return buttons
def _new_profile(self, action: DocUILocalAction) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.profile.edit import EditProfileWindow
bui.getsound('swish').play()
plus = bui.app.plus
assert plus is not None
# Clamp at 100 profiles (otherwise the server will and that's less
# elegant looking).
profiles = bui.app.config.get('Player Profiles', {})
if len(profiles) > 100:
bui.screenmessage(
bui.Lstr(
translate=(
'serverResponses',
'Max number of profiles reached.',
)
),
color=(1, 0, 0),
)
bui.getsound('error').play()
return
action.window.main_window_replace(
lambda: EditProfileWindow(
existing_profile=None,
on_profile_save=bui.WeakCallPartial(self._on_profile_save),
on_profile_delete=bui.WeakCallPartial(self._on_profile_delete),
)
)
def _edit_profile(self, action: DocUILocalAction) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.profile.edit import EditProfileWindow
bui.getsound('swish').play()
profile = action.args.get('profile')
assert isinstance(profile, str)
action.window.main_window_replace(
lambda: EditProfileWindow(
profile,
on_profile_save=bui.WeakCallPartial(self._on_profile_save),
on_profile_delete=bui.WeakCallPartial(self._on_profile_delete),
)
)
# 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