Source code for discord.ext.paginators.button_paginator
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
from collections.abc import Sequence
from discord import ButtonStyle, Emoji, PartialEmoji
import discord
from discord.ui import Button, Modal, TextInput
from .base_paginator import BaseClassPaginator
from ._types import PageT
if TYPE_CHECKING:
from typing_extensions import Unpack, Self
from ._types import BasePaginatorKwargs
ValidButtonKeys = Literal["FIRST", "LEFT", "RIGHT", "LAST", "STOP", "PAGE_INDICATOR"]
ValidButtonsDict = dict[ValidButtonKeys, "PaginatorButton"]
BaseClassPaginator = BaseClassPaginator
__all__: tuple[str, ...] = ("ButtonPaginator", "PaginatorButton")
class ChooseNumber(Modal):
number_input: TextInput[Any] = TextInput(
placeholder="Current: {0}",
label="Enter a number between 1 and {0}",
custom_id="paginator:textinput:choose_number",
max_length=0,
min_length=1,
)
def __init__(self, paginator: ButtonPaginator[Any], /, **kwargs: Any) -> None:
super().__init__(
title="Which page would you like to go to?",
timeout=paginator.timeout,
custom_id="paginator:modal:choose_number",
**kwargs,
)
self.paginator: ButtonPaginator[Any] = paginator
self.number_input.max_length = paginator.max_pages
self.number_input.label = self.number_input.label.format(paginator.max_pages)
# type checker
if not self.number_input.placeholder:
self.number_input.placeholder = f"Current: {paginator.current_page + 1}"
else:
self.number_input.placeholder = self.number_input.placeholder.format(paginator.current_page + 1)
self.value: Optional[int] = None
async def on_submit(self, interaction: discord.Interaction[Any]) -> None:
# can't happen but type checker
if not self.number_input.value:
await interaction.response.send_message("Please enter a number!", ephemeral=True)
self.stop()
return
if (
not self.number_input.value.isdigit()
or int(self.number_input.value) <= 0
or int(self.number_input.value) > self.paginator.max_pages
):
await interaction.response.send_message(
f"Please enter a valid number between 1 and {self.paginator.max_pages}", ephemeral=True
)
self.stop()
return
number = int(self.number_input.value) - 1
if number == self.paginator.current_page:
await interaction.response.send_message("That is the current page!", ephemeral=True)
self.stop()
return
self.value = number
await interaction.response.defer()
self.stop()
class PageSwitcherAndStopButtonView(discord.ui.View):
STOP: Optional[Button["ButtonPaginator[Any]"]] = None # filled in _add_buttons
PAGE_INDICATOR: Optional[Button["ButtonPaginator[Any]"]] = None # filled in _add_buttons
def __init__(self, paginator: ButtonPaginator[Any], /) -> None:
super().__init__(timeout=paginator.timeout)
def _add_buttons(self, paginator: ButtonPaginator[Any], /) -> None:
self._paginator: ButtonPaginator[Any] = paginator
if not any(key in ("STOP", "PAGE_INDICATOR") for key in paginator._buttons):
raise ValueError("STOP and PAGE_INDICATOR buttons are required if combine_switcher_and_stop_button is True.")
org_page_indicator_button: PaginatorButton = paginator._buttons["PAGE_INDICATOR"]
page_indicator_button = PaginatorButton(
label="Switch Page",
emoji=org_page_indicator_button.emoji,
style=org_page_indicator_button.style,
custom_id="switch_page",
disabled=False,
)
org_stop_button = paginator._buttons["STOP"]
stop_button = PaginatorButton(
label=org_stop_button.label,
emoji=org_stop_button.emoji,
style=org_stop_button.style,
custom_id="stop_button",
disabled=False,
)
buttons: dict[str, PaginatorButton] = {
"STOP": stop_button,
"PAGE_INDICATOR": page_indicator_button,
}
for name, button in buttons.items():
setattr(self, name, button)
self.add_item(button)
async def callback(self, interaction: discord.Interaction[Any], button: PaginatorButton) -> None:
if button.custom_id == "stop_button":
await interaction.response.defer()
await interaction.delete_original_response()
await self._paginator.stop_paginator(None)
return
if button.custom_id == "switch_page":
new_page = await self._paginator._handle_modal(interaction)
await interaction.delete_original_response()
if new_page is not None:
self._paginator.current_page = new_page
else:
return
await self._paginator.switch_page(None, self._paginator.current_page)
[docs]
class PaginatorButton(Button[Union["ButtonPaginator[Any]", PageSwitcherAndStopButtonView]]):
"""A button for the paginator.
This class has a few parameters that differ from the base button.
This can can be used passed to the ``buttons`` parameter in :class:`.ButtonPaginator`
to customize the buttons used.
See other parameters on :class:`discord.ui.Button`.
Parameters
-----------
position: Optional[:class:`int`]
The position of the button. Defaults to ``None``.
If not specified, the button will be placed in the order they were added
or whatever order discord.py adds them in.
"""
def __init__(
self,
*,
emoji: Optional[Union[Emoji, PartialEmoji, str]] = None,
label: Optional[str] = None,
custom_id: Optional[str] = None,
style: ButtonStyle = ButtonStyle.blurple,
row: Optional[int] = None,
disabled: bool = False,
position: Optional[int] = None,
) -> None:
self.__original_kwargs: dict[str, Any] = {
"emoji": emoji,
"label": label,
"custom_id": custom_id,
"style": style,
"row": row,
"disabled": disabled,
"position": position,
}
super().__init__(emoji=emoji, label=label, custom_id=custom_id, style=style, row=row, disabled=disabled)
self.position: Optional[int] = position
async def callback(self, interaction: discord.Interaction[Any]) -> None:
# type checker
if not self.view:
raise ValueError("Something went wrong... button.view is None")
if isinstance(self.view, PageSwitcherAndStopButtonView):
await self.view.callback(interaction, self)
return
if self.custom_id == "stop_button":
await self.view.stop_paginator(interaction)
return
if self.custom_id == "right_button":
self.view.current_page += 1
elif self.custom_id == "left_button":
self.view.current_page -= 1
elif self.custom_id == "first_button":
self.view.current_page = 0
elif self.custom_id == "last_button":
self.view.current_page = self.view.max_pages - 1
elif self.custom_id == "page_indicator_button":
if self.view._stop_button_and_page_switcher_view:
await interaction.response.send_message(view=self.view._stop_button_and_page_switcher_view, ephemeral=True)
return
new_page = await self.view._handle_modal(interaction)
if new_page is not None:
self.view.current_page = new_page
else:
return
await self.view.switch_page(interaction, self.view.current_page)
def _copy(self) -> PaginatorButton:
"""Create a copy of the button.
Returns
-------
:class:`.PaginatorButton`
A copy of the button.
"""
return PaginatorButton(**self.__original_kwargs)
[docs]
class ButtonPaginator(BaseClassPaginator[PageT]):
"""A paginator that uses buttons to switch pages.
This class has a few parameters that differ from the base paginator.
Just for clarification, here is a list of supported pages:
- :class:`discord.Embed`
- :class:`discord.File`
- :class:`discord.Attachment`
- :class:`str`
- :class:`dict`
- :class:`list` or :class:`tuple` of the above
See other parameters on :class:`.BaseClassPaginator`.
Parameters
----------
buttons: Dict[:class:`str`, :class:`.PaginatorButton`]
A dictionary of buttons to use. The keys must be one of the following:
"FIRST", "LEFT", "RIGHT", "LAST", "STOP", "PAGE_INDICATOR".
The values must be a PaginatorButton or ``None`` to remove the button.
If not specified, the default buttons will be used.
Example
-------
.. code-block:: python3
:linenos:
from discord.ext.paginators.button_paginator import ButtonPaginator, PaginatorButton
custom_buttons = {
# change the label of the first button from "First" to "Go to first page"
"FIRST": PaginatorButton(label="Go to first page"),
# change the style of the LAST button to red
"LAST": PaginatorButton(style=ButtonStyle.red),
}
# pass the custom buttons to the paginator
paginator = ButtonPaginator(pages, buttons=custom_buttons)
... # rest of code
always_show_stop_button: bool
Whether to always show the stop button, even if there is only one page.
Defaults to ``False``.
.. note::
If ``always_show_stop_button`` is ``True``, the ``STOP`` key in ``buttons`` cannot be ``None``.
combine_switcher_and_stop_button: :class:`bool`
Whether to combine the page switcher and stop button into the paginator indicator which will send another set
of buttons to switch pages and stop the paginator as an ephemeral message when clicked.
Defaults to ``False``.
.. note::
If ``combine_switcher_and_stop_button`` is ``True``, the ``STOP`` and ``PAGE_INDICATOR`` keys in ``buttons`` cannot be ``None``.
style_if_clickable: :class:`discord.ButtonStyle`
The style to change the buttons that are not disabled / clickable to and changes them back to the original style otherwise.
Defaults to :attr:`discord.ButtonStyle.green`. Pass ``None`` to disable this feature.
.. versionadded:: 0.3.0
**kwargs: Unpack[:class:`.BasePaginatorKwargs`]
See other parameters on :class:`discord.ext.paginator.base_paginator.BaseClassPaginator`.
"""
FIRST: Optional[PaginatorButton] = None # filled in __add_buttons
LEFT: Optional[PaginatorButton] = None # filled in __add_buttons
RIGHT: Optional[PaginatorButton] = None # filled in __add_buttons
LAST: Optional[PaginatorButton] = None # filled in __add_buttons
STOP: Optional[PaginatorButton] = None # filled in __add_buttons
PAGE_INDICATOR: Optional[PaginatorButton] = None # filled in __add_buttons
def __init__(
self,
pages: Sequence[PageT],
*,
buttons: ValidButtonsDict = {},
always_show_stop_button: bool = False,
combine_switcher_and_stop_button: bool = False,
style_if_clickable: ButtonStyle | None = discord.utils.MISSING,
**kwargs: Unpack[BasePaginatorKwargs[Self]],
) -> None:
"""Initialize the Paginator."""
super().__init__(pages, **kwargs)
DEFAULT_BUTTONS: dict[ValidButtonKeys, PaginatorButton] = {
"FIRST": PaginatorButton(label="First", position=0),
"LEFT": PaginatorButton(label="Left", position=1),
"PAGE_INDICATOR": PaginatorButton(label="Page N/A / N/A", position=2, disabled=False),
"RIGHT": PaginatorButton(label="Right", position=3),
"LAST": PaginatorButton(label="Last", position=4),
"STOP": PaginatorButton(label="Stop", style=ButtonStyle.danger, position=5),
}
self._buttons: dict[ValidButtonKeys, PaginatorButton] = DEFAULT_BUTTONS.copy()
if buttons:
valid_keys = ", ".join(DEFAULT_BUTTONS.keys())
error_message = (
f"buttons must be a dictionary of keys: {valid_keys} and PaginatorButton or None "
"to remove the button as the value. Or don't specify the kwarg to use the default buttons."
)
if (
not isinstance(buttons, dict)
or any(k not in DEFAULT_BUTTONS for k in buttons)
or not all(not v or isinstance(v, PaginatorButton) for v in buttons.values())
):
raise TypeError(error_message)
self._buttons.update(buttons)
self._stop_button_and_page_switcher_view: Optional[PageSwitcherAndStopButtonView] = (
PageSwitcherAndStopButtonView(self) if combine_switcher_and_stop_button else None
)
self.always_show_stop_button: bool = always_show_stop_button
if style_if_clickable is discord.utils.MISSING:
self._style_if_clickable = ButtonStyle.green
elif style_if_clickable is not None:
if not isinstance(style_if_clickable, ButtonStyle):
raise TypeError("style_if_clickable must be a discord.ButtonStyle or None.")
self._style_if_clickable = style_if_clickable
else:
self._style_if_clickable = None
self.__add_buttons()
def __handle_always_show_stop_button(self) -> None:
if not self.always_show_stop_button:
return
if "STOP" not in self._buttons:
raise ValueError("STOP button is required if always_show_stop_button is True.")
name = "STOP"
button = self._buttons[name]
button.custom_id = f"{name.lower()}_button"
setattr(self, name, button)
self.add_item(button)
async def _handle_modal(self, interaction: discord.Interaction[Any]) -> Optional[int]:
modal = ChooseNumber(self)
await interaction.response.send_modal(modal)
await modal.wait()
return modal.value
def __add_buttons(self) -> None:
if not self.max_pages > 1:
if self.always_show_stop_button:
self.__handle_always_show_stop_button()
return
self.stop()
return
_buttons: dict[str, PaginatorButton] = {
name: button._copy() for name, button in self._buttons.copy().items() if button
}
sorted_buttons = sorted(_buttons.items(), key=lambda b: b[1].position if b[1].position is not None else 0)
for name, button in sorted_buttons:
custom_id = f"{name.lower()}_button"
button.custom_id = custom_id
setattr(self, name, button)
if button.custom_id == "page_indicator_button":
button.label = self.page_string
if self.max_pages <= 2:
button.disabled = True
if button.custom_id in ("first_button", "last_button"):
if self.max_pages <= 2:
continue
label = button.label if button.label else ""
if button.custom_id == "first_button":
button.label = f"1 {label}"
else:
button.label = f"{label} {self.max_pages}"
if self._stop_button_and_page_switcher_view and button.custom_id == "stop_button":
continue
self.add_item(button)
if self._stop_button_and_page_switcher_view:
self._stop_button_and_page_switcher_view._add_buttons(self)
self._update_buttons_state()
def _update_buttons_state(self) -> None:
for button in self.children:
# type checker
if not isinstance(button, PaginatorButton):
continue
# type checker
if not button.custom_id:
raise ValueError("Something went wrong... button.custom_id is None")
original_button = self._buttons.get(f"{button.custom_id.split('_')[0].upper()}") # type: ignore
if button.custom_id in ("page_indicator_button", "stop_button"):
if button.custom_id == "page_indicator_button":
button.label = self.page_string
continue
if button.custom_id in ("right_button", "last_button"):
button.disabled = self._current_page >= self.max_pages - 1
elif button.custom_id in ("left_button", "first_button"):
button.disabled = self._current_page <= 0
if button.custom_id in ("first_button", "last_button"):
if self.max_pages <= 2:
button.disabled = True
if original_button:
label = original_button.label if original_button.label else ""
if button.custom_id == "first_button":
button.label = f"1 {label}"
else:
button.label = f"{label} {self.max_pages}"
if self._style_if_clickable is not None:
if not button.disabled:
button.style = self._style_if_clickable
else:
button.style = original_button.style if original_button else ButtonStyle.secondary
@property
def current_page(self) -> int:
return self._current_page
@current_page.setter
def current_page(self, value: int) -> None:
self._current_page = value
self._update_buttons_state()
def _send(self, *args: Any, **kwargs: Any) -> Any:
self._update_buttons_state()
return super()._send(*args, **kwargs)