1044 lines
42 KiB
Python
1044 lines
42 KiB
Python
from AAA3A_utils import CogsUtils # isort:skip
|
|
from redbot.core import commands # isort:skip
|
|
from redbot.core.bot import Red # isort:skip
|
|
from redbot.core.i18n import Translator # isort:skip
|
|
import discord # isort:skip
|
|
import typing # isort:skip
|
|
|
|
import asyncio
|
|
import re
|
|
|
|
import emoji as _emoji
|
|
from redbot.core.utils.chat_formatting import humanize_list
|
|
|
|
from .board import Board
|
|
from .color import Color
|
|
from .constants import (
|
|
ALPHABETS,
|
|
IMAGE_EXTENSION,
|
|
LETTER_TO_NUMBER,
|
|
MAIN_COLORS_DICT,
|
|
NUMBERS,
|
|
base_colors_options,
|
|
) # NOQA
|
|
from .tools import (
|
|
BrushTool,
|
|
DarkenTool,
|
|
EraseTool,
|
|
EyedropperTool,
|
|
FillTool,
|
|
InverseTool,
|
|
LightenTool,
|
|
ReplaceTool,
|
|
Tool,
|
|
) # NOQA
|
|
|
|
_: Translator = Translator("Draw", __file__)
|
|
|
|
ADD_COLORS_EMOJI = "🏳️🌈"
|
|
ADD_EMOJIS_EMOJI = discord.PartialEmoji(name="emojismiley", id=1056857231125123152) # "😃"
|
|
MIX_COLORS_EMOJI = "🔀"
|
|
SET_CURSOR_EMOJI = discord.PartialEmoji(name="ABCD", id=1032565203608547328)
|
|
AUTO_DRAW_EMOJI = discord.PartialEmoji(name="auto_draw", id=1032565224903016449) # "🔄"
|
|
SELECT_EMOJI = discord.PartialEmoji(name="select_tool", id=1037847279169704028) # "📓"
|
|
CURSOR_DISPLAY_EMOJI = "📍"
|
|
RAW_PAINT_EMOJI = "📤"
|
|
|
|
UP_LEFT_EMOJI = discord.PartialEmoji(name="up_left", id=1032565175930343484) # "↖️"
|
|
UP_EMOJI = discord.PartialEmoji(name="up", id=1032564978676400148) # "⬆️"
|
|
UP_RIGHT_EMOJI = discord.PartialEmoji(name="up_right", id=1032564997869543464) # "↗️"
|
|
LEFT_EMOJI = discord.PartialEmoji(name="left", id=1032565106934022185) # "⬅️"
|
|
RIGHT_EMOJI = discord.PartialEmoji(name="right", id=1032565019352764438) # "➡️"
|
|
DOWN_LEFT_EMOJI = discord.PartialEmoji(name="down_left", id=1032565090223935518) # "↙️"
|
|
DOWN_EMOJI = discord.PartialEmoji(name="down", id=1032565072981131324) # "⬇️"
|
|
DOWN_RIGHT_EMOJI = discord.PartialEmoji(name="down_right", id=1032565043604230214) # "↘️"
|
|
|
|
|
|
class Notification:
|
|
def __init__(
|
|
self,
|
|
content: typing.Optional[str] = "",
|
|
*,
|
|
emoji: typing.Optional[
|
|
typing.Union[discord.PartialEmoji, discord.Emoji]
|
|
] = discord.PartialEmoji.from_str("🔔"),
|
|
view: discord.ui.View,
|
|
):
|
|
self.emoji: typing.Union[discord.PartialEmoji, discord.Emoji] = emoji
|
|
self.content: str = content
|
|
self.view: discord.ui.View = view
|
|
|
|
async def edit(
|
|
self,
|
|
content: typing.Optional[str] = None,
|
|
*,
|
|
emoji: typing.Optional[typing.Union[discord.PartialEmoji, discord.Emoji]] = None,
|
|
) -> None:
|
|
if emoji is not None:
|
|
self.emoji = emoji
|
|
else:
|
|
emoji = self.emoji
|
|
self.content = content
|
|
await self.view._update()
|
|
|
|
def get_truncated_content(self, length: typing.Optional[int] = None) -> str:
|
|
if length is None:
|
|
trunc = self.content.split("\n")[0]
|
|
else:
|
|
trunc = self.content[:length]
|
|
return trunc + (" ..." if len(self.content) > len(trunc) else "")
|
|
|
|
|
|
class ToolsMenu(discord.ui.Select):
|
|
def __init__(
|
|
self,
|
|
view: discord.ui.View,
|
|
*,
|
|
options: typing.Optional[typing.List[discord.SelectOption]] = None,
|
|
) -> None:
|
|
self.tool_list: typing.List[Tool] = [
|
|
BrushTool(view),
|
|
EraseTool(view),
|
|
EyedropperTool(view),
|
|
FillTool(view),
|
|
ReplaceTool(view),
|
|
DarkenTool(view),
|
|
LightenTool(view),
|
|
InverseTool(view),
|
|
]
|
|
default_options: typing.List[discord.SelectOption] = [
|
|
discord.SelectOption(
|
|
label=tool.name,
|
|
emoji=tool.emoji,
|
|
value=tool.name.lower(),
|
|
description=f"{tool.description}{' (Used automatically)' if tool.auto_use is True else ''}",
|
|
)
|
|
for tool in self.tool_list
|
|
]
|
|
options = options or default_options
|
|
self.END_INDEX = len(default_options) # The ending index of default options.
|
|
super().__init__(
|
|
placeholder="🖌️ Tools",
|
|
max_values=1,
|
|
options=options,
|
|
)
|
|
self._view: discord.ui.View = view
|
|
|
|
@property
|
|
def tools(self) -> typing.Dict[str, Tool]:
|
|
return {tool.name.lower(): tool for tool in self.tool_list}
|
|
|
|
@property
|
|
def value_to_option_dict(self) -> typing.Dict[str, discord.SelectOption]:
|
|
return {option.value: option for option in self.options}
|
|
|
|
def value_to_option(
|
|
self, value: typing.Union[str, int]
|
|
) -> typing.Union[None, discord.SelectOption]:
|
|
return self.value_to_option_dict.get(value)
|
|
|
|
def set_default(self, def_option: discord.SelectOption):
|
|
for option in self.options:
|
|
option.default = False
|
|
def_option.default = True
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
await interaction.response.defer()
|
|
value = self.values[0]
|
|
tool = self.tools[value]
|
|
# If the tool selected is one of these, use it directly instead of equipping.
|
|
if tool.auto_use:
|
|
if await tool.use(
|
|
interaction=interaction
|
|
): # This is to decide whether or not to edit the message, depending on if the tool was used successfully.
|
|
await self.view._update()
|
|
# Else, equip the tool (to the primary tool button slot).
|
|
else:
|
|
self.view.primary_tool = tool
|
|
self.view.load_items()
|
|
self.set_default(self.value_to_option(value))
|
|
await self.view._update()
|
|
|
|
|
|
class ColorsMenu(discord.ui.Select):
|
|
def __init__(
|
|
self,
|
|
view: discord.ui.View,
|
|
*,
|
|
options: typing.Optional[typing.List[discord.SelectOption]] = None,
|
|
background: str,
|
|
) -> None:
|
|
default_options: typing.List[discord.SelectOption] = [
|
|
*base_colors_options(),
|
|
discord.SelectOption(
|
|
label="Add Color(s)",
|
|
emoji=ADD_COLORS_EMOJI,
|
|
value="color",
|
|
),
|
|
discord.SelectOption(
|
|
label="Add Emoji(s)",
|
|
emoji=ADD_EMOJIS_EMOJI,
|
|
value="emoji",
|
|
),
|
|
discord.SelectOption(label="Mix Colors", emoji=MIX_COLORS_EMOJI, value="mix"),
|
|
]
|
|
options = options or default_options
|
|
self.END_INDEX = len(default_options) # The ending index of default options
|
|
for option in options:
|
|
if str(option.emoji) == background and not option.label.endswith(" (bg)"):
|
|
option.label += " (bg)"
|
|
super().__init__(
|
|
placeholder="🎨 Palette",
|
|
options=options,
|
|
)
|
|
self._view: discord.ui.View = view
|
|
self.bot: Red = self._view.cog.bot
|
|
self.board: Board = self._view.board
|
|
|
|
@property
|
|
def value_to_option_dict(self) -> typing.Dict[str, discord.SelectOption]:
|
|
return {option.value: option for option in self.options}
|
|
|
|
def value_to_option(
|
|
self, value: typing.Union[str, int]
|
|
) -> typing.Union[None, discord.SelectOption]:
|
|
return self.value_to_option_dict.get(value)
|
|
|
|
def emoji_to_option(
|
|
self, emoji: typing.Union[discord.Emoji, discord.PartialEmoji, Color]
|
|
) -> typing.Union[None, discord.SelectOption]:
|
|
if isinstance(emoji, discord.Emoji):
|
|
identifier = emoji.id
|
|
elif isinstance(emoji, discord.PartialEmoji):
|
|
identifier = emoji.name if emoji.is_unicode_emoji() else emoji.id
|
|
else:
|
|
identifier = f"#{emoji.hex}"
|
|
return self.value_to_option_dict.get(str(identifier))
|
|
|
|
def append_option(
|
|
self, option: discord.SelectOption
|
|
) -> typing.Tuple[bool, typing.Union[discord.SelectOption, None]]:
|
|
if (found_option := self.value_to_option(option.value)) is not None:
|
|
return False, found_option
|
|
replaced_option = None
|
|
if len(self.options) == 25:
|
|
replaced_option = self.options.pop(self.END_INDEX)
|
|
replaced_option.emoji.name = replaced_option.label
|
|
super().append_option(option)
|
|
return replaced_option is not None, replaced_option
|
|
|
|
def set_default(self, def_option: discord.SelectOption) -> None:
|
|
for option in self.options:
|
|
option.default = False
|
|
def_option.default = True
|
|
|
|
async def append_sent_emojis(
|
|
self, sent_emojis: typing.List[typing.Union[discord.Emoji, discord.PartialEmoji, Color]]
|
|
) -> typing.Dict[typing.Union[discord.Emoji, discord.PartialEmoji, Color], str]:
|
|
added_emojis = {
|
|
sent_emoji: "Already exists." if self.emoji_to_option(sent_emoji) else "Added."
|
|
for sent_emoji in sent_emojis
|
|
}
|
|
replaced_emojis = {}
|
|
for added_emoji, status in added_emojis.items():
|
|
if status != "Added.":
|
|
continue
|
|
if isinstance(added_emoji, discord.Emoji) or (
|
|
isinstance(added_emoji, discord.PartialEmoji) and added_emoji.is_custom_emoji()
|
|
):
|
|
name = f"{added_emoji.name} ({added_emoji.id})"
|
|
emoji = added_emoji
|
|
value = str(added_emoji.id)
|
|
elif (
|
|
isinstance(added_emoji, discord.PartialEmoji) and not added_emoji.is_custom_emoji()
|
|
):
|
|
name = added_emoji.name
|
|
emoji = added_emoji
|
|
value = added_emoji.name
|
|
elif isinstance(added_emoji, Color):
|
|
name = (await added_emoji.get_name()) + f" ({added_emoji.hex})"
|
|
emoji = None
|
|
value = f"#{added_emoji.hex}"
|
|
else:
|
|
continue
|
|
option = discord.SelectOption(
|
|
label=name,
|
|
emoji=emoji,
|
|
value=value,
|
|
)
|
|
replaced, returned_option = self.append_option(option)
|
|
if replaced:
|
|
replaced_emoji = returned_option.value
|
|
replaced_emojis[added_emoji] = replaced_emoji
|
|
for added_emoji, replaced_emoji in replaced_emojis.items():
|
|
added_emojis[added_emoji] = f"Added (replaced {replaced_emoji})."
|
|
return added_emojis
|
|
|
|
async def added_emojis_respond(
|
|
self,
|
|
added_emojis: typing.Dict[typing.Union[discord.Emoji, discord.PartialEmoji, Color], str],
|
|
*,
|
|
notification: Notification,
|
|
interaction: discord.Interaction,
|
|
) -> None:
|
|
if not added_emojis:
|
|
return await notification.edit("Aborted.")
|
|
response = [f"{added_emoji} - {status}" for added_emoji, status in added_emojis.items()]
|
|
if any("Added." in status for status in added_emojis.values()):
|
|
value = self.options[-1].value
|
|
if value.startswith("#"):
|
|
value = Color.from_hex(value[1:])
|
|
self.board.cursor = value
|
|
self.set_default(self.options[-1])
|
|
response = "\n".join(response)
|
|
await notification.edit(f"{response}..." if len(response) > 2500 else response)
|
|
await self.view._update()
|
|
|
|
def extract_emojis(
|
|
self, content: str
|
|
) -> typing.List[typing.Union[discord.PartialEmoji, Color]]:
|
|
# Get any unicode emojis from the content and list them as SentEmoji objects.
|
|
unicode_emojis = [
|
|
discord.PartialEmoji.from_str(emoji) for emoji in _emoji.distinct_emoji_list(content)
|
|
]
|
|
# Get any flag/regional indicator emojis from the content and list them as SentEmoji objects.
|
|
FLAG_EMOJI_REGEX = re.compile("[\U0001F1E6-\U0001F1FF]")
|
|
flag_emojis = [
|
|
discord.PartialEmoji.from_str(emoji.group(0))
|
|
for emoji in FLAG_EMOJI_REGEX.finditer(content)
|
|
]
|
|
# Get any custom emojis from the content and list them as SentEmoji objects.
|
|
CUSTOM_EMOJI_REGEX = re.compile("<a?:[a-zA-Z0-9_]+:\\d+>")
|
|
custom_emojis = [
|
|
discord.PartialEmoji.from_str(emoji.group(0))
|
|
for emoji in CUSTOM_EMOJI_REGEX.finditer(content)
|
|
]
|
|
return unicode_emojis + flag_emojis + custom_emojis
|
|
|
|
async def callback(self, interaction: discord.Interaction) -> None:
|
|
await interaction.response.defer()
|
|
|
|
# Set max values to 1 everytime the menu is used.
|
|
initial_max_values = self.max_values
|
|
self.max_values = 1
|
|
|
|
# If the "Add Color(s)" option was selected. Always takes first priority.
|
|
if "color" in self.values:
|
|
|
|
def check(m):
|
|
return m.author == interaction.user and m.channel == interaction.channel
|
|
|
|
notification = await self.view.create_notification(
|
|
_(
|
|
"Please type all the colors you want to add. They can be either or all of:"
|
|
"\n• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,"
|
|
"\n• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**"
|
|
"\n• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)"
|
|
"\n• Any image file (first 5 abundant colors will be extracted)."
|
|
),
|
|
interaction=interaction,
|
|
)
|
|
try:
|
|
msg = await self.bot.wait_for("message", timeout=30, check=check)
|
|
except asyncio.TimeoutError:
|
|
await notification.edit("Timed out, aborted.")
|
|
return
|
|
await CogsUtils.delete_message(msg)
|
|
|
|
CHANNEL = "[a-fA-F0-9]{2}"
|
|
HEX_REGEX = re.compile(
|
|
rf"\b(?P<red>{CHANNEL})(?P<green>{CHANNEL})(?P<blue>{CHANNEL})(?P<alpha>{CHANNEL})?\b"
|
|
)
|
|
ZERO_TO_255 = "0*25[0-5]|0*2[0-4][0-9]|0*1[0-9]{2}|0*[1-9][0-9]|0*[0-9]"
|
|
RGB_A_REGEX = re.compile(
|
|
rf"\((?P<red>{ZERO_TO_255}) *,? +(?P<green>{ZERO_TO_255}) *,? +(?P<blue>{ZERO_TO_255})(?: *,? +(?P<alpha>{ZERO_TO_255}))?\)"
|
|
)
|
|
|
|
content = msg.content.lower().strip()
|
|
|
|
# Get any hex codes from the content
|
|
hex_matches = list(HEX_REGEX.finditer(content))
|
|
# Get any RGB/A values from the content
|
|
rgb_a_matches = list(RGB_A_REGEX.finditer(content))
|
|
total_matches = hex_matches + rgb_a_matches
|
|
# Organize all the matches into SentEmoji objects.
|
|
sent_emojis = []
|
|
for match in total_matches:
|
|
base = 16 if match in hex_matches else 10
|
|
red = int(match.group("red"), base)
|
|
green = int(match.group("green"), base)
|
|
blue = int(match.group("blue"), base)
|
|
alpha = int(
|
|
match.group("alpha") or ("ff" if match in hex_matches else "255"),
|
|
base,
|
|
)
|
|
color = Color((red, green, blue, alpha))
|
|
sent_emojis.append(color)
|
|
|
|
# Extract from emoji.
|
|
emoji_matches = self.extract_emojis(content)
|
|
for match in emoji_matches:
|
|
color = await Color.from_emoji(cog=self._view.cog, emoji=match)
|
|
sent_emojis.append(color)
|
|
|
|
# Extract from first attachment.
|
|
if msg.attachments:
|
|
attachment_colors = await Color.from_attachment(msg.attachments[0])
|
|
for color in attachment_colors:
|
|
sent_emojis.append(color)
|
|
|
|
added_emojis = await self.append_sent_emojis(sent_emojis)
|
|
await self.added_emojis_respond(
|
|
added_emojis, notification=notification, interaction=interaction
|
|
)
|
|
|
|
# First it checks if the "Add Emoji(s)" option was selected. Takes second priority.
|
|
elif "emoji" in self.values:
|
|
|
|
def check(m):
|
|
return m.author == interaction.user and m.channel == interaction.channel
|
|
|
|
notification = await self.view.create_notification(
|
|
_(
|
|
"Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`."
|
|
),
|
|
interaction=interaction,
|
|
)
|
|
try:
|
|
msg = await self.bot.wait_for("message", timeout=30, check=check)
|
|
except asyncio.TimeoutError:
|
|
await notification.edit("Timed out, aborted.")
|
|
return
|
|
await CogsUtils.delete_message(msg)
|
|
|
|
content = msg.content.strip()
|
|
sent_emojis = self.extract_emojis(content)
|
|
added_emojis = await self.append_sent_emojis(sent_emojis)
|
|
await self.added_emojis_respond(
|
|
added_emojis, notification=notification, interaction=interaction
|
|
)
|
|
|
|
# If user has chosen to "Mix Colors".
|
|
elif "mix" in self.values:
|
|
if initial_max_values > 1:
|
|
self.max_values = 1
|
|
await self.view.create_notification(
|
|
f"Mixing disabled.",
|
|
emoji="🔀",
|
|
interaction=interaction,
|
|
)
|
|
else:
|
|
self.max_values = len(self.options)
|
|
await self.view.create_notification(
|
|
f"Mixing enabled. You can now select multiple colors/emojis to mix their primary colors.",
|
|
emoji="🔀",
|
|
interaction=interaction,
|
|
)
|
|
|
|
# If multiple options were selected.
|
|
elif len(self.values) > 1:
|
|
selected_options = [self.value_to_option(value) for value in self.values]
|
|
selected_colors = [
|
|
str(option.value)
|
|
for option in selected_options
|
|
if option.value.startswith("#") or option.value in MAIN_COLORS_DICT
|
|
]
|
|
notification = await self.view.create_notification(
|
|
f"Mixing colors {humanize_list(selected_colors)}...",
|
|
emoji="🔀",
|
|
interaction=interaction,
|
|
)
|
|
colors = [
|
|
MAIN_COLORS_DICT.get(str(color)) or (Color.from_hex(color[1:]))
|
|
for color in selected_colors
|
|
]
|
|
mixed_color = Color.mix_colors(colors)
|
|
label = (await mixed_color.get_name()) + f" (#{mixed_color.hex})"
|
|
option = discord.SelectOption(
|
|
label=label,
|
|
value=f"#{mixed_color.hex}",
|
|
)
|
|
replaced, returned_option = self.append_option(option)
|
|
self.board.cursor = mixed_color
|
|
self.set_default(option)
|
|
await notification.edit(
|
|
f"Mixed colors:\n{' + '.join(selected_colors)} = {label}"
|
|
+ (f" (replaced {returned_option.emoji})." if replaced else "")
|
|
)
|
|
await self.view._update()
|
|
|
|
# If only one option was selected.
|
|
elif self.board.cursor != (value := self.values[0]):
|
|
if value.startswith("#"):
|
|
value = Color.from_hex(value[1:])
|
|
self.board.cursor = value
|
|
self.set_default(self.value_to_option(value))
|
|
await self.view._update()
|
|
|
|
|
|
class DrawView(discord.ui.View):
|
|
def __init__(
|
|
self,
|
|
cog: commands.Cog,
|
|
board: Board,
|
|
tool_options: typing.Optional[typing.List[discord.SelectOption]] = None,
|
|
color_options: typing.Optional[typing.List[discord.SelectOption]] = None,
|
|
) -> None:
|
|
super().__init__(timeout=600)
|
|
self.cog: commands.Cog = cog
|
|
self.ctx: commands.Context = None
|
|
|
|
self.board: Board = board
|
|
|
|
self.tool_menu: ToolsMenu = ToolsMenu(self, options=tool_options)
|
|
self.color_menu: ColorsMenu = ColorsMenu(
|
|
self, options=color_options, background=board.background
|
|
)
|
|
self.primary_tool: Tool = self.tool_menu.tools["brush"]
|
|
|
|
self.auto: bool = False
|
|
self.select: bool = False
|
|
self.disabled: bool = False
|
|
self.secondary_page: bool = False
|
|
|
|
self.lock: asyncio.Lock = asyncio.Lock()
|
|
self.notifications: typing.List[Notification] = [Notification(view=self)]
|
|
|
|
self._message: discord.Message = None
|
|
|
|
self._ready: asyncio.Event = asyncio.Event()
|
|
|
|
async def start(
|
|
self, ctx: commands.Context, message: typing.Optional[discord.Message] = None
|
|
) -> discord.Message:
|
|
self.ctx: commands.Context = ctx
|
|
self._message = message
|
|
await self._update()
|
|
await self._ready.wait()
|
|
return self._message
|
|
|
|
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
|
if interaction.user.id not in [self.ctx.author.id] + list(self.ctx.bot.owner_ids):
|
|
await interaction.response.send_message(
|
|
_("You are not allowed to use this interaction."), ephemeral=True
|
|
)
|
|
return False
|
|
return True
|
|
|
|
async def on_timeout(self) -> None:
|
|
self.board.clear_cursors(empty=True)
|
|
for child in self.children:
|
|
child: discord.ui.Item
|
|
if hasattr(child, "disabled") and not (
|
|
isinstance(child, discord.ui.Button) and child.style == discord.ButtonStyle.url
|
|
):
|
|
child.disabled = True
|
|
try:
|
|
await self._update(empty=True)
|
|
except discord.HTTPException:
|
|
pass
|
|
self._ready.set()
|
|
|
|
async def _update(self, empty: bool = False) -> None:
|
|
self._embed: discord.Embed = await self.get_embed(self.ctx)
|
|
file = await self.board.to_file()
|
|
if not empty:
|
|
self.load_items()
|
|
if self._message is None:
|
|
self._message: discord.Message = await self.ctx.send(
|
|
embed=self._embed,
|
|
file=file,
|
|
view=self,
|
|
)
|
|
self.cog.views[self._message] = self
|
|
else:
|
|
self._message: discord.Message = await self._message.edit(
|
|
content=None,
|
|
embed=self._embed,
|
|
attachments=[file],
|
|
view=self,
|
|
)
|
|
|
|
async def get_embed(self, ctx: commands.Context) -> discord.Embed:
|
|
embed: discord.Embed = discord.Embed(title="Draw Board", color=await ctx.embed_color())
|
|
# embed.description = str(self.board)
|
|
embed.set_image(url=f"attachment://image.{IMAGE_EXTENSION.lower()}")
|
|
# This section adds the notification field only if any one
|
|
# of the notifications is not empty. In such a case, it only
|
|
# shows the notification(s) that is not empty
|
|
if any((len(n.content) != 0 for n in self.notifications)):
|
|
embed.add_field(
|
|
name="Notifications",
|
|
value="\n\n".join(
|
|
[
|
|
(
|
|
(
|
|
f"{str(n.emoji)} "
|
|
+ (n.content if idx == 0 else n.get_truncated_content()).replace(
|
|
"\n", "\n> "
|
|
)
|
|
) # Put each notification into seperate quotes
|
|
if len(n.content) != 0
|
|
else ""
|
|
) # Show only non-empty notifications
|
|
for idx, n in enumerate(self.notifications)
|
|
]
|
|
),
|
|
)
|
|
# The embed footer.
|
|
embed.set_footer(
|
|
text=(
|
|
f"The board looks wack? Try decreasing its size! Do {self.ctx.clean_prefix}help draw for more info."
|
|
if any((len(self.board.row_labels) >= 10, len(self.board.col_labels) >= 10))
|
|
else f"You can customize this board! Do {self.ctx.clean_prefix}help draw for more info."
|
|
)
|
|
)
|
|
return embed
|
|
|
|
async def create_notification(
|
|
self,
|
|
content: typing.Optional[str] = None,
|
|
*,
|
|
emoji: typing.Optional[
|
|
typing.Union[discord.PartialEmoji, discord.Emoji]
|
|
] = discord.PartialEmoji.from_str("🔔"),
|
|
interaction: typing.Optional[discord.Interaction] = None,
|
|
) -> Notification:
|
|
self.notifications = self.notifications[:2]
|
|
notification = Notification(content, emoji=emoji, view=self)
|
|
self.notifications.insert(0, notification)
|
|
if interaction is not None:
|
|
await self._update()
|
|
return notification
|
|
|
|
@property
|
|
def placeholder_button(self) -> discord.ui.Button:
|
|
button = discord.ui.Button(
|
|
label="\u200b",
|
|
style=discord.ButtonStyle.gray,
|
|
custom_id=str(len(self.children)),
|
|
)
|
|
button.callback = lambda interaction: interaction.response.defer()
|
|
return button
|
|
|
|
def load_items(self) -> None:
|
|
self.clear_items()
|
|
self.add_item(self.tool_menu)
|
|
self.add_item(self.color_menu)
|
|
# This is necessary for "paginating" the view and different buttons.
|
|
if self.secondary_page is False:
|
|
self.add_item(self.undo)
|
|
self.add_item(self.up_left)
|
|
self.add_item(self.up)
|
|
self.add_item(self.up_right)
|
|
self.add_item(self.secondary_page_button)
|
|
|
|
self.add_item(self.redo)
|
|
self.add_item(self.left)
|
|
self.add_item(self.set_cursor)
|
|
self.add_item(self.right)
|
|
# self.add_item(self.placeholder_button)
|
|
self.add_item(self.set_auto_draw)
|
|
|
|
self.add_item(self.primary_tool)
|
|
self.add_item(self.down_left)
|
|
self.add_item(self.down)
|
|
self.add_item(self.down_right)
|
|
# self.add_item(self.placeholder_button)
|
|
self.add_item(self.select_area)
|
|
elif self.secondary_page is True:
|
|
self.add_item(self.stop_button)
|
|
self.add_item(self.up_left)
|
|
self.add_item(self.up)
|
|
self.add_item(self.up_right)
|
|
self.add_item(self.secondary_page_button)
|
|
|
|
self.add_item(self.clear)
|
|
self.add_item(self.left)
|
|
self.add_item(self.set_cursor)
|
|
self.add_item(self.right)
|
|
# self.add_item(self.placeholder_button)
|
|
self.add_item(self.raw_paint)
|
|
|
|
self.add_item(self.primary_tool)
|
|
self.add_item(self.down_left)
|
|
self.add_item(self.down)
|
|
self.add_item(self.down_right)
|
|
# self.add_item(self.placeholder_button)
|
|
self.add_item(self.set_cursor_display)
|
|
self.update_buttons()
|
|
|
|
def update_buttons(self) -> None:
|
|
self.secondary_page_button.style = (
|
|
discord.ButtonStyle.success if self.secondary_page else discord.ButtonStyle.secondary
|
|
)
|
|
self.undo.disabled = self.board.board_index == 0 or self.disabled
|
|
self.undo.label = f"{self.board.board_index} ↶"
|
|
self.redo.disabled = (
|
|
self.board.board_index == len(self.board.board_history) - 1
|
|
) or self.disabled
|
|
self.redo.label = f"↷ {(len(self.board.board_history) - 1) - self.board.board_index}"
|
|
|
|
async def move_cursor(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
row_move: typing.Optional[int] = 0,
|
|
col_move: typing.Optional[int] = 0,
|
|
) -> None:
|
|
self.board.move_cursor(row_move, col_move, self.select)
|
|
if self.auto:
|
|
await self.primary_tool.use(interaction=interaction)
|
|
await self._update()
|
|
|
|
# Buttons
|
|
|
|
# 1st row
|
|
@discord.ui.button(label="↶", style=discord.ButtonStyle.secondary)
|
|
async def undo(self, interaction: discord.Interaction, button: discord.Button) -> None:
|
|
await interaction.response.defer()
|
|
if self.board.board_index > 0:
|
|
self.board.board_index -= 1
|
|
await self._update()
|
|
|
|
@discord.ui.button(style=discord.ButtonStyle.danger, emoji="✖️", custom_id="close_page")
|
|
async def stop_button(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
) -> None:
|
|
try:
|
|
await interaction.response.defer()
|
|
except discord.errors.NotFound:
|
|
pass
|
|
self.stop()
|
|
await CogsUtils.delete_message(self._message)
|
|
self._ready.set()
|
|
|
|
@discord.ui.button(emoji=UP_LEFT_EMOJI, style=discord.ButtonStyle.primary)
|
|
async def up_left(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
await interaction.response.defer()
|
|
row_move = -1
|
|
col_move = -1
|
|
await self.move_cursor(interaction, row_move=row_move, col_move=col_move)
|
|
|
|
@discord.ui.button(emoji=UP_EMOJI, style=discord.ButtonStyle.primary)
|
|
async def up(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
await interaction.response.defer()
|
|
row_move = -1
|
|
col_move = 0
|
|
await self.move_cursor(interaction, row_move=row_move, col_move=col_move)
|
|
|
|
@discord.ui.button(emoji=UP_RIGHT_EMOJI, style=discord.ButtonStyle.primary)
|
|
async def up_right(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
await interaction.response.defer()
|
|
row_move = -1
|
|
col_move = 1
|
|
await self.move_cursor(interaction, row_move=row_move, col_move=col_move)
|
|
|
|
@discord.ui.button(label="2nd", style=discord.ButtonStyle.secondary)
|
|
async def secondary_page_button(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
) -> None:
|
|
await interaction.response.defer()
|
|
self.secondary_page = not self.secondary_page
|
|
|
|
self.load_items()
|
|
await self._update()
|
|
|
|
# 2nd Row
|
|
@discord.ui.button(label="↷", style=discord.ButtonStyle.secondary)
|
|
async def redo(self, interaction: discord.Interaction, button: discord.Button) -> None:
|
|
await interaction.response.defer()
|
|
if self.board.board_index < len(self.board.board_history) - 1:
|
|
self.board.board_index += 1
|
|
await self._update()
|
|
|
|
@discord.ui.button(label="Clear", style=discord.ButtonStyle.danger)
|
|
async def clear(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
await interaction.response.defer()
|
|
self.secondary_page = False
|
|
self.auto = False
|
|
self.select = False
|
|
self.board.clear()
|
|
self.load_items()
|
|
await self._update()
|
|
|
|
@discord.ui.button(emoji=LEFT_EMOJI, style=discord.ButtonStyle.primary)
|
|
async def left(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
await interaction.response.defer()
|
|
row_move = 0
|
|
col_move = -1
|
|
await self.move_cursor(interaction, row_move=row_move, col_move=col_move)
|
|
|
|
@discord.ui.button(emoji=RIGHT_EMOJI, style=discord.ButtonStyle.primary)
|
|
async def right(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
await interaction.response.defer()
|
|
row_move = 0
|
|
col_move = 1
|
|
await self.move_cursor(interaction, row_move=row_move, col_move=col_move)
|
|
|
|
# 3rd / Last Row
|
|
@discord.ui.button(emoji=DOWN_LEFT_EMOJI, style=discord.ButtonStyle.primary)
|
|
async def down_left(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
await interaction.response.defer()
|
|
row_move = 1
|
|
col_move = -1
|
|
await self.move_cursor(interaction, row_move=row_move, col_move=col_move)
|
|
|
|
@discord.ui.button(emoji=DOWN_EMOJI, style=discord.ButtonStyle.primary)
|
|
async def down(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
await interaction.response.defer()
|
|
row_move = 1
|
|
col_move = 0
|
|
await self.move_cursor(interaction, row_move=row_move, col_move=col_move)
|
|
|
|
@discord.ui.button(emoji=DOWN_RIGHT_EMOJI, style=discord.ButtonStyle.primary)
|
|
async def down_right(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
) -> None:
|
|
await interaction.response.defer()
|
|
row_move = 1
|
|
col_move = 1
|
|
await self.move_cursor(interaction, row_move=row_move, col_move=col_move)
|
|
|
|
@discord.ui.button(emoji=SET_CURSOR_EMOJI, style=discord.ButtonStyle.secondary)
|
|
async def set_cursor(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
) -> None:
|
|
await interaction.response.defer()
|
|
|
|
def check(m):
|
|
return m.author == interaction.user and m.channel == interaction.channel
|
|
|
|
notification = await self.create_notification(
|
|
_(
|
|
"Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc."
|
|
),
|
|
emoji="🔠",
|
|
interaction=interaction,
|
|
)
|
|
try:
|
|
msg = await self.ctx.bot.wait_for("message", timeout=30, check=check)
|
|
except asyncio.TimeoutError:
|
|
await notification.edit("Timed out, aborted.")
|
|
return
|
|
await CogsUtils.delete_message(msg)
|
|
|
|
cell: str = msg.content.upper()
|
|
ABC = ALPHABETS[: self.board.cursor_row_max + 1]
|
|
NUM = NUMBERS[: self.board.cursor_col_max + 1]
|
|
end_row_key = None
|
|
end_col_key = None
|
|
|
|
CELL_REGEX = (
|
|
f"^(?P<start_row>[A-{ABC[-1]}])(?P<start_col>[0-9]|(?:1[0-{NUM[-1] % 10}]))"
|
|
f"(-(?P<end_row>[A-{ABC[-1]}])(?P<end_col>[0-9]|(?:1[0-{NUM[-1] % 10}]))?)?$"
|
|
)
|
|
ROW_OR_COL_REGEX = (
|
|
f"(?:^(?P<row>[A-{ABC[-1]}])$)|(?:^(?P<col>[0-9]|(?:1[0-{NUM[-1] % 10}]))$)"
|
|
)
|
|
match = re.match(CELL_REGEX, cell)
|
|
if match is not None:
|
|
start_row_key = match["start_row"]
|
|
start_col_key = int(match["start_col"])
|
|
if match["end_row"] is not None:
|
|
end_row_key = match["end_row"]
|
|
end_col_key = (
|
|
int(match["end_col"]) if match["end_col"] is not None else start_col_key
|
|
)
|
|
else:
|
|
match = re.match(ROW_OR_COL_REGEX, cell)
|
|
if match is not None:
|
|
start_row_key = (
|
|
match["row"] if match["row"] is not None else ABC[self.board.cursor_row]
|
|
)
|
|
start_col_key = (
|
|
int(match["col"]) if match["col"] is not None else self.board.cursor_col
|
|
)
|
|
else:
|
|
return await notification.edit("Aborted.")
|
|
|
|
if (
|
|
start_row_key not in ABC
|
|
or start_col_key not in NUM
|
|
or (end_row_key is not None and end_row_key not in ABC)
|
|
or (end_col_key is not None and end_col_key not in NUM)
|
|
):
|
|
return await notification.edit("Aborted.")
|
|
|
|
if end_row_key is None and end_col_key is None:
|
|
row_move = LETTER_TO_NUMBER[start_row_key] - self.board.cursor_row
|
|
col_move = start_col_key - self.board.cursor_col
|
|
await notification.edit(
|
|
f"Moved cursor to **{cell}** ({LETTER_TO_NUMBER[start_row_key]}, {start_col_key}).",
|
|
)
|
|
await self.move_cursor(interaction, row_move=row_move, col_move=col_move)
|
|
else:
|
|
row_move = LETTER_TO_NUMBER[start_row_key] - self.board.cursor_row
|
|
col_move = start_col_key - self.board.cursor_col
|
|
self.board.move_cursor(row_move, col_move, self.select)
|
|
self.board.initial_coords = (
|
|
self.board.cursor_row,
|
|
self.board.cursor_col,
|
|
)
|
|
(
|
|
self.board.initial_row,
|
|
self.board.initial_col,
|
|
) = self.board.initial_coords
|
|
self.select = not self.select
|
|
row_move = LETTER_TO_NUMBER[end_row_key] - self.board.cursor_row
|
|
col_move = end_col_key - self.board.cursor_col
|
|
await notification.edit(
|
|
f"Moved cursor to select **{cell}** ({LETTER_TO_NUMBER[start_row_key]}, {start_col_key} | {LETTER_TO_NUMBER[end_row_key]}, {end_col_key}).",
|
|
)
|
|
await self.move_cursor(interaction, row_move=row_move, col_move=col_move)
|
|
|
|
@discord.ui.button(
|
|
label="Auto Draw", emoji=AUTO_DRAW_EMOJI, style=discord.ButtonStyle.secondary
|
|
)
|
|
async def set_auto_draw(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
) -> None:
|
|
await interaction.response.defer()
|
|
self.auto = not self.auto
|
|
state = "enabled" if self.auto else "disabled"
|
|
await self.create_notification(
|
|
f"Auto Draw {state}.",
|
|
emoji="🔄",
|
|
interaction=interaction,
|
|
)
|
|
|
|
@discord.ui.button(
|
|
label="Select an Area", emoji=SELECT_EMOJI, style=discord.ButtonStyle.secondary
|
|
)
|
|
async def select_area(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
) -> None:
|
|
await interaction.response.defer()
|
|
if self.select is False:
|
|
self.board.initial_coords = (
|
|
self.board.cursor_row,
|
|
self.board.cursor_col,
|
|
)
|
|
(
|
|
self.board.initial_row,
|
|
self.board.initial_col,
|
|
) = self.board.initial_coords
|
|
self.select = not self.select
|
|
elif self.select is True:
|
|
self.board.clear_cursors()
|
|
self.select = not self.select
|
|
await self._update()
|
|
|
|
@discord.ui.button(
|
|
label="Raw Paint", emoji=RAW_PAINT_EMOJI, style=discord.ButtonStyle.secondary
|
|
)
|
|
async def raw_paint(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
await interaction.response.defer()
|
|
|
|
def check(m):
|
|
return m.author == interaction.user and m.channel == interaction.channel
|
|
|
|
notification = await self.create_notification(
|
|
_(
|
|
'Please type the cells with the main colors names. One color for each line, separated from the cell by a ":". Example: `A1:red\nB7-C9:green`.'
|
|
),
|
|
emoji=RAW_PAINT_EMOJI,
|
|
interaction=interaction,
|
|
)
|
|
try:
|
|
msg = await self.ctx.bot.wait_for("message", timeout=30, check=check)
|
|
except asyncio.TimeoutError:
|
|
await notification.edit("Timed out, aborted.")
|
|
return
|
|
await CogsUtils.delete_message(msg)
|
|
|
|
for i, line in enumerate(msg.content.split("\n"), start=1):
|
|
if len(line.split(":")) != 2:
|
|
return await notification.edit(f"Aborted. No `:` in line {i}.")
|
|
cell: str = line.split(":")[0].strip().upper()
|
|
color: str = line.split(":")[1].strip().lower()
|
|
|
|
ABC = ALPHABETS[: self.board.cursor_row_max + 1]
|
|
NUM = NUMBERS[: self.board.cursor_col_max + 1]
|
|
end_row_key = None
|
|
end_col_key = None
|
|
|
|
CELL_REGEX = (
|
|
f"^(?P<start_row>[A-{ABC[-1]}])(?P<start_col>[0-9]|(?:1[0-{NUM[-1] % 10}]))"
|
|
f"(-(?P<end_row>[A-{ABC[-1]}])(?P<end_col>[0-9]|(?:1[0-{NUM[-1] % 10}]))?)?$"
|
|
)
|
|
ROW_OR_COL_REGEX = (
|
|
f"(?:^(?P<row>[A-{ABC[-1]}])$)|(?:^(?P<col>[0-9]|(?:1[0-{NUM[-1] % 10}]))$)"
|
|
)
|
|
match = re.match(CELL_REGEX, cell)
|
|
if match is not None:
|
|
start_row_key = match["start_row"]
|
|
start_col_key = int(match["start_col"])
|
|
if match["end_row"] is not None:
|
|
end_row_key = match["end_row"]
|
|
end_col_key = (
|
|
int(match["end_col"]) if match["end_col"] is not None else start_col_key
|
|
)
|
|
else:
|
|
match = re.match(ROW_OR_COL_REGEX, cell)
|
|
if match is not None:
|
|
start_row_key = (
|
|
match["row"] if match["row"] is not None else ABC[self.board.cursor_row]
|
|
)
|
|
start_col_key = (
|
|
int(match["col"]) if match["col"] is not None else self.board.cursor_col
|
|
)
|
|
else:
|
|
return await notification.edit(f"Aborted. No cell match in line {i}.")
|
|
|
|
if (
|
|
start_row_key not in ABC
|
|
or start_col_key not in NUM
|
|
or (end_row_key is not None and end_row_key not in ABC)
|
|
or (end_col_key is not None and end_col_key not in NUM)
|
|
):
|
|
return await notification.edit(
|
|
f"Aborted. Wrong letter/num for cell(s) in line {i}."
|
|
)
|
|
|
|
colors = {option.label.lower(): option.value for option in base_colors_options()}
|
|
if color not in colors:
|
|
return await notification.edit(f"Aborted. Invalid color in line {i}.")
|
|
color = colors[color]
|
|
|
|
if end_row_key is None and end_col_key is None:
|
|
self.board.draw(
|
|
color=color, coords=[(LETTER_TO_NUMBER[start_row_key], start_col_key)]
|
|
)
|
|
else:
|
|
self.board.draw(
|
|
color=color,
|
|
coords=[
|
|
(row, col)
|
|
for col in range(
|
|
min(start_col_key, end_col_key),
|
|
max(start_col_key, end_col_key) + 1,
|
|
)
|
|
for row in range(
|
|
min(LETTER_TO_NUMBER[start_row_key], LETTER_TO_NUMBER[end_row_key]),
|
|
max(LETTER_TO_NUMBER[start_row_key], LETTER_TO_NUMBER[end_row_key])
|
|
+ 1,
|
|
)
|
|
],
|
|
)
|
|
|
|
await notification.edit("Draw paint successful.")
|
|
|
|
@discord.ui.button(
|
|
label="Cursor Display", emoji=CURSOR_DISPLAY_EMOJI, style=discord.ButtonStyle.success
|
|
)
|
|
async def set_cursor_display(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
) -> None:
|
|
await interaction.response.defer()
|
|
self.board.cursor_display = not self.board.cursor_display
|
|
state = "enabled" if self.board.cursor_display else "disabled"
|
|
await self.create_notification(
|
|
f"Cursor Display {state}.",
|
|
emoji="📍",
|
|
interaction=interaction,
|
|
)
|