# Original source of reaction-based menu idea from
# https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py
#
# Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
import asyncio
import contextlib
import functools
from types import MappingProxyType
from typing import Callable, Dict, Iterable, List, Mapping, Optional, TypeVar, Union
import discord
from .. import commands
from .predicates import ReactionPredicate
from .views import SimpleMenu, _SimplePageSource
_T = TypeVar("_T")
_PageList = TypeVar("_PageList", List[str], List[discord.Embed])
_ReactableEmoji = Union[str, discord.Emoji]
_ControlCallable = Callable[[commands.Context, _PageList, discord.Message, int, float, str], _T]
_active_menus: Dict[int, SimpleMenu] = {}
class _GenericButton(discord.ui.Button):
def __init__(self, emoji: Union[str, discord.PartialEmoji], func):
super().__init__(
emoji=discord.PartialEmoji.from_str(emoji), style=discord.ButtonStyle.grey
)
self.func = func
async def callback(self, interaction: discord.Interaction):
ctx = self.view.ctx
pages = self.view.source.entries
controls = None
message = self.view.message
page = self.view.current_page
timeout = self.view.timeout
emoji = self.emoji
try:
await self.func(ctx, pages, controls, message, page, timeout, emoji)
except Exception:
pass
await interaction.response.defer()
[docs]async def next_page(
ctx: commands.Context,
pages: list,
controls: Mapping[str, _ControlCallable],
message: discord.Message,
page: int,
timeout: float,
emoji: str,
) -> _T:
"""
Function for showing next page which is suitable
for use in ``controls`` mapping that is passed to `menu()`.
"""
if page >= len(pages) - 1:
page = 0 # Loop around to the first item
else:
page = page + 1
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
[docs]async def prev_page(
ctx: commands.Context,
pages: list,
controls: Mapping[str, _ControlCallable],
message: discord.Message,
page: int,
timeout: float,
emoji: str,
) -> _T:
"""
Function for showing previous page which is suitable
for use in ``controls`` mapping that is passed to `menu()`.
"""
if page <= 0:
page = len(pages) - 1 # Loop around to the last item
else:
page = page - 1
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
[docs]def start_adding_reactions(
message: discord.Message, emojis: Iterable[_ReactableEmoji]
) -> asyncio.Task:
"""Start adding reactions to a message.
This is a non-blocking operation - calling this will schedule the
reactions being added, but the calling code will continue to
execute asynchronously. There is no need to await this function.
This is particularly useful if you wish to start waiting for a
reaction whilst the reactions are still being added - in fact,
this is exactly what `menu()` uses to do that.
Parameters
----------
message: discord.Message
The message to add reactions to.
emojis : Iterable[Union[str, discord.Emoji]]
The emojis to react to the message with.
Returns
-------
asyncio.Task
The task for the coroutine adding the reactions.
"""
async def task():
# The task should exit silently if the message is deleted
with contextlib.suppress(discord.NotFound):
for emoji in emojis:
await message.add_reaction(emoji)
return asyncio.create_task(task())
#: Default controls for `menu()` that contain controls for
#: previous page, closing menu, and next page.
DEFAULT_CONTROLS: Mapping[str, _ControlCallable] = MappingProxyType(
{
"\N{LEFTWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}": prev_page,
"\N{CROSS MARK}": close_menu,
"\N{BLACK RIGHTWARDS ARROW}\N{VARIATION SELECTOR-16}": next_page,
}
)