Source code for bauiv1lib.profile.browser

# Released under the MIT License. See LICENSE for details.
#
"""UI functionality related to browsing player profiles."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

import bauiv1 as bui
import bascenev1 as bs

if TYPE_CHECKING:
    from typing import Any


[docs] class ProfileBrowserWindow(bui.Window): """Window for browsing player profiles.""" def __init__( self, transition: str = 'in_right', in_main_menu: bool = True, selected_profile: str | None = None, origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals self._in_main_menu = in_main_menu if self._in_main_menu: back_label = bui.Lstr(resource='backText') else: back_label = bui.Lstr(resource='doneText') assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale self._width = 800.0 if uiscale is bui.UIScale.SMALL else 600.0 x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 self._height = ( 360.0 if uiscale is bui.UIScale.SMALL else 385.0 if uiscale is bui.UIScale.MEDIUM else 410.0 ) # If we're being called up standalone, handle pause/resume ourself. if not self._in_main_menu: assert bui.app.classic is not None bui.app.classic.pause() # If they provided an origin-widget, scale up from that. scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' scale_origin = origin_widget.get_screen_space_center() transition = 'in_scale' else: self._transition_out = 'out_right' scale_origin = None self._r = 'playerProfilesWindow' # Ensure we've got an account-profile in cases where we're signed in. assert bui.app.classic is not None bui.app.classic.accounts.ensure_have_account_player_profile() top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), transition=transition, scale_origin_stack_offset=scale_origin, scale=( 2.2 if uiscale is bui.UIScale.SMALL else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, -14) if uiscale is bui.UIScale.SMALL else (0, 0) ), ) ) self._back_button = btn = bui.buttonwidget( parent=self._root_widget, position=(40 + x_inset, self._height - 59), size=(120, 60), scale=0.8, label=back_label, button_type='back' if self._in_main_menu else None, autoselect=True, on_activate_call=self._back, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 36), size=(0, 0), text=bui.Lstr(resource=self._r + '.titleText'), maxwidth=300, color=bui.app.ui_v1.title_color, scale=0.9, h_align='center', v_align='center', ) if self._in_main_menu: bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), label=bui.charstr(bui.SpecialChar.BACK), ) scroll_height = self._height - 140.0 self._scroll_width = self._width - (188 + x_inset * 2) v = self._height - 84.0 h = 50 + x_inset b_color = (0.6, 0.53, 0.63) scl = ( 1.055 if uiscale is bui.UIScale.SMALL else 1.18 if uiscale is bui.UIScale.MEDIUM else 1.3 ) v -= 70.0 * scl self._new_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(80, 66.0 * scl), on_activate_call=self._new_profile, color=b_color, button_type='square', autoselect=True, textcolor=(0.75, 0.7, 0.8), text_scale=0.7, label=bui.Lstr(resource=self._r + '.newButtonText'), ) v -= 70.0 * scl self._edit_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(80, 66.0 * scl), on_activate_call=self._edit_profile, color=b_color, button_type='square', autoselect=True, textcolor=(0.75, 0.7, 0.8), text_scale=0.7, label=bui.Lstr(resource=self._r + '.editButtonText'), ) v -= 70.0 * scl self._delete_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(80, 66.0 * scl), on_activate_call=self._delete_profile, color=b_color, button_type='square', autoselect=True, textcolor=(0.75, 0.7, 0.8), text_scale=0.7, label=bui.Lstr(resource=self._r + '.deleteButtonText'), ) v = self._height - 87 bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 71), size=(0, 0), text=bui.Lstr(resource=self._r + '.explanationText'), color=bui.app.ui_v1.infotextcolor, maxwidth=self._width * 0.83, scale=0.6, h_align='center', v_align='center', ) self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, position=(140 + x_inset, v - scroll_height), size=(self._scroll_width, scroll_height), ) bui.widget( edit=self._scrollwidget, autoselect=True, left_widget=self._new_button, ) bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._scroll_width, 32), background=False, ) v -= 255 self._profiles: dict[str, dict[str, Any]] | None = None self._selected_profile = selected_profile self._profile_widgets: list[bui.Widget] = [] self._refresh() self._restore_state() def _new_profile(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.profile.edit import EditProfileWindow from bauiv1lib.purchase import PurchaseWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return plus = bui.app.plus assert plus is not None # Limit to a handful profiles if they don't have pro-options. max_non_pro_profiles = plus.get_v1_account_misc_read_val('mnpp', 5) assert self._profiles is not None assert bui.app.classic is not None if ( not bui.app.classic.accounts.have_pro_options() and len(self._profiles) >= max_non_pro_profiles ): PurchaseWindow( items=['pro'], header_text=bui.Lstr( resource='unlockThisProfilesText', subs=[('${NUM}', str(max_non_pro_profiles))], ), ) return # Clamp at 100 profiles (otherwise the server will and that's less # elegant looking). if len(self._profiles) > 100: bui.screenmessage( bui.Lstr( translate=( 'serverResponses', 'Max number of profiles reached.', ) ), color=(1, 0, 0), ) bui.getsound('error').play() return self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') bui.app.ui_v1.set_main_menu_window( EditProfileWindow( existing_profile=None, in_main_menu=self._in_main_menu ).get_root_widget(), from_window=self._root_widget if self._in_main_menu else False, ) def _delete_profile(self) -> None: # pylint: disable=cyclic-import from bauiv1lib import confirm if self._selected_profile is None: bui.getsound('error').play() bui.screenmessage( bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) ) return if self._selected_profile == '__account__': bui.getsound('error').play() bui.screenmessage( bui.Lstr(resource=self._r + '.cantDeleteAccountProfileText'), color=(1, 0, 0), ) return confirm.ConfirmWindow( bui.Lstr( resource=self._r + '.deleteConfirmText', subs=[('${PROFILE}', self._selected_profile)], ), self._do_delete_profile, 350, ) def _do_delete_profile(self) -> None: plus = bui.app.plus assert plus is not None plus.add_v1_account_transaction( {'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile} ) plus.run_v1_account_transactions() bui.getsound('shieldDown').play() self._refresh() # Select profile list. bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) def _edit_profile(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.profile.edit import EditProfileWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return if self._selected_profile is None: bui.getsound('error').play() bui.screenmessage( bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) ) return self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( EditProfileWindow( self._selected_profile, in_main_menu=self._in_main_menu ).get_root_widget(), from_window=self._root_widget if self._in_main_menu else False, ) def _select(self, name: str, index: int) -> None: del index # Unused. self._selected_profile = name def _back(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.account.settings import AccountSettingsWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return assert bui.app.classic is not None self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if self._in_main_menu: assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( AccountSettingsWindow(transition='in_left').get_root_widget(), from_window=self._root_widget, ) # If we're being called up standalone, handle pause/resume ourself. else: bui.app.classic.resume() def _refresh(self) -> None: # pylint: disable=too-many-locals # pylint: disable=too-many-statements from efro.util import asserttype from bascenev1 import PlayerProfilesChangedMessage from bascenev1lib.actor import spazappearance assert bui.app.classic is not None plus = bui.app.plus assert plus is not None old_selection = self._selected_profile # Delete old. while self._profile_widgets: self._profile_widgets.pop().delete() self._profiles = bui.app.config.get('Player Profiles', {}) assert self._profiles is not None items = list(self._profiles.items()) items.sort(key=lambda x: asserttype(x[0], str).lower()) spazzes = spazappearance.get_appearances() spazzes.sort() icon_textures = [ bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture) for s in spazzes ] icon_tint_textures = [ bui.gettexture( bui.app.classic.spaz_appearances[s].icon_mask_texture ) for s in spazzes ] index = 0 y_val = 35 * (len(self._profiles) - 1) account_name: str | None if plus.get_v1_account_state() == 'signed_in': account_name = plus.get_v1_account_display_string() else: account_name = None widget_to_select = None for p_name, p_info in items: if p_name == '__account__' and account_name is None: continue color, _highlight = bui.app.classic.get_player_profile_colors( p_name ) scl = 1.1 tval = ( account_name if p_name == '__account__' else bui.app.classic.get_player_profile_icon(p_name) + p_name ) try: char_index = spazzes.index(p_info['character']) except Exception: char_index = spazzes.index('Spaz') assert isinstance(tval, str) txtw = bui.textwidget( parent=self._subcontainer, position=(5, y_val), size=((self._width - 210) / scl, 28), text=bui.Lstr(value=f' {tval}'), h_align='left', v_align='center', on_select_call=bui.WeakCall(self._select, p_name, index), maxwidth=self._scroll_width * 0.86, corner_scale=scl, color=bui.safecolor(color, 0.4), always_highlight=True, on_activate_call=bui.Call(self._edit_button.activate), selectable=True, ) character = bui.imagewidget( parent=self._subcontainer, position=(0, y_val), size=(30, 30), color=(1, 1, 1), mask_texture=bui.gettexture('characterIconMask'), tint_color=color, tint2_color=_highlight, texture=icon_textures[char_index], tint_texture=icon_tint_textures[char_index], ) if index == 0: bui.widget(edit=txtw, up_widget=self._back_button) if self._selected_profile is None: self._selected_profile = p_name bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) self._profile_widgets.append(txtw) self._profile_widgets.append(character) # Select/show this one if it was previously selected # (but defer till after this loop since our height is # still changing). if p_name == old_selection: widget_to_select = txtw index += 1 y_val -= 35 bui.containerwidget( edit=self._subcontainer, size=(self._scroll_width, index * 35), ) if widget_to_select is not None: bui.containerwidget( edit=self._subcontainer, selected_child=widget_to_select, visible_child=widget_to_select, ) # 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(PlayerProfilesChangedMessage()) def _save_state(self) -> None: try: sel = self._root_widget.get_selected_child() if sel == self._new_button: sel_name = 'New' elif sel == self._edit_button: sel_name = 'Edit' elif sel == self._delete_button: sel_name = 'Delete' elif sel == self._scrollwidget: sel_name = 'Scroll' else: sel_name = 'Back' assert bui.app.classic is not None bui.app.ui_v1.window_states[type(self)] = sel_name except Exception: logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: try: assert bui.app.classic is not None sel_name = bui.app.ui_v1.window_states.get(type(self)) if sel_name == 'Scroll': sel = self._scrollwidget elif sel_name == 'New': sel = self._new_button elif sel_name == 'Delete': sel = self._delete_button elif sel_name == 'Edit': sel = self._edit_button elif sel_name == 'Back': sel = self._back_button else: # By default we select our scroll widget if we have profiles; # otherwise our new widget. if not self._profile_widgets: sel = self._new_button else: sel = self._scrollwidget bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: logging.exception('Error restoring state for %s.', self)