299 lines
9.3 KiB
Python
299 lines
9.3 KiB
Python
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}`"
|