feat(skins): Implement CustomSkin editor.
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
from typing import Literal, Optional
|
||||
from enum import Enum
|
||||
|
||||
from discord.ui import TextInput
|
||||
|
||||
from meta import LionBot
|
||||
from meta.errors import UserInputError
|
||||
from babel.translator import LazyStr
|
||||
from gui.base import Card, FieldDesc, AppSkin
|
||||
|
||||
from .. import babel
|
||||
from ..skinlib import CustomSkin
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class SettingInputType(Enum):
|
||||
SkinInput = -1
|
||||
ModalInput = 0
|
||||
MenuInput = 1
|
||||
ButtonInput = 2
|
||||
|
||||
|
||||
class Setting:
|
||||
"""
|
||||
An abstract base interface for a custom skin 'setting'.
|
||||
|
||||
A skin setting is considered to be some readable and usually writeable
|
||||
information extractable from a `CustomSkin`.
|
||||
This will usually consist of the value of one or more properties,
|
||||
which are themselves associated to fields of GUI Cards.
|
||||
|
||||
The methods in this ABC describe the interface for such a setting.
|
||||
Each method accepts a `CustomSkin`,
|
||||
and an implementation should describe how to
|
||||
get, set, parse, format, or display the setting
|
||||
for that given skin.
|
||||
|
||||
This is very similar to how Settings are implemented in the bot,
|
||||
except here all settings have a shared external source of state, the CustomSkin.
|
||||
Thus, each setting is simply an instance of an appropriate setting class,
|
||||
rather than a class itself.
|
||||
"""
|
||||
|
||||
# What type of input method this setting requires for input
|
||||
input_type: SettingInputType = SettingInputType.ModalInput
|
||||
|
||||
def __init__(self, *args, display_name, description, **kwargs):
|
||||
self.display_name: LazyStr = display_name
|
||||
self.description: LazyStr = description
|
||||
|
||||
def default_value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
"""
|
||||
The default value of this setting in this skin.
|
||||
|
||||
This takes into account base skin data and localisation.
|
||||
May be `None` if the setting does not have a default value.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
"""
|
||||
The current value of this setting from this skin.
|
||||
|
||||
May be None if the setting is not set or does not have a value.
|
||||
Usually should not take into account defaults.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_in(self, skin: CustomSkin, value: Optional[str]):
|
||||
"""
|
||||
Set this setting to the given value in this skin.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def format_value_in(self, skin: CustomSkin, value: Optional[str]) -> str:
|
||||
"""
|
||||
Format the given setting value for display (typically in a setting table).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def parse_input(self, skin: CustomSkin, userstr: str) -> Optional[str]:
|
||||
"""
|
||||
Parse a user provided string into a value for this setting.
|
||||
|
||||
Will raise 'UserInputError' with a readable message if parsing fails.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def make_input_field(self, skin: CustomSkin) -> TextInput:
|
||||
"""
|
||||
Create a TextInput field for this setting, using the current value.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PropertySetting(Setting):
|
||||
"""
|
||||
A skin setting corresponding to a single property of a single card.
|
||||
|
||||
Note that this is still abstract,
|
||||
as it does not implement any formatting or parsing methods.
|
||||
|
||||
This will usually (but may not always) correspond to a single Field of the card skin.
|
||||
"""
|
||||
def __init__(self, card: type[Card], property_name: str, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.card = card
|
||||
self.property_name = property_name
|
||||
|
||||
@property
|
||||
def card_id(self):
|
||||
"""
|
||||
The `card_id` of the Card class this setting belongs to.
|
||||
"""
|
||||
return self.card.card_id
|
||||
|
||||
@property
|
||||
def field(self) -> Optional[FieldDesc]:
|
||||
"""
|
||||
The CardSkin field overwrriten by this setting, if it exists.
|
||||
"""
|
||||
return self.card.skin._fields.get(self.property_name, None)
|
||||
|
||||
def default_value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
"""
|
||||
For a PropertySetting, the default value is determined as follows:
|
||||
base skin value from:
|
||||
- card base skin
|
||||
- custom base skin
|
||||
- global app base skin
|
||||
fallback (field) value from the CardSkin
|
||||
"""
|
||||
base_skin = skin.get_prop(self.card_id, 'base_skin_id')
|
||||
base_skin = base_skin or skin.base_skin_name
|
||||
base_skin = base_skin or skin.cog.current_default
|
||||
|
||||
app_skin_args = AppSkin.get(base_skin).for_card(self.card_id)
|
||||
|
||||
if self.property_name in app_skin_args:
|
||||
return app_skin_args[self.property_name]
|
||||
elif self.field:
|
||||
return self.field.default
|
||||
else:
|
||||
return None
|
||||
|
||||
def value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
return skin.get_prop(self.card_id, self.property_name)
|
||||
|
||||
def set_in(self, skin: CustomSkin, value: Optional[str]):
|
||||
skin.set_prop(self.card_id, self.property_name, value)
|
||||
|
||||
|
||||
class _ColourInterface(Setting):
|
||||
"""
|
||||
Skin setting mixin for parsing and formatting colour typed settings.
|
||||
"""
|
||||
|
||||
def format_value_in(self, skin: CustomSkin, value: Optional[str]) -> str:
|
||||
if value:
|
||||
formatted = f"`{value}`"
|
||||
else:
|
||||
formatted = skin.bot.translator.t(_p(
|
||||
'skinsettings|colours|format:not_set',
|
||||
"Not Set"
|
||||
))
|
||||
return formatted
|
||||
|
||||
async def parse_input(self, skin: CustomSkin, userstr: str) -> Optional[str]:
|
||||
stripped = userstr.strip('# ').upper()
|
||||
if not stripped:
|
||||
value = None
|
||||
elif len(stripped) not in (6, 8) or any(c not in '0123456789ABCDEF' for c in stripped):
|
||||
raise UserInputError(
|
||||
skin.bot.translator.t(_p(
|
||||
'skinsettings|colours|parse|error:invalid',
|
||||
"Could not parse `{given}` as a colour!"
|
||||
" Please use RGB/RGBA format (e.g. `#ABABABF0`)."
|
||||
)).format(given=userstr)
|
||||
)
|
||||
else:
|
||||
value = f"#{stripped}"
|
||||
return value
|
||||
|
||||
def make_input_field(self, skin: CustomSkin) -> TextInput:
|
||||
t = skin.bot.translator.t
|
||||
|
||||
value = self.value_in(skin)
|
||||
default_value = self.default_value_in(skin)
|
||||
|
||||
label = t(self.display_name)
|
||||
default = value
|
||||
if default_value:
|
||||
placeholder = f"{default_value} ({t(self.description)})"
|
||||
else:
|
||||
placeholder = t(self.description)
|
||||
|
||||
return TextInput(
|
||||
label=label,
|
||||
placeholder=placeholder,
|
||||
default=default,
|
||||
min_length=0,
|
||||
max_length=9,
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class ColourSetting(_ColourInterface, PropertySetting):
|
||||
"""
|
||||
A Property skin setting representing a single colour field.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class SkinSetting(PropertySetting):
|
||||
"""
|
||||
A Property setting representing the base skin of a card.
|
||||
"""
|
||||
input_type = SettingInputType.SkinInput
|
||||
|
||||
def format_value_in(self, skin: CustomSkin, value: Optional[str]) -> str:
|
||||
if value:
|
||||
app_skin = AppSkin.get(value)
|
||||
formatted = f"`{app_skin.display_name}`"
|
||||
else:
|
||||
formatted = skin.bot.translator.t(_p(
|
||||
'skinsettings|base_skin|format:not_set',
|
||||
"Default"
|
||||
))
|
||||
return formatted
|
||||
|
||||
def default_value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
return skin.base_skin_name
|
||||
|
||||
|
||||
class CompoundSetting(Setting):
|
||||
"""
|
||||
A Setting combining several PropertySettings across (potentially) multiple cards.
|
||||
"""
|
||||
NOTSHARED = ''
|
||||
|
||||
def __init__(self, *settings: PropertySetting, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.settings = settings
|
||||
|
||||
def default_value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
"""
|
||||
The default value of a CompoundSetting is the shared default of the component settings.
|
||||
|
||||
If the components do not share a default value, returns None.
|
||||
"""
|
||||
value = None
|
||||
for setting in self.settings:
|
||||
setting_value = setting.default_value_in(skin)
|
||||
if setting_value is None:
|
||||
value = None
|
||||
break
|
||||
if value is None:
|
||||
value = setting_value
|
||||
elif value != setting_value:
|
||||
value = None
|
||||
break
|
||||
return value
|
||||
|
||||
def value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
"""
|
||||
The value of a compound setting is the shared value of the components.
|
||||
"""
|
||||
value = self.NOTSHARED
|
||||
for setting in self.settings:
|
||||
setting_value = setting.value_in(skin) or setting.default_value_in(skin)
|
||||
|
||||
if value is self.NOTSHARED:
|
||||
value = setting_value
|
||||
elif value != setting_value:
|
||||
value = self.NOTSHARED
|
||||
break
|
||||
return value
|
||||
|
||||
def set_in(self, skin: CustomSkin, value: Optional[str]):
|
||||
"""
|
||||
Set all of the components individually.
|
||||
"""
|
||||
for setting in self.settings:
|
||||
setting.set_in(skin, value)
|
||||
|
||||
|
||||
class ColoursSetting(_ColourInterface, CompoundSetting):
|
||||
"""
|
||||
Compound setting representing multiple colours.
|
||||
"""
|
||||
def format_value_in(self, skin: CustomSkin, value: Optional[str]) -> str:
|
||||
if value is self.NOTSHARED:
|
||||
return "Mixed"
|
||||
elif value is None:
|
||||
return "Not Set"
|
||||
else:
|
||||
return f"`{value}`"
|
||||
|
||||
Reference in New Issue
Block a user