Source code for bauiv1lib.profile.edit

# Released under the MIT License. See LICENSE for details.
#
"""Provides UI to edit a player profile."""

from __future__ import annotations

import random
from typing import cast, override

from bauiv1lib.colorpicker import ColorPicker
from bauiv1lib.characterpicker import CharacterPickerDelegate
from bauiv1lib.iconpicker import IconPickerDelegate
import bauiv1 as bui
import bascenev1 as bs


[docs] class EditProfileWindow( bui.MainWindow, CharacterPickerDelegate, IconPickerDelegate ): """Window for editing a player profile."""
[docs] def reload_window(self) -> None: """Transitions out and recreates ourself.""" # no-op if we're not in control. if not self.main_window_has_control(): return # Replace ourself with ourself, but keep the same back location. assert self.main_window_back_state is not None self.main_window_replace( EditProfileWindow(self.getname()), back_state=self.main_window_back_state, )
# def __del__(self) -> None: # print(f'~EditProfileWindow({id(self)})') def __init__( self, existing_profile: str | None, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # FIXME: Tidy this up a bit. # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-locals assert bui.app.classic is not None # print(f'EditProfileWindow({id(self)})') plus = bui.app.plus assert plus is not None self._existing_profile = existing_profile self._r = 'editProfileWindow' self._spazzes: list[str] = [] self._icon_textures: list[bui.Texture] = [] self._icon_tint_textures: list[bui.Texture] = [] # Grab profile colors or pick random ones. ( self._color, self._highlight, ) = bui.app.classic.get_player_profile_colors(existing_profile) uiscale = bui.app.ui_v1.uiscale self._width = width = 880.0 if uiscale is bui.UIScale.SMALL else 680.0 self._x_inset = x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 self._height = height = ( 500.0 if uiscale is bui.UIScale.SMALL else 400.0 if uiscale is bui.UIScale.MEDIUM else 450.0 ) yoffs = -42 if uiscale is bui.UIScale.SMALL else 0 spacing = 40 self._base_scale = ( 2.0 if uiscale is bui.UIScale.SMALL else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 ) top_extra = 70 if uiscale is bui.UIScale.SMALL else 15 super().__init__( root_widget=bui.containerwidget( size=(width, height + top_extra), scale=self._base_scale, stack_offset=(0, 0), toolbar_visibility=None, ), transition=transition, origin_widget=origin_widget, ) cancel_button = btn = bui.buttonwidget( parent=self._root_widget, position=(52 + x_inset, height - 60 + yoffs), size=(155, 60), scale=0.8, autoselect=True, label=bui.Lstr(resource='cancelText'), on_activate_call=self._cancel, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) save_button = btn = bui.buttonwidget( parent=self._root_widget, position=(width - (177 + x_inset), height - 60 + yoffs), size=(155, 60), autoselect=True, scale=0.8, label=bui.Lstr(resource='saveText'), ) bui.widget(edit=save_button, left_widget=cancel_button) bui.widget(edit=cancel_button, right_widget=save_button) bui.containerwidget(edit=self._root_widget, start_button=btn) bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, height - 38 + yoffs), size=(0, 0), text=( bui.Lstr(resource=f'{self._r}.titleNewText') if existing_profile is None else bui.Lstr(resource=f'{self._r}.titleEditText') ), color=bui.app.ui_v1.title_color, maxwidth=290, scale=1.0, h_align='center', v_align='center', ) # Make a list of spaz icons. self.refresh_characters() profile = bui.app.config.get('Player Profiles', {}).get( self._existing_profile, {} ) if 'global' in profile: self._global = profile['global'] else: self._global = False if 'icon' in profile: self._icon = profile['icon'] else: self._icon = bui.charstr(bui.SpecialChar.LOGO) assigned_random_char = False # Look for existing character choice or pick random one otherwise. try: icon_index = self._spazzes.index(profile['character']) except Exception: # Let's set the default icon to spaz for our first profile; after # that we go random. # (SCRATCH THAT.. we now hard-code account-profiles to start with # spaz which has a similar effect) # try: p_len = len(bui.app.config['Player Profiles']) # except Exception: p_len = 0 # if p_len == 0: icon_index = self._spazzes.index('Spaz') # else: random.seed() icon_index = random.randrange(len(self._spazzes)) assigned_random_char = True self._icon_index = icon_index bui.buttonwidget(edit=save_button, on_activate_call=self.save) v = height - 115.0 + yoffs self._name = ( '' if self._existing_profile is None else self._existing_profile ) self._is_account_profile = self._name == '__account__' # If we just picked a random character, see if it has specific # colors/highlights associated with it and assign them if so. if assigned_random_char: assert bui.app.classic is not None clr = bui.app.classic.spaz_appearances[ self._spazzes[icon_index] ].default_color if clr is not None: self._color = clr highlight = bui.app.classic.spaz_appearances[ self._spazzes[icon_index] ].default_highlight if highlight is not None: self._highlight = highlight # Assign a random name if they had none. if self._name == '': names = bs.get_random_names() self._name = names[random.randrange(len(names))] self._clipped_name_text = bui.textwidget( parent=self._root_widget, text='', position=(580 + x_inset, v - 8), flatness=1.0, shadow=0.0, scale=0.55, size=(0, 0), maxwidth=100, h_align='center', v_align='center', color=(1, 1, 0, 0.5), ) if not self._is_account_profile and not self._global: bui.textwidget( parent=self._root_widget, text=bui.Lstr(resource=f'{self._r}.nameText'), position=(200 + x_inset, v - 6), size=(0, 0), h_align='right', v_align='center', color=(1, 1, 1, 0.5), scale=0.9, ) self._upgrade_button = None if self._is_account_profile: if plus.get_v1_account_state() == 'signed_in': sval = plus.get_v1_account_display_string() else: sval = '??' bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v - 7), size=(0, 0), scale=1.2, text=sval, maxwidth=270, h_align='center', v_align='center', ) txtl = bui.Lstr( resource='editProfileWindow.accountProfileText' ).evaluate() b_width = min( 270.0, bui.get_string_width(txtl, suppress_warning=True) * 0.6, ) bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v - 39), size=(0, 0), scale=0.6, color=bui.app.ui_v1.infotextcolor, text=txtl, maxwidth=270, h_align='center', v_align='center', ) self._account_type_info_button = bui.buttonwidget( parent=self._root_widget, label='?', size=(15, 15), text_scale=0.6, position=(self._width * 0.5 + b_width * 0.5 + 13, v - 47), button_type='square', color=(0.6, 0.5, 0.65), autoselect=True, on_activate_call=self.show_account_profile_info, ) elif self._global: b_size = 60 self._icon_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width * 0.5 - 160 - b_size * 0.5, v - 38 - 15), size=(b_size, b_size), color=(0.6, 0.5, 0.6), label='', button_type='square', text_scale=1.2, on_activate_call=self._on_icon_press, ) self._icon_button_label = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5 - 160, v - 35), draw_controller=btn, h_align='center', v_align='center', size=(0, 0), color=(1, 1, 1), text='', scale=2.0, ) bui.textwidget( parent=self._root_widget, h_align='center', v_align='center', position=(self._width * 0.5 - 160, v - 55 - 15), size=(0, 0), draw_controller=btn, text=bui.Lstr(resource=f'{self._r}.iconText'), scale=0.7, color=bui.app.ui_v1.title_color, maxwidth=120, ) self._update_icon() bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v - 7), size=(0, 0), scale=1.2, text=self._name, maxwidth=240, h_align='center', v_align='center', ) # FIXME hard coded strings are bad txtl = bui.Lstr( resource='editProfileWindow.globalProfileText' ).evaluate() b_width = min( 240.0, bui.get_string_width(txtl, suppress_warning=True) * 0.6, ) bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v - 39), size=(0, 0), scale=0.6, color=bui.app.ui_v1.infotextcolor, text=txtl, maxwidth=240, h_align='center', v_align='center', ) self._account_type_info_button = bui.buttonwidget( parent=self._root_widget, label='?', size=(15, 15), text_scale=0.6, position=(self._width * 0.5 + b_width * 0.5 + 13, v - 47), button_type='square', color=(0.6, 0.5, 0.65), autoselect=True, on_activate_call=self.show_global_profile_info, ) else: self._text_field = bui.textwidget( parent=self._root_widget, position=(220 + x_inset, v - 30), size=(265, 40), text=self._name, h_align='left', v_align='center', max_chars=16, description=bui.Lstr(resource=f'{self._r}.nameDescriptionText'), autoselect=True, editable=True, padding=4, color=(0.9, 0.9, 0.9, 1.0), on_return_press_call=bui.Call(save_button.activate), ) # FIXME hard coded strings are bad txtl = bui.Lstr( resource='editProfileWindow.localProfileText' ).evaluate() b_width = min( 270.0, bui.get_string_width(txtl, suppress_warning=True) * 0.6, ) bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v - 43), size=(0, 0), scale=0.6, color=bui.app.ui_v1.infotextcolor, text=txtl, maxwidth=270, h_align='center', v_align='center', ) self._account_type_info_button = bui.buttonwidget( parent=self._root_widget, label='?', size=(15, 15), text_scale=0.6, position=(self._width * 0.5 + b_width * 0.5 + 13, v - 50), button_type='square', color=(0.6, 0.5, 0.65), autoselect=True, on_activate_call=self.show_local_profile_info, ) self._upgrade_button = bui.buttonwidget( parent=self._root_widget, label=bui.Lstr(resource='upgradeText'), size=(40, 17), text_scale=1.0, button_type='square', position=(self._width * 0.5 + b_width * 0.5 + 13 + 43, v - 51), color=(0.6, 0.5, 0.65), autoselect=True, on_activate_call=self.upgrade_profile, ) self._random_name_button = bui.buttonwidget( parent=self._root_widget, label=bui.Lstr(resource='randomText'), size=(30, 20), position=(495 + x_inset, v - 20), button_type='square', color=(0.6, 0.5, 0.65), autoselect=True, on_activate_call=self.assign_random_name, ) self._update_clipped_name() self._clipped_name_timer = bui.AppTimer( 0.333, bui.WeakCall(self._update_clipped_name), repeat=True ) v -= spacing * 3.0 b_size = 80 b_size_2 = 100 b_offs = 150 self._color_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width * 0.5 - b_offs - b_size * 0.5, v - 50), size=(b_size, b_size), color=self._color, label='', button_type='square', ) origin = self._color_button.get_screen_space_center() bui.buttonwidget( edit=self._color_button, on_activate_call=bui.WeakCall(self._make_picker, 'color', origin), ) bui.textwidget( parent=self._root_widget, h_align='center', v_align='center', position=(self._width * 0.5 - b_offs, v - 65), size=(0, 0), draw_controller=btn, text=bui.Lstr(resource=f'{self._r}.colorText'), scale=0.7, color=bui.app.ui_v1.title_color, maxwidth=120, ) self._character_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width * 0.5 - b_size_2 * 0.5, v - 60), up_widget=self._account_type_info_button, on_activate_call=self._on_character_press, size=(b_size_2, b_size_2), label='', color=(1, 1, 1), mask_texture=bui.gettexture('characterIconMask'), ) if not self._is_account_profile and not self._global: bui.containerwidget( edit=self._root_widget, selected_child=self._text_field ) bui.textwidget( parent=self._root_widget, h_align='center', v_align='center', position=(self._width * 0.5, v - 80), size=(0, 0), draw_controller=btn, text=bui.Lstr(resource=f'{self._r}.characterText'), scale=0.7, color=bui.app.ui_v1.title_color, maxwidth=130, ) self._highlight_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width * 0.5 + b_offs - b_size * 0.5, v - 50), up_widget=( self._upgrade_button if self._upgrade_button is not None else self._account_type_info_button ), size=(b_size, b_size), color=self._highlight, label='', button_type='square', ) if not self._is_account_profile and not self._global: bui.widget(edit=cancel_button, down_widget=self._text_field) bui.widget(edit=save_button, down_widget=self._text_field) bui.widget(edit=self._color_button, up_widget=self._text_field) bui.widget( edit=self._account_type_info_button, down_widget=self._character_button, ) origin = self._highlight_button.get_screen_space_center() bui.buttonwidget( edit=self._highlight_button, on_activate_call=bui.WeakCall( self._make_picker, 'highlight', origin ), ) bui.textwidget( parent=self._root_widget, h_align='center', v_align='center', position=(self._width * 0.5 + b_offs, v - 65), size=(0, 0), draw_controller=btn, text=bui.Lstr(resource=f'{self._r}.highlightText'), scale=0.7, color=bui.app.ui_v1.title_color, maxwidth=120, ) self._update_character()
[docs] @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. cls = type(self) # Pull things out of self here; if we do it within the lambda # we'll keep ourself alive which is bad. existing_profile = self._existing_profile return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget, existing_profile=existing_profile, ) )
[docs] def assign_random_name(self) -> None: """Assigning a random name to the player.""" names = bs.get_random_names() name = names[random.randrange(len(names))] bui.textwidget( edit=self._text_field, text=name, )
[docs] def upgrade_profile(self) -> None: """Attempt to upgrade the profile to global.""" from bauiv1lib.account.signin import show_sign_in_prompt from bauiv1lib.profile import upgrade as pupgrade new_name = self.getname().strip() if self._existing_profile and self._existing_profile != new_name: bui.screenmessage( 'Unsaved changes found; you must save first.', color=(1, 0, 0) ) bui.getsound('error').play() return plus = bui.app.plus assert plus is not None if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return pupgrade.ProfileUpgradeWindow(self)
[docs] def show_account_profile_info(self) -> None: """Show an explanation of account profiles.""" from bauiv1lib.confirm import ConfirmWindow icons_str = ' '.join( [ bui.charstr(n) for n in [ bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO, bui.SpecialChar.GAME_CENTER_LOGO, bui.SpecialChar.LOCAL_ACCOUNT, bui.SpecialChar.OCULUS_LOGO, bui.SpecialChar.NVIDIA_LOGO, bui.SpecialChar.V2_LOGO, ] ] ) txtl = bui.Lstr( resource='editProfileWindow.accountProfileInfoText', subs=[('${ICONS}', icons_str)], ) ConfirmWindow( txtl, cancel_button=False, width=500, height=300, origin_widget=self._account_type_info_button, )
[docs] def show_local_profile_info(self) -> None: """Show an explanation of local profiles.""" from bauiv1lib.confirm import ConfirmWindow txtl = bui.Lstr(resource='editProfileWindow.localProfileInfoText') ConfirmWindow( txtl, cancel_button=False, width=600, height=250, origin_widget=self._account_type_info_button, )
[docs] def show_global_profile_info(self) -> None: """Show an explanation of global profiles.""" from bauiv1lib.confirm import ConfirmWindow txtl = bui.Lstr(resource='editProfileWindow.globalProfileInfoText') ConfirmWindow( txtl, cancel_button=False, width=600, height=250, origin_widget=self._account_type_info_button, )
[docs] def refresh_characters(self) -> None: """Refresh available characters/icons.""" from bascenev1lib.actor import spazappearance assert bui.app.classic is not None self._spazzes = spazappearance.get_appearances() self._spazzes.sort() self._icon_textures = [ bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture) for s in self._spazzes ] self._icon_tint_textures = [ bui.gettexture( bui.app.classic.spaz_appearances[s].icon_mask_texture ) for s in self._spazzes ]
[docs] @override def on_icon_picker_pick(self, icon: str) -> None: """An icon has been selected by the picker.""" self._icon = icon self._update_icon()
[docs] @override def on_icon_picker_get_more_press(self) -> None: """User wants to get more icons.""" from bauiv1lib.store.browser import StoreBrowserWindow if not self.main_window_has_control(): return self.main_window_replace( StoreBrowserWindow( minimal_toolbars=True, show_tab=StoreBrowserWindow.TabID.ICONS, ) )
[docs] @override def on_character_picker_pick(self, character: str) -> None: """A character has been selected by the picker.""" if not self._root_widget: return # The player could have bought a new one while the picker was # up. self.refresh_characters() self._icon_index = ( self._spazzes.index(character) if character in self._spazzes else 0 ) self._update_character()
[docs] @override def on_character_picker_get_more_press(self) -> None: from bauiv1lib.store.browser import StoreBrowserWindow if not self.main_window_has_control(): return self.main_window_replace( StoreBrowserWindow( minimal_toolbars=True, show_tab=StoreBrowserWindow.TabID.CHARACTERS, ) )
def _on_character_press(self) -> None: from bauiv1lib import characterpicker characterpicker.CharacterPicker( parent=self._root_widget, position=self._character_button.get_screen_space_center(), selected_character=self._spazzes[self._icon_index], delegate=self, tint_color=self._color, tint2_color=self._highlight, ) def _on_icon_press(self) -> None: from bauiv1lib import iconpicker iconpicker.IconPicker( parent=self._root_widget, position=self._icon_button.get_screen_space_center(), selected_icon=self._icon, delegate=self, tint_color=self._color, tint2_color=self._highlight, ) def _make_picker( self, picker_type: str, origin: tuple[float, float] ) -> None: if picker_type == 'color': initial_color = self._color elif picker_type == 'highlight': initial_color = self._highlight else: raise ValueError('invalid picker_type: ' + picker_type) ColorPicker( parent=self._root_widget, position=origin, offset=( self._base_scale * (-100 if picker_type == 'color' else 100), 0, ), initial_color=initial_color, delegate=self, tag=picker_type, ) def _cancel(self) -> None: self.main_window_back() def _set_color(self, color: tuple[float, float, float]) -> None: self._color = color if self._color_button: bui.buttonwidget(edit=self._color_button, color=color) def _set_highlight(self, color: tuple[float, float, float]) -> None: self._highlight = color if self._highlight_button: bui.buttonwidget(edit=self._highlight_button, color=color)
[docs] def color_picker_closing(self, picker: ColorPicker) -> None: """Called when a color picker is closing.""" if not self._root_widget: return tag = picker.get_tag() if tag == 'color': bui.containerwidget( edit=self._root_widget, selected_child=self._color_button ) elif tag == 'highlight': bui.containerwidget( edit=self._root_widget, selected_child=self._highlight_button ) else: print('color_picker_closing got unknown tag ' + str(tag))
[docs] def color_picker_selected_color( self, picker: ColorPicker, color: tuple[float, float, float] ) -> None: """Called when a color is selected in a color picker.""" if not self._root_widget: return tag = picker.get_tag() if tag == 'color': self._set_color(color) elif tag == 'highlight': self._set_highlight(color) else: print('color_picker_selected_color got unknown tag ' + str(tag)) self._update_character()
def _update_clipped_name(self) -> None: plus = bui.app.plus assert plus is not None if not self._clipped_name_text: return name = self.getname() if name == '__account__': name = ( plus.get_v1_account_name() if plus.get_v1_account_state() == 'signed_in' else '???' ) if len(name) > 10 and not (self._global or self._is_account_profile): name = name.strip() display_name = (name[:10] + '...') if len(name) > 10 else name bui.textwidget( edit=self._clipped_name_text, text=bui.Lstr( resource='inGameClippedNameText', subs=[('${NAME}', display_name)], ), ) else: bui.textwidget(edit=self._clipped_name_text, text='') def _update_character(self, change: int = 0) -> None: self._icon_index = (self._icon_index + change) % len(self._spazzes) if self._character_button: bui.buttonwidget( edit=self._character_button, texture=self._icon_textures[self._icon_index], tint_texture=self._icon_tint_textures[self._icon_index], tint_color=self._color, tint2_color=self._highlight, ) def _update_icon(self) -> None: if self._icon_button_label: bui.textwidget(edit=self._icon_button_label, text=self._icon)
[docs] def getname(self) -> str: """Return the current profile name value.""" if self._is_account_profile: new_name = '__account__' elif self._global: new_name = self._name else: new_name = cast(str, bui.textwidget(query=self._text_field)) return new_name
[docs] def save(self, transition_out: bool = True) -> bool: """Save has been selected.""" # 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 False plus = bui.app.plus assert plus is not None new_name = self.getname().strip() if not new_name: bui.screenmessage(bui.Lstr(resource='nameNotEmptyText')) bui.getsound('error').play() return False # Make sure we're not renaming to another existing profile. profiles: dict = bui.app.config.get('Player Profiles', {}) if self._existing_profile != new_name and new_name in profiles.keys(): bui.screenmessage( bui.Lstr(resource='editProfileWindow.profileAlreadyExistsText') ) bui.getsound('error').play() return False if transition_out: bui.getsound('gunCocking').play() # Delete old in case we're renaming. if self._existing_profile and self._existing_profile != new_name: plus.add_v1_account_transaction( { 'type': 'REMOVE_PLAYER_PROFILE', 'name': self._existing_profile, } ) # Also lets be aware we're no longer global if we're taking # a new name (will need to re-request it). self._global = False plus.add_v1_account_transaction( { 'type': 'ADD_PLAYER_PROFILE', 'name': new_name, 'profile': { 'character': self._spazzes[self._icon_index], 'color': list(self._color), 'global': self._global, 'icon': self._icon, 'highlight': list(self._highlight), }, } ) if transition_out: plus.run_v1_account_transactions() self.main_window_back() return True
# 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