Make controls in menu() optional (#5678)

* Make `controls` in `menu()` optional

You might wonder, shouldn't we pass `None` to functions from controls?
No, we shouldn't because when `None` is passed, only DEFAULT_CONTROLS
can be used and that means that the length of pages list won't change.

* Update usage in core and core cogs

* Add missing docstrings to `redbot.core.utils.menus` module
This commit is contained in:
Jakub Kuczys 2022-04-16 21:29:12 +02:00 committed by GitHub
parent 955b40ac6d
commit 27bed5010f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 59 additions and 45 deletions

View File

@ -9,7 +9,7 @@ import discord
from redbot.core import Config, commands, checks
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box, pagify
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.utils.menus import menu
from redbot.core.bot import Red
from .alias_entry import AliasEntry, AliasCache, ArgParseError
@ -185,7 +185,7 @@ class Alias(commands.Cog):
if len(alias_list) == 1:
await ctx.send(alias_list[0])
return
await menu(ctx, alias_list, DEFAULT_CONTROLS)
await menu(ctx, alias_list)
@commands.group()
async def alias(self, ctx: commands.Context):

View File

@ -14,7 +14,7 @@ from redbot.core import bank, commands
from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator
from redbot.core.utils.chat_formatting import box, humanize_number
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu, start_adding_reactions
from redbot.core.utils.menus import menu, start_adding_reactions
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
from ...audio_dataclasses import LocalPath
@ -102,7 +102,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
discord.Embed(title=_("Global Whitelist"), description=page, colour=embed_colour)
for page in pages
)
await menu(ctx, pages, DEFAULT_CONTROLS)
await menu(ctx, pages)
@command_audioset_perms_global_whitelist.command(name="clear")
async def command_audioset_perms_global_whitelist_clear(self, ctx: commands.Context):
@ -196,7 +196,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
discord.Embed(title=_("Global Blacklist"), description=page, colour=embed_colour)
for page in pages
)
await menu(ctx, pages, DEFAULT_CONTROLS)
await menu(ctx, pages)
@command_audioset_perms_global_blacklist.command(name="clear")
async def command_audioset_perms_global_blacklist_clear(self, ctx: commands.Context):
@ -292,7 +292,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
discord.Embed(title=_("Whitelist"), description=page, colour=embed_colour)
for page in pages
)
await menu(ctx, pages, DEFAULT_CONTROLS)
await menu(ctx, pages)
@command_audioset_perms_whitelist.command(name="clear")
async def command_audioset_perms_whitelist_clear(self, ctx: commands.Context):
@ -385,7 +385,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
discord.Embed(title=_("Blacklist"), description=page, colour=embed_colour)
for page in pages
)
await menu(ctx, pages, DEFAULT_CONTROLS)
await menu(ctx, pages)
@command_audioset_perms_blacklist.command(name="clear")
async def command_audioset_perms_blacklist_clear(self, ctx: commands.Context):

View File

@ -10,7 +10,7 @@ from red_commons.logging import getLogger
from redbot.core import commands
from redbot.core.i18n import Translator
from redbot.core.utils.chat_formatting import box, humanize_number, pagify
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu, start_adding_reactions
from redbot.core.utils.menus import menu, start_adding_reactions
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
from ...equalizer import Equalizer
@ -141,7 +141,7 @@ class EqualizerCommands(MixinMeta, metaclass=CompositeMetaClass):
text=_("{num} preset(s)").format(num=humanize_number(len(list(eq_presets.keys()))))
)
page_list.append(embed)
await menu(ctx, page_list, DEFAULT_CONTROLS)
await menu(ctx, page_list)
@command_equalizer.command(name="load")
async def command_equalizer_load(self, ctx: commands.Context, eq_preset: str):

View File

@ -8,7 +8,7 @@ from red_commons.logging import getLogger
from redbot.core import commands
from redbot.core.i18n import Translator
from redbot.core.utils.menus import DEFAULT_CONTROLS, close_menu, menu, next_page, prev_page
from redbot.core.utils.menus import close_menu, menu, next_page, prev_page
from ...audio_dataclasses import LocalPath, Query
from ..abc import MixinMeta
@ -113,7 +113,7 @@ class LocalTrackCommands(MixinMeta, metaclass=CompositeMetaClass):
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
if dj_enabled and not await self._can_instaskip(ctx, ctx.author):
return await menu(ctx, folder_page_list, DEFAULT_CONTROLS)
return await menu(ctx, folder_page_list)
else:
await menu(ctx, folder_page_list, local_folder_controls)

View File

@ -12,7 +12,7 @@ from redbot.core import commands
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import humanize_number, pagify
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
from redbot.core.utils.menus import menu
from ..abc import MixinMeta
from ..cog_utils import CompositeMetaClass
@ -92,7 +92,7 @@ class MiscellaneousCommands(MixinMeta, metaclass=CompositeMetaClass):
pages += 1
servers_embed.append(em)
await menu(ctx, servers_embed, DEFAULT_CONTROLS)
await menu(ctx, servers_embed)
@commands.command(name="percent")
@commands.guild_only()

View File

@ -15,7 +15,7 @@ from redbot.core import commands
from redbot.core.commands import UserInputOptional
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core.utils.menus import DEFAULT_CONTROLS, close_menu, menu, next_page, prev_page
from redbot.core.utils.menus import close_menu, menu, next_page, prev_page
from ...audio_dataclasses import _PARTIALLY_SUPPORTED_MUSIC_EXT, Query
from ...errors import (
@ -930,6 +930,6 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
search_page_list.append(embed)
if dj_enabled and not can_skip:
return await menu(ctx, search_page_list, DEFAULT_CONTROLS)
return await menu(ctx, search_page_list)
await menu(ctx, search_page_list, search_controls)

View File

@ -19,7 +19,7 @@ from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import bold, pagify
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
from redbot.core.utils.menus import menu
from redbot.core.utils.predicates import MessagePredicate
from ...apis.api_utils import FakePlaylist
@ -899,7 +899,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
)
)
page_list.append(embed)
await menu(ctx, page_list, DEFAULT_CONTROLS)
await menu(ctx, page_list)
@commands.cooldown(1, 15, commands.BucketType.guild)
@command_playlist.command(name="list", usage="[args]", cooldown_after_parsing=True)
@ -1052,7 +1052,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
async for page_num in AsyncIter(range(1, len_playlist_list_pages + 1)):
embed = await self._build_playlist_list_page(ctx, page_num, abc_names, name)
playlist_embeds.append(embed)
await menu(ctx, playlist_embeds, DEFAULT_CONTROLS)
await menu(ctx, playlist_embeds)
@command_playlist.command(name="queue", usage="<name> [args]", cooldown_after_parsing=True)
@commands.cooldown(1, 300, commands.BucketType.member)
@ -1742,7 +1742,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
),
)
if embeds:
await menu(ctx, embeds, DEFAULT_CONTROLS)
await menu(ctx, embeds)
@command_playlist.command(name="upload", usage="[args]")
@commands.is_owner()

View File

@ -14,7 +14,6 @@ from redbot.core import commands
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core.utils.menus import (
DEFAULT_CONTROLS,
close_menu,
menu,
next_page,
@ -304,7 +303,7 @@ class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
async for page_num in AsyncIter(range(1, len_search_pages + 1)):
embed = await self._build_queue_search_page(ctx, page_num, search_list)
search_page_list.append(embed)
await menu(ctx, search_page_list, DEFAULT_CONTROLS)
await menu(ctx, search_page_list)
@command_queue.command(name="shuffle")
@commands.cooldown(1, 30, commands.BucketType.guild)

View File

@ -309,7 +309,7 @@ class CustomCommands(commands.Cog):
if len(msg) > 2000:
msg = f"{msg[:1997]}..."
msglist.append(msg)
await menus.menu(ctx, msglist, menus.DEFAULT_CONTROLS)
await menus.menu(ctx, msglist)
@customcom.command(name="search")
@commands.guild_only()
@ -572,11 +572,11 @@ class CustomCommands(commands.Cog):
)
embed.set_footer(text=_("Page {num}/{total}").format(num=idx, total=len(pages)))
embed_pages.append(embed)
await menus.menu(ctx, embed_pages, menus.DEFAULT_CONTROLS)
await menus.menu(ctx, embed_pages)
else:
content = "\n".join(map("{0[0]:<12} : {0[1]}".format, results))
pages = list(map(box, pagify(content, page_length=2000, shorten_by=10)))
await menus.menu(ctx, pages, menus.DEFAULT_CONTROLS)
await menus.menu(ctx, pages)
@customcom.command(name="show")
async def cc_show(self, ctx, command_name: str):

View File

@ -14,7 +14,7 @@ from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import box, humanize_number
from redbot.core.utils.menus import close_menu, menu, DEFAULT_CONTROLS
from redbot.core.utils.menus import close_menu, menu
from .converters import positive_int
T_ = Translator("Economy", __file__)
@ -516,11 +516,7 @@ class Economy(commands.Cog):
highscores.append(box(temp_msg, lang="md"))
if highscores:
await menu(
ctx,
highscores,
DEFAULT_CONTROLS if len(highscores) > 1 else {"\N{CROSS MARK}": close_menu},
)
await menu(ctx, highscores)
else:
await ctx.send(_("No balances found."))

View File

@ -9,7 +9,7 @@ import discord
from redbot.core import commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.utils.menus import menu
from redbot.core.utils.chat_formatting import (
bold,
escape,
@ -511,7 +511,6 @@ class General(commands.Cog):
await menu(
ctx,
pages=embeds,
controls=DEFAULT_CONTROLS,
message=None,
page=0,
timeout=30,
@ -537,7 +536,6 @@ class General(commands.Cog):
await menu(
ctx,
pages=messages,
controls=DEFAULT_CONTROLS,
message=None,
page=0,
timeout=30,

View File

@ -9,7 +9,7 @@ from redbot.core import checks, commands, modlog
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import bold, box, pagify
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
from redbot.core.utils.menus import menu
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("ModLog", __file__)
@ -84,7 +84,7 @@ class ModLog(commands.Cog):
)
rendered_cases.append(message)
await menu(ctx, rendered_cases, DEFAULT_CONTROLS)
await menu(ctx, rendered_cases)
@commands.command()
@commands.guild_only()
@ -119,7 +119,7 @@ class ModLog(commands.Cog):
)
for page in pagify(message, ["\n\n", "\n"], priority=True):
rendered_cases.append(page)
await menu(ctx, rendered_cases, DEFAULT_CONTROLS)
await menu(ctx, rendered_cases)
@commands.command()
@commands.guild_only()

View File

@ -19,7 +19,7 @@ from redbot.core.commands import UserInputOptional
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import warning, pagify
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.utils.menus import menu
_ = Translator("Warnings", __file__)
@ -324,7 +324,7 @@ class Warnings(commands.Cog):
).format(reason_name=r, **v)
)
if msg_list:
await menu(ctx, msg_list, DEFAULT_CONTROLS)
await menu(ctx, msg_list)
else:
await ctx.send(_("There are no reasons configured!"))
@ -359,7 +359,7 @@ class Warnings(commands.Cog):
).format(**r)
)
if msg_list:
await menu(ctx, msg_list, DEFAULT_CONTROLS)
await menu(ctx, msg_list)
else:
await ctx.send(_("There are no actions configured!"))

View File

@ -18,7 +18,7 @@ import pip
import traceback
from pathlib import Path
from redbot.core import data_manager
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.utils.menus import menu
from redbot.core.commands import GuildConverter, RawUserIdConverter
from string import ascii_letters, digits
from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Iterable, Sequence, Dict, Set
@ -1659,7 +1659,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
if len(pages) == 1:
await ctx.send(pages[0])
else:
await menu(ctx, pages, DEFAULT_CONTROLS)
await menu(ctx, pages)
@commands.command(require_var_positional=True)
@checks.is_owner()

View File

@ -6,7 +6,7 @@ import asyncio
import contextlib
import functools
from types import MappingProxyType
from typing import Callable, Iterable, List, Mapping, TypeVar, Union
from typing import Callable, Iterable, List, Mapping, Optional, TypeVar, Union
import discord
@ -22,7 +22,7 @@ _ControlCallable = Callable[[commands.Context, _PageList, discord.Message, int,
async def menu(
ctx: commands.Context,
pages: _PageList,
controls: Mapping[str, _ControlCallable],
controls: Optional[Mapping[str, _ControlCallable]] = None,
message: discord.Message = None,
page: int = 0,
timeout: float = 30.0,
@ -45,10 +45,12 @@ async def menu(
The command context
pages: `list` of `str` or `discord.Embed`
The pages of the menu.
controls: Mapping[str, Callable],
controls: Optional[Mapping[str, Callable]]
A mapping of emoji to the function which handles the action for the
emoji. The signature of the function should be the same as of this function
and should additionally accept an ``emoji`` parameter of type `str`.
If not passed, `DEFAULT_CONTROLS` is used *or*
only a close menu control is shown when ``pages`` is of length 1.
message: discord.Message
The message representing the menu. Usually :code:`None` when first opening
the menu
@ -68,6 +70,11 @@ async def menu(
isinstance(x, str) for x in pages
):
raise RuntimeError("All pages must be of the same type")
if controls is None:
if len(pages) == 1:
controls = {"\N{CROSS MARK}": close_menu}
else:
controls = DEFAULT_CONTROLS
for key, value in controls.items():
maybe_coro = value
if isinstance(value, functools.partial):
@ -144,6 +151,10 @@ async def next_page(
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:
@ -160,6 +171,10 @@ async def prev_page(
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:
@ -176,6 +191,10 @@ async def close_menu(
timeout: float,
emoji: str,
) -> None:
"""
Function for closing (deleting) menu which is suitable
for use in ``controls`` mapping that is passed to `menu()`.
"""
with contextlib.suppress(discord.NotFound):
await message.delete()
@ -191,7 +210,7 @@ def start_adding_reactions(
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.
this is exactly what `menu()` uses to do that.
Parameters
----------
@ -216,6 +235,8 @@ def start_adding_reactions(
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,