Skip to content

Commit 08148a6

Browse files
committed
Adjust strongly how paginator buttons work
1 parent d041bed commit 08148a6

File tree

1 file changed

+114
-83
lines changed

1 file changed

+114
-83
lines changed

jishaku/paginators.py

+114-83
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
from jishaku.hljs import get_language, guess_file_traits
2525
from jishaku.types import BotT, ContextA
2626

27+
if typing.TYPE_CHECKING:
28+
from discord.types.components import ButtonComponent
29+
2730
__all__ = ('EmojiSettings', 'PaginatorInterface', 'PaginatorEmbedInterface',
2831
'WrappedPaginator', 'FilePaginator', 'use_file_check')
2932

@@ -214,22 +217,37 @@ class EmojiSettings(typing.NamedTuple):
214217
)
215218

216219

217-
T = typing.TypeVar('T', bound=ui.View)
218-
219-
MaybeButton = typing.Union[discord.Interaction, ui.Button[T]]
220+
V_co = typing.TypeVar('V_co', bound='ui.View', covariant=True)
220221

221222

222-
def button_either_arg(
223-
a: MaybeButton[T],
224-
b: MaybeButton[T]
225-
) -> typing.Tuple[discord.Interaction, ui.Button[T]]:
223+
class DynamicButton(ui.Button[V_co]):
226224
"""
227-
Compatibility function to allow interaction and button to come in either order
225+
A ui.Button that has its label be a callback instead of a string.
228226
"""
229227

230-
if isinstance(a, discord.Interaction):
231-
return (a, b) # type: ignore
232-
return (b, a) # type: ignore
228+
def __init__(
229+
self,
230+
callback: typing.Callable[[discord.Interaction], typing.Coroutine[typing.Any, typing.Any, typing.Any]],
231+
label_callback: typing.Callable[[typing.Self], str],
232+
**kwargs, # type: ignore
233+
):
234+
super().__init__(**kwargs) # type: ignore
235+
236+
self.callback = callback # type: ignore
237+
self.label_callback = label_callback
238+
239+
@property
240+
def label(self) -> str:
241+
return self.label_callback(self)
242+
243+
@label.setter
244+
def label(self, value: typing.Any):
245+
# Absorbed to be compatible with ui.Button
246+
pass
247+
248+
def to_component_dict(self) -> 'ButtonComponent':
249+
self._underlying.label = str(self.label_callback(self))
250+
return self._underlying.to_dict()
233251

234252

235253
class PaginatorInterface(ui.View): # pylint: disable=too-many-instance-attributes
@@ -269,7 +287,7 @@ class PaginatorInterface(ui.View): # pylint: disable=too-many-instance-attribut
269287
await interface.add_line("I'm still here!")
270288
"""
271289

272-
def __init__(self, bot: BotT, paginator: commands.Paginator, **kwargs: typing.Any):
290+
def __init__(self, bot: BotT, paginator: commands.Paginator, additional_buttons: typing.Optional[typing.List[ui.Button[typing.Self]]] = None, **kwargs: typing.Any):
273291
if not isinstance(paginator, commands.Paginator): # type: ignore
274292
raise TypeError('paginator must be a commands.Paginator instance')
275293

@@ -300,6 +318,39 @@ def __init__(self, bot: BotT, paginator: commands.Paginator, **kwargs: typing.An
300318

301319
super().__init__(timeout=self.timeout_length)
302320

321+
self.button_start: DynamicButton[typing.Self] = DynamicButton(self.button_start_callback, self.button_start_label, style=discord.ButtonStyle.secondary)
322+
self.button_previous: DynamicButton[typing.Self] = DynamicButton(self.button_previous_callback, self.button_previous_label, style=discord.ButtonStyle.secondary)
323+
self.button_current: DynamicButton[typing.Self] = DynamicButton(self.button_current_callback, self.button_current_label, style=discord.ButtonStyle.primary)
324+
self.button_next: DynamicButton[typing.Self] = DynamicButton(self.button_next_callback, self.button_next_label, style=discord.ButtonStyle.secondary)
325+
self.button_last: DynamicButton[typing.Self] = DynamicButton(self.button_last_callback, self.button_last_label, style=discord.ButtonStyle.secondary)
326+
self.button_goto: DynamicButton[typing.Self] = DynamicButton(self.button_goto_callback, self.button_goto_label, style=discord.ButtonStyle.primary)
327+
self.button_close: DynamicButton[typing.Self] = DynamicButton(self.button_close_callback, self.button_close_label, style=discord.ButtonStyle.danger)
328+
329+
self.additional_buttons = additional_buttons or []
330+
331+
self.buttons: typing.List[ui.Button[typing.Self]] = self.button_definitions()
332+
333+
for button in self.buttons:
334+
self.add_item(button)
335+
336+
def button_definitions(self) -> typing.List[ui.Button[typing.Self]]:
337+
"""
338+
This is an overridable function you can use to remove buttons or customize their order.
339+
340+
If you only need to add buttons, consider passing `additional_buttons` to the constructor.
341+
"""
342+
343+
return [
344+
self.button_start,
345+
self.button_previous,
346+
self.button_current,
347+
self.button_next,
348+
self.button_last,
349+
*self.additional_buttons,
350+
self.button_goto,
351+
self.button_close,
352+
]
353+
303354
@property
304355
def pages(self):
305356
"""
@@ -368,19 +419,6 @@ def send_kwargs(self) -> typing.Dict[str, typing.Any]:
368419
content = self.pages[self.display_page]
369420
return {'content': content, 'view': self}
370421

371-
def update_view(self):
372-
"""
373-
Updates view buttons to correspond to current interface state.
374-
This is used internally.
375-
"""
376-
377-
self.button_start.label = f"1 \u200b {self.emojis.start}"
378-
self.button_previous.label = str(self.emojis.back)
379-
self.button_current.label = str(self.display_page + 1)
380-
self.button_next.label = str(self.emojis.forward)
381-
self.button_last.label = f"{self.emojis.end} \u200b {self.page_count}"
382-
self.button_close.label = f"{self.emojis.close} \u200b Close paginator"
383-
384422
async def add_line(self, *args: typing.Any, **kwargs: typing.Any):
385423
"""
386424
A proxy function that allows this PaginatorInterface to remain locked to the last page
@@ -457,8 +495,6 @@ async def wait_loop(self):
457495
while not self.bot.is_closed():
458496
await asyncio.wait_for(self.send_lock_delayed(), timeout=self.timeout_length)
459497

460-
self.update_view()
461-
462498
try:
463499
await self.message.edit(**self.send_kwargs)
464500
except discord.NotFound:
@@ -487,93 +523,85 @@ async def interaction_check(self, *args: typing.Any): # pylint: disable=argumen
487523
interaction: discord.Interaction
488524
return not self.owner or interaction.user.id == self.owner.id
489525

490-
@ui.button(label="1 \u200b \N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}", style=discord.ButtonStyle.secondary)
491-
async def button_start(self, a: MaybeButton['PaginatorInterface'], b: MaybeButton['PaginatorInterface']): # pylint: disable=unused-argument
526+
async def button_start_callback(self, interaction: discord.Interaction): # pylint: disable=unused-argument
492527
"""Button to send interface to first page"""
493528

494-
interaction, _ = button_either_arg(a, b)
495-
496529
self._display_page = 0
497-
self.update_view()
498530
await interaction.response.edit_message(**self.send_kwargs)
499531

500-
@ui.button(label="\N{BLACK LEFT-POINTING TRIANGLE}", style=discord.ButtonStyle.secondary)
501-
async def button_previous(self, a: MaybeButton['PaginatorInterface'], b: MaybeButton['PaginatorInterface']): # pylint: disable=unused-argument
502-
"""Button to send interface to previous page"""
532+
def button_start_label(self, _button: ui.Button[typing.Self]) -> str:
533+
return f"1 \u200b {self.emojis.start}"
503534

504-
interaction, _ = button_either_arg(a, b)
535+
async def button_previous_callback(self, interaction: discord.Interaction): # pylint: disable=unused-argument
536+
"""Button to send interface to previous page"""
505537

506538
self._display_page -= 1
507-
self.update_view()
508539
await interaction.response.edit_message(**self.send_kwargs)
509540

510-
@ui.button(label="1", style=discord.ButtonStyle.primary)
511-
async def button_current(self, a: MaybeButton['PaginatorInterface'], b: MaybeButton['PaginatorInterface']): # pylint: disable=unused-argument
512-
"""Button to refresh the interface"""
541+
def button_previous_label(self, _button: ui.Button[typing.Self]) -> str:
542+
return str(self.emojis.back)
513543

514-
interaction, _ = button_either_arg(a, b)
544+
async def button_current_callback(self, interaction: discord.Interaction): # pylint: disable=unused-argument
545+
"""Button to refresh the interface"""
515546

516-
self.update_view()
517547
await interaction.response.edit_message(**self.send_kwargs)
518548

519-
@ui.button(label="\N{BLACK RIGHT-POINTING TRIANGLE}", style=discord.ButtonStyle.secondary)
520-
async def button_next(self, a: MaybeButton['PaginatorInterface'], b: MaybeButton['PaginatorInterface']): # pylint: disable=unused-argument
521-
"""Button to send interface to next page"""
549+
def button_current_label(self, _button: ui.Button[typing.Self]) -> str:
550+
return str(self.display_page + 1)
522551

523-
interaction, _ = button_either_arg(a, b)
552+
async def button_next_callback(self, interaction: discord.Interaction): # pylint: disable=unused-argument
553+
"""Button to send interface to next page"""
524554

525555
self._display_page += 1
526-
self.update_view()
527556
await interaction.response.edit_message(**self.send_kwargs)
528557

529-
@ui.button(label="\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR} \u200b 1", style=discord.ButtonStyle.secondary)
530-
async def button_last(self, a: MaybeButton['PaginatorInterface'], b: MaybeButton['PaginatorInterface']): # pylint: disable=unused-argument
531-
"""Button to send interface to last page"""
558+
def button_next_label(self, _button: ui.Button[typing.Self]) -> str:
559+
return str(self.emojis.forward)
532560

533-
interaction, _ = button_either_arg(a, b)
561+
async def button_last_callback(self, interaction: discord.Interaction): # pylint: disable=unused-argument
562+
"""Button to send interface to last page"""
534563

535564
self._display_page = self.page_count - 1
536-
self.update_view()
537565
await interaction.response.edit_message(**self.send_kwargs)
538566

539-
if typing.TYPE_CHECKING or hasattr(ui, 'TextInput'):
540-
class PageChangeModal(ui.Modal, title="Go to page"):
541-
"""Modal that prompts users for the page number to change to"""
567+
def button_last_label(self, _button: ui.Button[typing.Self]) -> str:
568+
return f"{self.emojis.end} \u200b {self.page_count}"
542569

543-
page_number: ui.TextInput[ui.Modal] = ui.TextInput(label="Page number", style=discord.TextStyle.short)
570+
class PageChangeModal(ui.Modal, title="Go to page"):
571+
"""Modal that prompts users for the page number to change to"""
544572

545-
def __init__(self, interface: 'PaginatorInterface', *args: typing.Any, **kwargs: typing.Any):
546-
super().__init__(*args, timeout=interface.timeout_length, **kwargs)
547-
self.interface = interface
548-
self.page_number.label = f"Page number (1-{interface.page_count})"
549-
self.page_number.min_length = 1
550-
self.page_number.max_length = len(str(interface.page_count))
573+
page_number: ui.TextInput[ui.Modal] = ui.TextInput(label="Page number", style=discord.TextStyle.short)
551574

552-
async def on_submit(self, interaction: discord.Interaction, /):
553-
try:
554-
if not self.page_number.value:
555-
raise ValueError("Page number not filled")
556-
557-
self.interface.display_page = int(self.page_number.value) - 1
558-
except ValueError:
559-
await interaction.response.send_message(
560-
content=f"``{self.page_number.value}`` could not be converted to a page number",
561-
ephemeral=True
562-
)
563-
else:
564-
self.interface.update_view()
565-
await interaction.response.edit_message(**self.interface.send_kwargs)
575+
def __init__(self, interface: 'PaginatorInterface', *args: typing.Any, **kwargs: typing.Any):
576+
super().__init__(*args, timeout=interface.timeout_length, **kwargs)
577+
self.interface = interface
578+
self.page_number.label = f"Page number (1-{interface.page_count})"
579+
self.page_number.min_length = 1
580+
self.page_number.max_length = len(str(interface.page_count))
566581

567-
@ui.button(label="\N{RIGHTWARDS ARROW WITH HOOK} \u200b Go to page", style=discord.ButtonStyle.primary)
568-
async def button_goto(self, a: MaybeButton['PaginatorInterface'], b: MaybeButton['PaginatorInterface']): # pylint: disable=unused-argument
569-
"""Button to jump directly to a page"""
582+
async def on_submit(self, interaction: discord.Interaction, /):
583+
try:
584+
if not self.page_number.value:
585+
raise ValueError("Page number not filled")
586+
587+
self.interface.display_page = int(self.page_number.value) - 1
588+
except ValueError:
589+
await interaction.response.send_message(
590+
content=f"``{self.page_number.value}`` could not be converted to a page number",
591+
ephemeral=True
592+
)
593+
else:
594+
await interaction.response.edit_message(**self.interface.send_kwargs)
570595

571-
interaction, _ = button_either_arg(a, b)
596+
async def button_goto_callback(self, interaction: discord.Interaction): # pylint: disable=unused-argument
597+
"""Button to jump directly to a page"""
572598

573-
await interaction.response.send_modal(self.PageChangeModal(self))
599+
await interaction.response.send_modal(self.PageChangeModal(self))
574600

575-
@ui.button(label="\N{BLACK SQUARE FOR STOP} \u200b Close paginator", style=discord.ButtonStyle.danger)
576-
async def button_close(self, a: MaybeButton['PaginatorInterface'], b: MaybeButton['PaginatorInterface']): # pylint: disable=unused-argument
601+
def button_goto_label(self, _button: ui.Button[typing.Self]) -> str:
602+
return "\N{RIGHTWARDS ARROW WITH HOOK} \u200b Go to page"
603+
604+
async def button_close_callback(self, interaction: discord.Interaction): # pylint: disable=unused-argument
577605
"""Button to close the interface"""
578606

579607
message = self.message
@@ -584,6 +612,9 @@ async def button_close(self, a: MaybeButton['PaginatorInterface'], b: MaybeButto
584612
if message:
585613
await message.delete()
586614

615+
def button_close_label(self, _button: ui.Button[typing.Self]) -> str:
616+
return f"{self.emojis.close} \u200b Close paginator"
617+
587618

588619
class PaginatorEmbedInterface(PaginatorInterface):
589620
"""

0 commit comments

Comments
 (0)