Files
croccybot/src/modules/skins/editor/skinsetting.py

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}`"