Skip to content
Merged
Changes from all commits
Commits
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
124 changes: 110 additions & 14 deletions discord/ext/pages/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ def __init__(
self.paginator = None

async def callback(self, interaction: discord.Interaction):
"""|coro|

The coroutine that is called when the navigation button is clicked.

Parameters
-----------
interaction: :class:`discord.Interaction`
The interaction created by clicking the navigation button.
"""
if self.button_type == "first":
self.paginator.current_page = 0
elif self.button_type == "prev":
Expand All @@ -102,7 +111,7 @@ async def callback(self, interaction: discord.Interaction):
self.paginator.current_page += 1
elif self.button_type == "last":
self.paginator.current_page = self.paginator.page_count
await self.paginator.goto_page(page_number=self.paginator.current_page)
await self.paginator.goto_page(page_number=self.paginator.current_page, interaction=interaction)


class Page:
Expand All @@ -116,15 +125,34 @@ class Page:
The content of the page. Corresponds to the :class:`discord.Message.content` attribute.
embeds: Optional[List[Union[List[:class:`discord.Embed`], :class:`discord.Embed`]]]
The embeds of the page. Corresponds to the :class:`discord.Message.embeds` attribute.
custom_view: Optional[:class:`discord.ui.View`]
The custom view shown when the page is visible. Overrides the `custom_view` attribute of the main paginator.
"""

def __init__(
self, content: Optional[str] = None, embeds: Optional[List[Union[List[discord.Embed], discord.Embed]]] = None
self,
content: Optional[str] = None,
embeds: Optional[List[Union[List[discord.Embed], discord.Embed]]] = None,
custom_view: Optional[discord.ui.View] = None,
**kwargs,
):
if content is None and embeds is None:
raise discord.InvalidArgument("A page cannot have both content and embeds equal to None.")
self._content = content
self._embeds = embeds
self._embeds = embeds or []
self._custom_view = custom_view

async def callback(self, interaction: Optional[discord.Interaction] = None):
"""|coro|

The coroutine associated to a specific page. If `Paginator.page_action()` is used, this coroutine is called.

Parameters
----------
interaction: Optional[:class:`discord.Interaction`]
The interaction associated with the callback, if any.
"""
pass

@property
def content(self) -> Optional[str]:
Expand All @@ -146,6 +174,16 @@ def embeds(self, value: Optional[List[Union[List[discord.Embed], discord.Embed]]
"""Sets the embeds for the page."""
self._embeds = value

@property
def custom_view(self) -> Optional[discord.ui.View]:
"""Gets the custom view assigned to the page."""
return self._custom_view

@custom_view.setter
def custom_view(self, value: Optional[discord.ui.View]):
"""Assigns a custom view to be shown when the page is displayed."""
self._custom_view = value


class PageGroup:
"""Creates a group of pages which the user can switch between.
Expand Down Expand Up @@ -189,6 +227,9 @@ class PageGroup:
custom_buttons: Optional[List[:class:`PaginatorButton`]]
A list of PaginatorButtons to initialize the Paginator with.
If ``use_default_buttons`` is ``True``, this parameter is ignored.
trigger_on_display: :class:`bool`
Whether to automatically trigger the callback associated with a `Page` whenever it is displayed.
Has no effect if no callback exists for a `Page`.
"""

def __init__(
Expand All @@ -207,6 +248,7 @@ def __init__(
custom_view: Optional[discord.ui.View] = None,
timeout: Optional[float] = None,
custom_buttons: Optional[List[PaginatorButton]] = None,
trigger_on_display: Optional[bool] = None,
):
self.label = label
self.description = description
Expand All @@ -222,6 +264,7 @@ def __init__(
self.custom_view: discord.ui.View = custom_view
self.timeout: float = timeout
self.custom_buttons: List = custom_buttons
self.trigger_on_display = trigger_on_display


class Paginator(discord.ui.View):
Expand Down Expand Up @@ -253,11 +296,15 @@ class Paginator(discord.ui.View):
Whether to loop the pages when clicking prev/next while at the first/last page in the list.
custom_view: Optional[:class:`discord.ui.View`]
A custom view whose items are appended below the pagination components.
If the currently displayed page has a `custom_view` assigned, it will replace these view components when that page is displayed.
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the paginator before no longer accepting input.
custom_buttons: Optional[List[:class:`PaginatorButton`]]
A list of PaginatorButtons to initialize the Paginator with.
If ``use_default_buttons`` is ``True``, this parameter is ignored.
trigger_on_display: :class:`bool`
Whether to automatically trigger the callback associated with a `Page` whenever it is displayed.
Has no effect if no callback exists for a `Page`.

Attributes
----------
Expand Down Expand Up @@ -292,6 +339,7 @@ def __init__(
custom_view: Optional[discord.ui.View] = None,
timeout: Optional[float] = 180.0,
custom_buttons: Optional[List[PaginatorButton]] = None,
trigger_on_display: Optional[bool] = None,
) -> None:
super().__init__(timeout=timeout)
self.timeout: float = timeout
Expand All @@ -310,7 +358,7 @@ def __init__(
List[str], List[Page], List[Union[List[discord.Embed], discord.Embed]]
] = self.page_groups[0].pages

self.page_count = len(self.pages) - 1
self.page_count = max(len(self.pages), 0)
self.buttons = {}
self.custom_buttons: List = custom_buttons
self.show_disabled = show_disabled
Expand All @@ -320,6 +368,7 @@ def __init__(
self.default_button_row = default_button_row
self.loop_pages = loop_pages
self.custom_view: discord.ui.View = custom_view
self.trigger_on_display = trigger_on_display
self.message: Union[discord.Message, discord.WebhookMessage, None] = None

if self.custom_buttons and not self.use_default_buttons:
Expand Down Expand Up @@ -348,6 +397,7 @@ async def update(
custom_view: Optional[discord.ui.View] = None,
timeout: Optional[float] = None,
custom_buttons: Optional[List[PaginatorButton]] = None,
trigger_on_display: Optional[bool] = None,
):
"""Updates the existing :class:`Paginator` instance with the provided options.

Expand Down Expand Up @@ -379,6 +429,9 @@ async def update(
custom_buttons: Optional[List[:class:`PaginatorButton`]]
A list of PaginatorButtons to initialize the Paginator with.
If ``use_default_buttons`` is ``True``, this parameter is ignored.
trigger_on_display: :class:`bool`
Whether to automatically trigger the callback associated with a `Page` whenever it is displayed.
Has no effect if no callback exists for a `Page`.
"""

# Update pages and reset current_page to 0 (default)
Expand All @@ -398,6 +451,7 @@ async def update(
self.loop_pages = loop_pages if loop_pages is not None else self.loop_pages
self.custom_view: discord.ui.View = None if custom_view is None else custom_view
self.timeout: float = timeout if timeout is not None else self.timeout
self.trigger_on_display = trigger_on_display if trigger_on_display is not None else self.trigger_on_display
if custom_buttons and not self.use_default_buttons:
self.buttons = {}
for button in custom_buttons:
Expand Down Expand Up @@ -470,7 +524,7 @@ async def cancel(
else:
await self.message.edit(view=self)

async def goto_page(self, page_number=0) -> discord.Message:
async def goto_page(self, page_number: int = 0, *, interaction: Optional[discord.Interaction] = None) -> None:
"""Updates the paginator message to show the specified page number.

Parameters
Expand All @@ -482,6 +536,10 @@ async def goto_page(self, page_number=0) -> discord.Message:

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

interaction: Optional[:class:`discord.Interaction`]
The interaction to use when editing the message. If not provided, the message will be edited using the paginator's
stored :attr:`message` attribute instead.

Returns
-------
:class:`~discord.Message`
Expand All @@ -495,11 +553,19 @@ async def goto_page(self, page_number=0) -> discord.Message:
page = self.pages[page_number]
page = self.get_page_content(page)

return await self.message.edit(
content=page.content,
embeds=page.embeds,
view=self,
)
if page.custom_view:
self.update_custom_view(page.custom_view)

if interaction:
await interaction.response.edit_message(content=page.content, embeds=page.embeds, view=self)
else:
await self.message.edit(
content=page.content,
embeds=page.embeds,
view=self,
)
if self.trigger_on_display:
await self.page_action(interaction=interaction)

async def interaction_check(self, interaction: discord.Interaction) -> bool:
if self.usercheck:
Expand All @@ -508,7 +574,7 @@ async def interaction_check(self, interaction: discord.Interaction) -> bool:

def add_menu(self):
"""Adds the default :class:`PaginatorMenu` instance to the paginator."""
self.menu = PaginatorMenu(self.page_groups, placeholder=self.menu_placeholder)
self.menu = PaginatorMenu(self.page_groups, placeholder=self.menu_placeholder, custom_id="pages_group_menu")
self.menu.paginator = self
self.add_item(self.menu)

Expand All @@ -521,32 +587,37 @@ def add_default_buttons(self):
label="<<",
style=discord.ButtonStyle.blurple,
row=self.default_button_row,
custom_id="pages_first_button",
),
PaginatorButton(
"prev",
label="<",
style=discord.ButtonStyle.red,
loop_label="↪",
row=self.default_button_row,
custom_id="pages_prev_button",
),
PaginatorButton(
"page_indicator",
style=discord.ButtonStyle.gray,
disabled=True,
row=self.default_button_row,
custom_id="pages_indicator_button",
),
PaginatorButton(
"next",
label=">",
style=discord.ButtonStyle.green,
loop_label="↩",
row=self.default_button_row,
custom_id="pages_next_button",
),
PaginatorButton(
"last",
label=">>",
style=discord.ButtonStyle.blurple,
row=self.default_button_row,
custom_id="pages_last_button",
),
]
for button in default_buttons:
Expand Down Expand Up @@ -640,13 +711,21 @@ def update_buttons(self) -> Dict:
# We're done adding standard buttons and menus, 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)
self.update_custom_view(self.custom_view)

return self.buttons

def update_custom_view(self, custom_view: discord.ui.View):
"""Updates the custom view shown on the paginator."""
if isinstance(self.custom_view, discord.ui.View):
for item in self.custom_view.children:
self.remove_item(item)
for item in custom_view.children:
self.add_item(item)

@staticmethod
def get_page_content(page: Union[Page, str, discord.Embed, List[discord.Embed]]) -> Page:
"""Returns the correct content type for a page based on its content."""
"""Converts a page into a :class:`Page` object based on its content."""
if isinstance(page, Page):
return page
elif isinstance(page, str):
Expand All @@ -659,6 +738,17 @@ def get_page_content(page: Union[Page, str, discord.Embed, List[discord.Embed]])
else:
raise TypeError("All list items must be embeds.")

async def page_action(self, interaction: Optional[discord.Interaction] = None) -> None:
"""Triggers the callback associated with the current page, if any.

Parameters
----------
interaction: Optional[:class:`discord.Interaction`]
The interaction that was used to trigger the page action.
"""
if self.get_page_content(self.pages[self.current_page]).callback:
await self.get_page_content(self.pages[self.current_page]).callback(interaction=interaction)

async def send(
self,
ctx: Context,
Expand Down Expand Up @@ -719,6 +809,9 @@ async def send(
page = self.pages[self.current_page]
page_content = self.get_page_content(page)

if page_content.custom_view:
self.update_custom_view(page_content.custom_view)

self.user = ctx.author

if target:
Expand Down Expand Up @@ -790,6 +883,9 @@ async def respond(
page: Union[Page, str, discord.Embed, List[discord.Embed]] = self.pages[self.current_page]
page_content: Page = self.get_page_content(page)

if page_content.custom_view:
self.update_custom_view(page_content.custom_view)

self.user = interaction.user
if target:
await interaction.response.send_message(target_message, ephemeral=ephemeral)
Expand Down