Add global buttons to base menus (#5683)

Co-authored-by: Kowlin <10947836+Kowlin@users.noreply.github.com>
This commit is contained in:
TrustyJAID 2022-10-12 14:13:33 -06:00 committed by GitHub
parent aaeb1b5daa
commit b0a3f00f41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 122 additions and 2 deletions

View File

@ -35,7 +35,7 @@ Embed Helpers
:members: :members:
:exclude-members: randomize_color :exclude-members: randomize_color
Reaction Menus Menus
============== ==============
.. automodule:: redbot.core.utils.menus .. automodule:: redbot.core.utils.menus

View File

@ -142,6 +142,7 @@ class Red(
schema_version=0, schema_version=0,
datarequests__allow_user_requests=True, datarequests__allow_user_requests=True,
datarequests__user_requests_are_strict=True, datarequests__user_requests_are_strict=True,
use_buttons=False,
) )
self._config.register_guild( self._config.register_guild(
@ -1351,6 +1352,17 @@ class Red(
global_setting = await self._config.embeds() global_setting = await self._config.embeds()
return global_setting return global_setting
async def use_buttons(self) -> bool:
"""
Determines whether the bot owner has enabled use of buttons instead of
reactions for basic menus.
Returns
-------
bool
"""
return await self._config.use_buttons()
async def is_owner(self, user: Union[discord.User, discord.Member], /) -> bool: async def is_owner(self, user: Union[discord.User, discord.Member], /) -> bool:
""" """
Determines if the user should be considered a bot owner. Determines if the user should be considered a bot owner.

View File

@ -3621,6 +3621,31 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
else: else:
await ctx.send(_("Server prefixes set.")) await ctx.send(_("Server prefixes set."))
@_set.command(name="usebuttons")
async def use_buttons(self, ctx: commands.Context, use_buttons: bool = None):
"""
Set a global bot variable for using buttons in menus.
When enabled, all usage of cores menus API will use buttons instead of reactions.
This defaults to False.
Using this without a setting will toggle.
**Examples:**
- `[p]set usebuttons True` - Enables using buttons.
- `[p]helpset usebuttons` - Toggles the value.
**Arguments:**
- `[use_buttons]` - Whether to use buttons. Leave blank to toggle.
"""
if use_buttons is None:
use_buttons = not await ctx.bot._config.use_buttons()
await ctx.bot._config.use_buttons.set(use_buttons)
if use_buttons:
await ctx.send(_("I will use buttons on basic menus."))
else:
await ctx.send(_("I will not use buttons on basic menus."))
@commands.group() @commands.group()
@checks.is_owner() @checks.is_owner()
async def helpset(self, ctx: commands.Context): async def helpset(self, ctx: commands.Context):

View File

@ -6,18 +6,43 @@ import asyncio
import contextlib import contextlib
import functools import functools
from types import MappingProxyType from types import MappingProxyType
from typing import Callable, Iterable, List, Mapping, Optional, TypeVar, Union from typing import Callable, Dict, Iterable, List, Mapping, Optional, TypeVar, Union
import discord import discord
from .. import commands from .. import commands
from .predicates import ReactionPredicate from .predicates import ReactionPredicate
from .views import SimpleMenu, _SimplePageSource
_T = TypeVar("_T") _T = TypeVar("_T")
_PageList = TypeVar("_PageList", List[str], List[discord.Embed]) _PageList = TypeVar("_PageList", List[str], List[discord.Embed])
_ReactableEmoji = Union[str, discord.Emoji] _ReactableEmoji = Union[str, discord.Emoji]
_ControlCallable = Callable[[commands.Context, _PageList, discord.Message, int, float, str], _T] _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()
async def menu( async def menu(
ctx: commands.Context, ctx: commands.Context,
@ -64,6 +89,18 @@ async def menu(
RuntimeError RuntimeError
If either of the notes above are violated If either of the notes above are violated
""" """
if message is not None and message.id in _active_menus:
# prevents the expected callback from going any further
# our custom button will always pass the message the view is
# attached to, allowing one to send multiple menus on the same
# context.
view = _active_menus[message.id]
if pages != view.source.entries:
view._source = _SimplePageSource(pages)
new_page = await view.get_page(page)
view.current_page = page
await view.message.edit(**new_page)
return
if not isinstance(pages[0], (discord.Embed, str)): if not isinstance(pages[0], (discord.Embed, str)):
raise RuntimeError("Pages must be of type discord.Embed or str") raise RuntimeError("Pages must be of type discord.Embed or str")
if not all(isinstance(x, discord.Embed) for x in pages) and not all( if not all(isinstance(x, discord.Embed) for x in pages) and not all(
@ -81,6 +118,52 @@ async def menu(
maybe_coro = value.func maybe_coro = value.func
if not asyncio.iscoroutinefunction(maybe_coro): if not asyncio.iscoroutinefunction(maybe_coro):
raise RuntimeError("Function must be a coroutine") raise RuntimeError("Function must be a coroutine")
if await ctx.bot.use_buttons() and message is None:
# Only send the button version if `message` is None
# This is because help deals with this menu in weird ways
# where the original message is already sent prior to starting.
# This is not normally the way we recommend sending this because
# internally we already include the emojis we expect.
if controls == DEFAULT_CONTROLS:
view = SimpleMenu(pages)
await view.start(ctx)
await view.wait()
return
else:
view = SimpleMenu(pages)
view.remove_item(view.last_button)
view.remove_item(view.first_button)
has_next = False
has_prev = False
has_close = False
to_add = {}
for emoji, func in controls.items():
if func == next_page:
has_next = True
if emoji != view.forward_button.emoji:
view.forward_button.emoji = discord.PartialEmoji.from_str(emoji)
elif func == prev_page:
has_prev = True
if emoji != view.backward_button.emoji:
view.backward_button.emoji = discord.PartialEmoji.from_str(emoji)
elif func == close_menu:
has_close = True
else:
to_add[emoji] = func
if not has_next:
view.remove_item(view.forward_button)
if not has_prev:
view.remove_item(view.backward_button)
if not has_close:
view.remove_item(view.stop_button)
for emoji, func in to_add.items():
view.add_item(_GenericButton(emoji, func))
await view.start(ctx)
_active_menus[view.message.id] = view
await view.wait()
del _active_menus[view.message.id]
return
current_page = pages[page] current_page = pages[page]
if not message: if not message: