6
6
import logging
7
7
import re
8
8
from collections import OrderedDict , namedtuple
9
+ from typing import List , Union
9
10
10
11
# Needed for the setup.py script
11
12
__version__ = '1.0.0'
12
13
13
14
# consistency with the `nextcord` namespaced logging
14
15
log = logging .getLogger (__name__ )
15
16
17
+ DEFAULT_TIMEOUT = 180.0
16
18
17
19
class MenuError (Exception ):
18
20
pass
@@ -311,7 +313,7 @@ class Menu(metaclass=_MenuMeta):
311
313
calling :meth:`send_initial_message`\, if for example you have a pre-existing
312
314
message you want to attach a menu to.
313
315
"""
314
- def __init__ (self , * , timeout = 180.0 , delete_message_after = False ,
316
+ def __init__ (self , * , timeout = DEFAULT_TIMEOUT , delete_message_after = False ,
315
317
clear_reactions_after = False , check_embeds = False , message = None ):
316
318
317
319
self .timeout = timeout
@@ -504,7 +506,15 @@ async def dummy():
504
506
505
507
def should_add_reactions (self ):
506
508
""":class:`bool`: Whether to add reactions to this menu session."""
507
- return len (self .buttons )
509
+ return len (self .buttons ) > 0
510
+
511
+ def should_add_buttons (self ):
512
+ """:class:`bool`: Whether to add button components to this menu session."""
513
+ return hasattr (self , 'children' ) and len (self .children ) > 0
514
+
515
+ def should_add_reactions_or_buttons (self ):
516
+ """:class:`bool`: Whether to add reactions or buttons to this menu session."""
517
+ return self .should_add_reactions () or self .should_add_buttons ()
508
518
509
519
def _verify_permissions (self , ctx , channel , permissions ):
510
520
if not permissions .send_messages :
@@ -698,7 +708,7 @@ async def start(self, ctx, *, channel=None, wait=False):
698
708
if msg is None :
699
709
self .message = msg = await self .send_initial_message (ctx , channel )
700
710
701
- if self .should_add_reactions ():
711
+ if self .should_add_reactions_or_buttons ():
702
712
# Start the task first so we can listen to reactions before doing anything
703
713
for task in self .__tasks :
704
714
task .cancel ()
@@ -761,6 +771,53 @@ def stop(self):
761
771
task .cancel ()
762
772
self .__tasks .clear ()
763
773
774
+ class ButtonMenu (Menu , nextcord .ui .View ):
775
+ r"""An interface that allows handling menus by using button interaction components.
776
+
777
+ Buttons should be marked with the :func:`nextcord.ui.button` decorator. Please note that
778
+ this expects the methods to have two parameters, the ``button`` and the ``interaction``.
779
+ The ``button`` is of type :class:`nextcord.ui.Button`.
780
+ The ``interaction`` is of type :class:`nextcord.Interaction`.
781
+ """
782
+ def __init__ (self , timeout = DEFAULT_TIMEOUT , * args , ** kwargs ):
783
+ Menu .__init__ (self , timeout = timeout , * args , ** kwargs )
784
+ nextcord .ui .View .__init__ (self , timeout = timeout )
785
+
786
+ async def _set_all_disabled (self , disable : bool ):
787
+ """|coro|
788
+
789
+ Enables or disable all :class:`nextcord.ui.Button` components in the menu.
790
+
791
+ Parameters
792
+ ------------
793
+ disable: :class:`bool`
794
+ Whether to disable or enable the buttons.
795
+ """
796
+ for child in self .children :
797
+ child .disabled = disable
798
+ await self .message .edit (view = self )
799
+
800
+ async def enable (self ):
801
+ """|coro|
802
+
803
+ Enables all :class:`nextcord.ui.Button` components in the menu.
804
+ """
805
+ await self ._set_all_disabled (False )
806
+
807
+ async def disable (self ):
808
+ """|coro|
809
+
810
+ Disables all :class:`nextcord.ui.Button` components in the menu.
811
+ """
812
+ await self ._set_all_disabled (True )
813
+
814
+ def stop (self ):
815
+ """Stops the internal loop and view interactions."""
816
+ # stop the menu loop
817
+ Menu .stop (self )
818
+ # stop view interactions
819
+ nextcord .ui .View .stop (self )
820
+
764
821
765
822
class PageSource :
766
823
"""An interface representing a menu page's data source for the actual menu page.
@@ -797,7 +854,7 @@ async def prepare(self):
797
854
return
798
855
799
856
def is_paginating (self ):
800
- """An abstract method that notifies the :class:`MenuPages ` whether or not
857
+ """An abstract method that notifies the :class:`MenuPagesBase ` whether or not
801
858
to start paginating. This signals whether to add reactions or not.
802
859
803
860
Subclasses must implement this.
@@ -883,15 +940,21 @@ async def format_page(self, menu, page):
883
940
raise NotImplementedError
884
941
885
942
886
- class MenuPages ( Menu ):
887
- """A special type of Menu dedicated to pagination.
943
+ class MenuPagesBase ( ButtonMenu ):
944
+ """A base class dedicated to pagination for reaction and button menus .
888
945
889
946
Attributes
890
947
------------
891
948
current_page: :class:`int`
892
949
The current page that we are in. Zero-indexed
893
950
between [0, :attr:`PageSource.max_pages`).
894
951
"""
952
+ FIRST_PAGE = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR} \ufe0f '
953
+ PREVIOUS_PAGE = '\N{BLACK LEFT-POINTING TRIANGLE} \ufe0f '
954
+ NEXT_PAGE = '\N{BLACK RIGHT-POINTING TRIANGLE} \ufe0f '
955
+ LAST_PAGE = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR} \ufe0f '
956
+ STOP = '\N{BLACK SQUARE FOR STOP} \ufe0f '
957
+
895
958
def __init__ (self , source , ** kwargs ):
896
959
self ._source = source
897
960
self .current_page = 0
@@ -954,6 +1017,8 @@ async def send_initial_message(self, ctx, channel):
954
1017
"""
955
1018
page = await self ._source .get_page (0 )
956
1019
kwargs = await self ._get_kwargs_from_page (page )
1020
+ if hasattr (self , '__discord_ui_view__' ):
1021
+ kwargs ['view' ] = self
957
1022
return await channel .send (** kwargs )
958
1023
959
1024
async def start (self , ctx , * , channel = None , wait = False ):
@@ -982,24 +1047,35 @@ def _skip_double_triangle_buttons(self):
982
1047
return True
983
1048
return max_pages <= 2
984
1049
985
- @button ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR} \ufe0f ' ,
986
- position = First (0 ), skip_if = _skip_double_triangle_buttons )
1050
+
1051
+ class MenuPages (MenuPagesBase ):
1052
+ """A special type of Menu dedicated to pagination with reactions.
1053
+
1054
+ Attributes
1055
+ ------------
1056
+ current_page: :class:`int`
1057
+ The current page that we are in. Zero-indexed
1058
+ between [0, :attr:`PageSource.max_pages`).
1059
+ """
1060
+ def __init__ (self , source , ** kwargs ):
1061
+ super ().__init__ (source , ** kwargs )
1062
+
1063
+ @button (MenuPagesBase .FIRST_PAGE , position = First (0 ), skip_if = MenuPagesBase ._skip_double_triangle_buttons )
987
1064
async def go_to_first_page (self , payload ):
988
1065
"""go to the first page"""
989
1066
await self .show_page (0 )
990
1067
991
- @button (' \N{BLACK LEFT-POINTING TRIANGLE} \ufe0f ' , position = First (1 ))
1068
+ @button (MenuPagesBase . PREVIOUS_PAGE , position = First (1 ))
992
1069
async def go_to_previous_page (self , payload ):
993
1070
"""go to the previous page"""
994
1071
await self .show_checked_page (self .current_page - 1 )
995
1072
996
- @button (' \N{BLACK RIGHT-POINTING TRIANGLE} \ufe0f ' , position = Last (0 ))
1073
+ @button (MenuPagesBase . NEXT_PAGE , position = Last (0 ))
997
1074
async def go_to_next_page (self , payload ):
998
1075
"""go to the next page"""
999
1076
await self .show_checked_page (self .current_page + 1 )
1000
1077
1001
- @button ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR} \ufe0f ' ,
1002
- position = Last (1 ), skip_if = _skip_double_triangle_buttons )
1078
+ @button (MenuPagesBase .LAST_PAGE , position = Last (1 ), skip_if = MenuPagesBase ._skip_double_triangle_buttons )
1003
1079
async def go_to_last_page (self , payload ):
1004
1080
"""go to the last page"""
1005
1081
# The call here is safe because it's guarded by skip_if
@@ -1011,6 +1087,80 @@ async def stop_pages(self, payload):
1011
1087
self .stop ()
1012
1088
1013
1089
1090
+ class MenuPaginationButton (nextcord .ui .Button ['MenuPaginationButton' ]):
1091
+ """
1092
+ A custom button for pagination that will be disabled when unavailable.
1093
+ """
1094
+ def __init__ (self , style : nextcord .ButtonStyle , emoji : Union [str , nextcord .Emoji , nextcord .PartialEmoji ]):
1095
+ super ().__init__ (style = style , emoji = emoji )
1096
+ self ._emoji = _cast_emoji (emoji )
1097
+
1098
+ async def callback (self , interaction : nextcord .Interaction ):
1099
+ """
1100
+ Callback for when this button is pressed
1101
+ """
1102
+ assert self .view is not None
1103
+ view : ButtonMenuPages = self .view
1104
+
1105
+ # change the current page
1106
+ if self ._emoji .name == view .FIRST_PAGE :
1107
+ await view .show_page (0 )
1108
+ elif self ._emoji .name == view .PREVIOUS_PAGE :
1109
+ await view .show_checked_page (view .current_page - 1 )
1110
+ elif self ._emoji .name == view .NEXT_PAGE :
1111
+ await view .show_checked_page (view .current_page + 1 )
1112
+ elif self ._emoji .name == view .LAST_PAGE :
1113
+ await view .show_page (view ._source .get_max_pages () - 1 )
1114
+
1115
+ # disable buttons that are unavailable
1116
+ view ._disable_unavailable_buttons ()
1117
+
1118
+ # disable all buttons if stop is pressed
1119
+ if self ._emoji .name == view .STOP :
1120
+ await view .disable ()
1121
+ view .stop ()
1122
+
1123
+ # update the view
1124
+ await interaction .response .edit_message (view = view )
1125
+
1126
+
1127
+ class ButtonMenuPages (MenuPagesBase ):
1128
+ """A special type of Menu dedicated to pagination with button components.
1129
+
1130
+ Parameters
1131
+ -----------
1132
+ style: :class:`nextcord.ui.ButtonStyle`
1133
+ The button style to use for the pagination buttons.
1134
+
1135
+ Attributes
1136
+ ------------
1137
+ current_page: :class:`int`
1138
+ The current page that we are in. Zero-indexed
1139
+ between [0, :attr:`PageSource.max_pages`).
1140
+ """
1141
+ def __init__ (self , source : PageSource , style : nextcord .ButtonStyle = nextcord .ButtonStyle .secondary , ** kwargs ):
1142
+ super ().__init__ (source , ** kwargs )
1143
+ # add buttons to the view
1144
+ for emoji in (self .FIRST_PAGE , self .PREVIOUS_PAGE , self .NEXT_PAGE , self .LAST_PAGE , self .STOP ):
1145
+ if emoji in (self .FIRST_PAGE , self .LAST_PAGE ) and self ._skip_double_triangle_buttons ():
1146
+ continue
1147
+ self .add_item (MenuPaginationButton (style = style , emoji = emoji ))
1148
+ self ._disable_unavailable_buttons ()
1149
+
1150
+ def _disable_unavailable_buttons (self ):
1151
+ """
1152
+ Disables buttons that are unavailable to be pressed.
1153
+ """
1154
+ buttons : List [MenuPaginationButton ] = self .children
1155
+ max_pages = self ._source .get_max_pages ()
1156
+ for button in buttons :
1157
+ if button .emoji .name in (self .FIRST_PAGE , self .PREVIOUS_PAGE ):
1158
+ button .disabled = self .current_page == 0
1159
+ elif max_pages and button .emoji .name in (self .LAST_PAGE , self .NEXT_PAGE ):
1160
+ button .disabled = self .current_page == max_pages - 1
1161
+
1162
+
1163
+
1014
1164
class ListPageSource (PageSource ):
1015
1165
"""A data source for a sequence of items.
1016
1166
0 commit comments