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("") 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{CHANNEL})(?P{CHANNEL})(?P{CHANNEL})(?P{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{ZERO_TO_255}) *,? +(?P{ZERO_TO_255}) *,? +(?P{ZERO_TO_255})(?: *,? +(?P{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[A-{ABC[-1]}])(?P[0-9]|(?:1[0-{NUM[-1] % 10}]))" f"(-(?P[A-{ABC[-1]}])(?P[0-9]|(?:1[0-{NUM[-1] % 10}]))?)?$" ) ROW_OR_COL_REGEX = ( f"(?:^(?P[A-{ABC[-1]}])$)|(?:^(?P[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[A-{ABC[-1]}])(?P[0-9]|(?:1[0-{NUM[-1] % 10}]))" f"(-(?P[A-{ABC[-1]}])(?P[0-9]|(?:1[0-{NUM[-1] % 10}]))?)?$" ) ROW_OR_COL_REGEX = ( f"(?:^(?P[A-{ABC[-1]}])$)|(?:^(?P[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, )