Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ext.menus pagination module #539

Merged
merged 57 commits into from
Dec 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
1c9c4fe
add back ext.menus
krittick Nov 17, 2021
9f33089
Merge branch 'Pycord-Development:master' into ext-menus
krittick Nov 17, 2021
e93ddf1
Merge branch 'master' into ext-menus
krittick Nov 24, 2021
62edad8
Merge branch 'master' into ext-menus
krittick Dec 3, 2021
450d2a6
almost a complete rework - needs more tweaking
krittick Dec 4, 2021
46217e7
minor formatting changes
krittick Dec 4, 2021
e851d6a
only show first/last buttons when not on a page directly adjacent to …
krittick Dec 4, 2021
8155142
add ability to append items from a custom view to the pagination view
krittick Dec 4, 2021
345a2b5
more formatting changes, change unnecessary usage of MISSING to None
krittick Dec 4, 2021
0b079f7
add optional page indicator
krittick Dec 4, 2021
8f590b7
fix weird refactor recommendation, thanks sourcery
krittick Dec 4, 2021
8c7d325
initial attempt to add a customize_button method
krittick Dec 4, 2021
22a297f
fix bug when custom views aren't specified
krittick Dec 4, 2021
4b139b3
make page indicator counts use a more human-friendly format (i.e. +1)
krittick Dec 4, 2021
e62f33c
change main class name, add some comments, etc
krittick Dec 5, 2021
9972b08
add goto_page method
krittick Dec 5, 2021
fea9dcb
more comments
krittick Dec 5, 2021
e54b039
add support for sending paginated messages from interactions
krittick Dec 5, 2021
97dab81
whoops
krittick Dec 5, 2021
f2c0183
add basic example
krittick Dec 6, 2021
64a1ea8
add additional example for custom view support
krittick Dec 6, 2021
b474686
consistent quote usage
krittick Dec 6, 2021
4deb7a1
Revert "consistent quote usage"
krittick Dec 6, 2021
f75dfda
Documenting ext.menus
VincentRPS Dec 7, 2021
e559486
Merge pull request #1 from RPSMain/ext-menus
krittick Dec 7, 2021
df5cde2
Fixing Menus Docs
VincentRPS Dec 7, 2021
4a9c149
Fix
VincentRPS Dec 7, 2021
5709ca6
Merge pull request #2 from RPSMain/ext-menus
krittick Dec 7, 2021
a973410
Merge branch 'master' into ext-menus
krittick Dec 7, 2021
0e6fc9a
Merge branch 'Pycord-Development:master' into ext-menus
krittick Dec 7, 2021
195ba20
docs updates
krittick Dec 7, 2021
9e014b4
more docstring updates
krittick Dec 7, 2021
9813fe0
more docstring updates
krittick Dec 7, 2021
12adb79
make return types for send/respond more consistent
krittick Dec 7, 2021
973e49e
Apply suggestions from code review
Lulalaby Dec 7, 2021
818be51
more docstrings, add attributes block for Paginator class
krittick Dec 7, 2021
61844cc
Merge remote-tracking branch 'origin/ext-menus' into ext-menus
krittick Dec 7, 2021
2e00f84
more docstrings
krittick Dec 7, 2021
b84d358
fix note
krittick Dec 7, 2021
a4228dd
Merge branch 'master' into ext-menus
krittick Dec 10, 2021
e8d17a8
start work on adding timeout handling to paginator
krittick Dec 12, 2021
5038080
Merge branch 'Pycord-Development:master' into ext-menus
krittick Dec 13, 2021
ffb1027
Merge remote-tracking branch 'origin/ext-menus' into ext-menus
krittick Dec 14, 2021
67f0a7f
timeout work
krittick Dec 14, 2021
3a0101f
update return values, add message attribute
krittick Dec 14, 2021
546a51e
update return values, add message attribute
krittick Dec 14, 2021
0106c48
update return values, add message attribute
krittick Dec 14, 2021
ecf7850
add disable_on_timeout parameter
krittick Dec 14, 2021
b88d833
add changes to message returns in respond() as well
krittick Dec 14, 2021
35a0bab
revert inadvertent change to setup.py
krittick Dec 14, 2021
0ccbeee
[wip] partial fix for slash groups example (#574)
krittick Dec 16, 2021
1f60568
potential fix for second part of slash groups example
krittick Dec 16, 2021
824a5c6
Revert "potential fix for second part of slash groups example"
krittick Dec 16, 2021
039ccd7
Merge branch 'Pycord-Development:master' into ext-menus
krittick Dec 17, 2021
14ec7c0
Merge branch 'Pycord-Development:master' into master
krittick Dec 18, 2021
08cf279
Merge remote-tracking branch 'origin/master' into ext-menus
krittick Dec 19, 2021
36204e2
fix typing
krittick Dec 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions discord/ext/menus/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
discord.ext.menus
~~~~~~~~~~~~~~~~~~~~~
An extension module to provide useful menu options.

:copyright: 2021-present Pycord-Development
:license: MIT, see LICENSE for more details.
"""

from .pagination import *
375 changes: 375 additions & 0 deletions discord/ext/menus/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,375 @@
"""
The MIT License (MIT)

Copyright (c) 2021-present Pycord Development

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import Dict, List, Optional, Union

import discord
from discord import abc
from discord.commands import ApplicationContext
from discord.ext.commands import Context


class PaginatorButton(discord.ui.Button):
"""Creates a button used to navigate the paginator.

Parameters
----------

button_type: :class:`str`
The type of button being created.
Must be one of ``first``, ``prev``, ``next``, or ``last``.
paginator: :class:`Paginator`
The Paginator class where this button will be used
"""

def __init__(self, label, emoji, style, disabled, button_type, paginator):
super().__init__(label=label, emoji=emoji, style=style, disabled=disabled, row=0)
self.label = label
self.emoji = emoji
self.style = style
self.disabled = disabled
self.button_type = button_type
self.paginator = paginator

async def callback(self, interaction: discord.Interaction):
if self.button_type == "first":
self.paginator.current_page = 0
elif self.button_type == "prev":
self.paginator.current_page -= 1
elif self.button_type == "next":
self.paginator.current_page += 1
elif self.button_type == "last":
self.paginator.current_page = self.paginator.page_count
await self.paginator.goto_page(interaction=interaction, page_number=self.paginator.current_page)


class Paginator(discord.ui.View):
"""Creates a paginator which can be sent as a message and uses buttons for navigation.

Attributes
----------
current_page: :class:`int`
Zero-indexed value showing the current page number
page_count: :class:`int`
Zero-indexed value showing the total number of pages
buttons: Dict[:class:`str`, Dict[:class:`str`, Union[:class:`~PaginatorButton`, :class:`bool`]]]
Dictionary containing the :class:`~PaginatorButton` objects included in this Paginator
user: Optional[Union[:class:`~discord.User`, :class:`~discord.Member`]]
The user or member that invoked the Paginator.
message: Union[:class:`~discord.Message`, :class:`~discord.WebhookMessage`]
The message sent from the Paginator.

Parameters
----------
pages: Union[List[:class:`str`], List[:class:`discord.Embed`]]
Your list of strings or embeds to paginate
show_disabled: :class:`bool`
Choose whether or not to show disabled buttons
show_indicator: :class:`bool`
Choose whether to show the page indicator
author_check: :class:`bool`
Choose whether or not only the original user of the command can change pages
disable_on_timeout: :class:`bool`
Should the buttons be disabled when the pagintator view times out?
custom_view: Optional[:class:`discord.ui.View`]
A custom view whose items are appended below the pagination buttons
"""

def __init__(
self,
pages: Union[List[str], List[discord.Embed]],
show_disabled=True,
show_indicator=True,
author_check=True,
disable_on_timeout=True,
custom_view: Optional[discord.ui.View] = None,
timeout: Optional[float] = 180.0,
):
super().__init__(timeout=timeout)
self.timeout = timeout
self.pages = pages
self.current_page = 0
self.page_count = len(self.pages) - 1
self.show_disabled = show_disabled
self.show_indicator = show_indicator
self.disable_on_timeout = disable_on_timeout
self.custom_view = custom_view
self.message: Union[discord.Message, discord.WebhookMessage]
self.buttons = {
"first": {
"object": PaginatorButton(
label="<<",
style=discord.ButtonStyle.blurple,
emoji=None,
disabled=True,
button_type="first",
paginator=self,
),
"hidden": True,
},
"prev": {
"object": PaginatorButton(
label="<",
style=discord.ButtonStyle.red,
emoji=None,
disabled=True,
button_type="prev",
paginator=self,
),
"hidden": True,
},
"page_indicator": {
"object": discord.ui.Button(
label=f"{self.current_page + 1}/{self.page_count + 1}",
style=discord.ButtonStyle.gray,
disabled=True,
row=0,
),
"hidden": False,
},
"next": {
"object": PaginatorButton(
label=">",
style=discord.ButtonStyle.green,
emoji=None,
disabled=True,
button_type="next",
paginator=self,
),
"hidden": True,
},
"last": {
"object": PaginatorButton(
label=">>",
style=discord.ButtonStyle.blurple,
emoji=None,
disabled=True,
button_type="last",
paginator=self,
),
"hidden": True,
},
}
self.update_buttons()

self.usercheck = author_check
self.user = None

async def on_timeout(self) -> None:
"""Disables all buttons when the view times out."""
if self.disable_on_timeout:
for item in self.children:
item.disabled = True
await self.message.edit(view=self)

async def goto_page(self, interaction: discord.Interaction, page_number=0):
"""Updates the interaction response message to show the specified page number.

Parameters
----------
interaction: :class:`discord.Interaction`
The interaction which called the Paginator
page_number: :class:`int`
The page to display.

.. note::

Page numbers are zero-indexed when referenced internally, but appear as one-indexed when shown to the user.

Returns
---------
:class:`~Paginator`
The Paginator class
"""
self.update_buttons()
page = self.pages[page_number]
await interaction.response.edit_message(
content=page if isinstance(page, str) else None, embed=page if isinstance(page, discord.Embed) else None, view=self
)

async def interaction_check(self, interaction):
if self.usercheck:
return self.user == interaction.user
return True

def customize_button(
self, button_name: str = None, button_label: str = None, button_emoji=None, button_style: discord.ButtonStyle = discord.ButtonStyle.gray
) -> Union[PaginatorButton, bool]:
"""Allows you to easily customize the various pagination buttons.

Parameters
----------
button_name: :class:`str`
Name of the button to customize
button_label: :class:`str`
Label to display on the button
button_emoji:
Emoji to display on the button
button_style: :class:`~discord.ButtonStyle`
ButtonStyle to use for the button

Returns
-------
:class:`~PaginatorButton`
The button that was customized
"""

if button_name not in self.buttons.keys():
return False
button: PaginatorButton = self.buttons[button_name]["object"]
button.label = button_label
button.emoji = button_emoji
button.style = button_style
return button

def update_buttons(self) -> Dict:
"""Updates the display state of the buttons (disabled/hidden)

Returns
-------
Dict[:class:`str`, Dict[:class:`str`, Union[:class:`~PaginatorButton`, :class:`bool`]]]
The dictionary of buttons that was updated.
"""
for key, button in self.buttons.items():
if key == "first":
if self.current_page <= 1:
button["hidden"] = True
elif self.current_page >= 1:
button["hidden"] = False
elif key == "last":
if self.current_page >= self.page_count - 1:
button["hidden"] = True
if self.current_page < self.page_count - 1:
button["hidden"] = False
elif key == "next":
if self.current_page == self.page_count:
button["hidden"] = True
elif self.current_page < self.page_count:
button["hidden"] = False
elif key == "prev":
if self.current_page <= 0:
button["hidden"] = True
elif self.current_page >= 0:
button["hidden"] = False
self.clear_items()
if self.show_indicator:
self.buttons["page_indicator"]["object"].label = f"{self.current_page + 1}/{self.page_count + 1}"
for key, button in self.buttons.items():
if key != "page_indicator":
if button["hidden"]:
button["object"].disabled = True
if self.show_disabled:
self.add_item(button["object"])
else:
button["object"].disabled = False
self.add_item(button["object"])
elif self.show_indicator:
self.add_item(button["object"])

# We're done adding standard buttons, so we can now add any specified custom view items below them
# The bot developer should handle row assignments for their view before passing it to Paginator
if self.custom_view:
for item in self.custom_view.children:
self.add_item(item)

return self.buttons

async def send(self, messageable: abc.Messageable, ephemeral: bool = False):
"""Sends a message with the paginated items.


Parameters
Lulalaby marked this conversation as resolved.
Show resolved Hide resolved
------------
messageable: :class:`discord.abc.Messageable`
The messageable channel to send to.
ephemeral: :class:`bool`
Choose whether the message is ephemeral or not. Only works with slash commands.

Returns
--------
Union[:class:`~discord.Message`, :class:`~discord.WebhookMessage`]
The message that was sent with the Paginator.
"""

if not isinstance(messageable, abc.Messageable):
raise TypeError("messageable should be a subclass of abc.Messageable")

page = self.pages[0]

if isinstance(messageable, (ApplicationContext, Context)):
self.user = messageable.author

if isinstance(messageable, ApplicationContext):
msg = await messageable.respond(
content=page if isinstance(page, str) else None,
embed=page if isinstance(page, discord.Embed) else None,
view=self,
ephemeral=ephemeral,
)

else:
msg = await messageable.send(
content=page if isinstance(page, str) else None,
embed=page if isinstance(page, discord.Embed) else None,
view=self,
)
if isinstance(msg, (discord.WebhookMessage, discord.Message)):
self.message = msg
elif isinstance(msg, discord.Interaction):
self.message = await msg.original_message()

return self.message

async def respond(self, interaction: discord.Interaction, ephemeral: bool = False):
"""Sends an interaction response or followup with the paginated items.


Parameters
Lulalaby marked this conversation as resolved.
Show resolved Hide resolved
------------
interaction: :class:`discord.Interaction`
The interaction associated with this response.
ephemeral: :class:`bool`
Choose whether the message is ephemeral or not.

Returns
--------
:class:`~discord.Interaction`
The message sent with the paginator
"""
page = self.pages[0]
self.user = interaction.user

if interaction.response.is_done():
msg = await interaction.followup.send(
content=page if isinstance(page, str) else None, embed=page if isinstance(page, discord.Embed) else None, view=self, ephemeral=ephemeral
)

else:
msg = await interaction.response.send_message(
content=page if isinstance(page, str) else None, embed=page if isinstance(page, discord.Embed) else None, view=self, ephemeral=ephemeral
)
if isinstance(msg, (discord.WebhookMessage, discord.Message)):
self.message = msg
elif isinstance(msg, discord.Interaction):
self.message = await msg.original_message()
return self.message
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
'discord_extensions': [
('discord.ext.commands', 'ext/commands'),
('discord.ext.tasks', 'ext/tasks'),
('discord.ext.menus', 'ext/menus'),
],
}

Expand Down
Loading