committed by
GitHub
34 changed files with 4429 additions and 226 deletions
@ -0,0 +1,599 @@ |
|||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-present Rapptz |
||||
|
|
||||
|
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 __future__ import annotations |
||||
|
|
||||
|
import sys |
||||
|
from itertools import groupby |
||||
|
from typing import ( |
||||
|
TYPE_CHECKING, |
||||
|
Any, |
||||
|
Callable, |
||||
|
ClassVar, |
||||
|
Coroutine, |
||||
|
Dict, |
||||
|
Generator, |
||||
|
List, |
||||
|
Literal, |
||||
|
Optional, |
||||
|
Sequence, |
||||
|
Type, |
||||
|
TypeVar, |
||||
|
Union, |
||||
|
overload, |
||||
|
) |
||||
|
|
||||
|
from .item import Item, ItemCallbackType |
||||
|
from .button import Button, button as _button |
||||
|
from .dynamic import DynamicItem |
||||
|
from .select import select as _select, Select, UserSelect, RoleSelect, ChannelSelect, MentionableSelect |
||||
|
from ..components import ActionRow as ActionRowComponent |
||||
|
from ..enums import ButtonStyle, ComponentType, ChannelType |
||||
|
from ..partial_emoji import PartialEmoji |
||||
|
from ..utils import MISSING, get as _utils_get |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from typing_extensions import Self |
||||
|
|
||||
|
from .view import LayoutView |
||||
|
from .select import ( |
||||
|
BaseSelectT, |
||||
|
ValidDefaultValues, |
||||
|
MentionableSelectT, |
||||
|
ChannelSelectT, |
||||
|
RoleSelectT, |
||||
|
UserSelectT, |
||||
|
SelectT, |
||||
|
SelectCallbackDecorator, |
||||
|
) |
||||
|
from ..emoji import Emoji |
||||
|
from ..components import SelectOption |
||||
|
from ..interactions import Interaction |
||||
|
|
||||
|
V = TypeVar('V', bound='LayoutView', covariant=True) |
||||
|
|
||||
|
__all__ = ('ActionRow',) |
||||
|
|
||||
|
|
||||
|
class _ActionRowCallback: |
||||
|
__slots__ = ('row', 'callback', 'item') |
||||
|
|
||||
|
def __init__(self, callback: ItemCallbackType[Any], row: ActionRow, item: Item[Any]) -> None: |
||||
|
self.callback: ItemCallbackType[Any] = callback |
||||
|
self.row: ActionRow = row |
||||
|
self.item: Item[Any] = item |
||||
|
|
||||
|
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: |
||||
|
return self.callback(self.row, interaction, self.item) |
||||
|
|
||||
|
|
||||
|
class ActionRow(Item[V]): |
||||
|
"""Represents a UI action row. |
||||
|
|
||||
|
This is a top-level layout component that can only be used on :class:`LayoutView` |
||||
|
and can contain :class:`Button` 's and :class:`Select` 's in it. |
||||
|
|
||||
|
This can be inherited. |
||||
|
|
||||
|
.. note:: |
||||
|
|
||||
|
Action rows can contain up to 5 components, which is, 5 buttons or 1 select. |
||||
|
|
||||
|
.. versionadded:: 2.6 |
||||
|
|
||||
|
Examples |
||||
|
-------- |
||||
|
|
||||
|
.. code-block:: python3 |
||||
|
|
||||
|
import discord |
||||
|
from discord import ui |
||||
|
|
||||
|
# you can subclass it and add components with the decorators |
||||
|
class MyActionRow(ui.ActionRow): |
||||
|
@ui.button(label='Click Me!') |
||||
|
async def click_me(self, interaction: discord.Interaction, button: discord.ui.Button): |
||||
|
await interaction.response.send_message('You clicked me!') |
||||
|
|
||||
|
# or use it directly on LayoutView |
||||
|
class MyView(ui.LayoutView): |
||||
|
row = ui.ActionRow() |
||||
|
# or you can use your subclass: |
||||
|
# row = MyActionRow() |
||||
|
|
||||
|
# you can create items with row.button and row.select |
||||
|
@row.button(label='A button!') |
||||
|
async def row_button(self, interaction: discord.Interaction, button: discord.ui.Button): |
||||
|
await interaction.response.send_message('You clicked a button!') |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
*children: :class:`Item` |
||||
|
The initial children of this action row. |
||||
|
row: Optional[:class:`int`] |
||||
|
The relative row this action row belongs to. By default |
||||
|
items are arranged automatically into those rows. If you'd |
||||
|
like to control the relative positioning of the row then |
||||
|
passing an index is advised. For example, row=1 will show |
||||
|
up before row=2. Defaults to ``None``, which is automatic |
||||
|
ordering. The row number must be between 0 and 39 (i.e. zero indexed) |
||||
|
id: Optional[:class:`int`] |
||||
|
The ID of this component. This must be unique across the view. |
||||
|
""" |
||||
|
|
||||
|
__action_row_children_items__: ClassVar[List[ItemCallbackType[Any]]] = [] |
||||
|
__discord_ui_action_row__: ClassVar[bool] = True |
||||
|
__discord_ui_update_view__: ClassVar[bool] = True |
||||
|
|
||||
|
def __init__( |
||||
|
self, |
||||
|
*children: Item[V], |
||||
|
row: Optional[int] = None, |
||||
|
id: Optional[int] = None, |
||||
|
) -> None: |
||||
|
super().__init__() |
||||
|
self._weight: int = 0 |
||||
|
self._children: List[Item[V]] = self._init_children() |
||||
|
self._children.extend(children) |
||||
|
self._weight += sum(i.width for i in children) |
||||
|
|
||||
|
if self._weight > 5: |
||||
|
raise ValueError('maximum number of children exceeded') |
||||
|
|
||||
|
self.id = id |
||||
|
self.row = row |
||||
|
|
||||
|
def __init_subclass__(cls) -> None: |
||||
|
super().__init_subclass__() |
||||
|
|
||||
|
children: Dict[str, ItemCallbackType[Any]] = {} |
||||
|
for base in reversed(cls.__mro__): |
||||
|
for name, member in base.__dict__.items(): |
||||
|
if hasattr(member, '__discord_ui_model_type__'): |
||||
|
children[name] = member |
||||
|
|
||||
|
if len(children) > 5: |
||||
|
raise TypeError('ActionRow cannot have more than 5 children') |
||||
|
|
||||
|
cls.__action_row_children_items__ = list(children.values()) |
||||
|
|
||||
|
def _init_children(self) -> List[Item[Any]]: |
||||
|
children = [] |
||||
|
|
||||
|
for func in self.__action_row_children_items__: |
||||
|
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) |
||||
|
item.callback = _ActionRowCallback(func, self, item) # type: ignore |
||||
|
item._parent = getattr(func, '__discord_ui_parent__', self) |
||||
|
setattr(self, func.__name__, item) |
||||
|
self._weight += item.width |
||||
|
children.append(item) |
||||
|
return children |
||||
|
|
||||
|
def _update_store_data(self, dispatch_info: Dict, dynamic_items: Dict) -> bool: |
||||
|
is_fully_dynamic = True |
||||
|
|
||||
|
for item in self._children: |
||||
|
if isinstance(item, DynamicItem): |
||||
|
pattern = item.__discord_ui_compiled_template__ |
||||
|
dynamic_items[pattern] = item.__class__ |
||||
|
elif item.is_dispatchable(): |
||||
|
dispatch_info[(item.type.value, item.custom_id)] = item |
||||
|
is_fully_dynamic = False |
||||
|
return is_fully_dynamic |
||||
|
|
||||
|
def is_dispatchable(self) -> bool: |
||||
|
return any(c.is_dispatchable() for c in self.children) |
||||
|
|
||||
|
def is_persistent(self) -> bool: |
||||
|
return all(c.is_persistent() for c in self.children) |
||||
|
|
||||
|
def _update_children_view(self, view: LayoutView) -> None: |
||||
|
for child in self._children: |
||||
|
child._view = view # pyright: ignore[reportAttributeAccessIssue] |
||||
|
|
||||
|
def _is_v2(self) -> bool: |
||||
|
# although it is not really a v2 component the only usecase here is for |
||||
|
# LayoutView which basically represents the top-level payload of components |
||||
|
# and ActionRow is only allowed there anyways. |
||||
|
# If the user tries to add any V2 component to a View instead of LayoutView |
||||
|
# it should error anyways. |
||||
|
return True |
||||
|
|
||||
|
@property |
||||
|
def width(self): |
||||
|
return 5 |
||||
|
|
||||
|
@property |
||||
|
def type(self) -> Literal[ComponentType.action_row]: |
||||
|
return ComponentType.action_row |
||||
|
|
||||
|
@property |
||||
|
def children(self) -> List[Item[V]]: |
||||
|
"""List[:class:`Item`]: The list of children attached to this action row.""" |
||||
|
return self._children.copy() |
||||
|
|
||||
|
def walk_children(self) -> Generator[Item[V], Any, None]: |
||||
|
"""An iterator that recursively walks through all the children of this view |
||||
|
and it's children, if applicable. |
||||
|
|
||||
|
Yields |
||||
|
------ |
||||
|
:class:`Item` |
||||
|
An item in the action row. |
||||
|
""" |
||||
|
|
||||
|
for child in self.children: |
||||
|
yield child |
||||
|
|
||||
|
def add_item(self, item: Item[Any]) -> Self: |
||||
|
"""Adds an item to this row. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
item: :class:`Item` |
||||
|
The item to add to the row. |
||||
|
|
||||
|
Raises |
||||
|
------ |
||||
|
TypeError |
||||
|
An :class:`Item` was not passed. |
||||
|
ValueError |
||||
|
Maximum number of children has been exceeded (5). |
||||
|
""" |
||||
|
|
||||
|
if len(self._children) >= 5: |
||||
|
raise ValueError('maximum number of children exceeded') |
||||
|
|
||||
|
if not isinstance(item, Item): |
||||
|
raise TypeError(f'expected Item not {item.__class__.__name__}') |
||||
|
|
||||
|
item._view = self._view |
||||
|
item._parent = self |
||||
|
self._children.append(item) |
||||
|
|
||||
|
if self._view and getattr(self._view, '__discord_ui_layout_view__', False): |
||||
|
self._view._total_children += 1 |
||||
|
|
||||
|
if item.is_dispatchable() and self._parent and getattr(self._parent, '__discord_ui_container__', False): |
||||
|
self._parent._add_dispatchable(item) # type: ignore |
||||
|
|
||||
|
return self |
||||
|
|
||||
|
def remove_item(self, item: Item[Any]) -> Self: |
||||
|
"""Removes an item from the row. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
item: :class:`Item` |
||||
|
The item to remove from the view. |
||||
|
""" |
||||
|
|
||||
|
try: |
||||
|
self._children.remove(item) |
||||
|
except ValueError: |
||||
|
pass |
||||
|
else: |
||||
|
if self._view and getattr(self._view, '__discord_ui_layout_view__', False): |
||||
|
self._view._total_children -= 1 |
||||
|
|
||||
|
return self |
||||
|
|
||||
|
def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: |
||||
|
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if |
||||
|
not found. |
||||
|
|
||||
|
.. warning:: |
||||
|
|
||||
|
This is **not the same** as ``custom_id``. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
id: :class:`int` |
||||
|
The ID of the component. |
||||
|
|
||||
|
Returns |
||||
|
------- |
||||
|
Optional[:class:`Item`] |
||||
|
The item found, or ``None``. |
||||
|
""" |
||||
|
return _utils_get(self._children, id=id) |
||||
|
|
||||
|
def clear_items(self) -> Self: |
||||
|
"""Removes all items from the row. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
""" |
||||
|
if self._view and getattr(self._view, '__discord_ui_layout_view__', False): |
||||
|
self._view._total_children -= len(self._children) |
||||
|
self._children.clear() |
||||
|
return self |
||||
|
|
||||
|
def to_component_dict(self) -> Dict[str, Any]: |
||||
|
components = [] |
||||
|
|
||||
|
def key(item: Item) -> int: |
||||
|
if item._rendered_row is not None: |
||||
|
return item._rendered_row |
||||
|
if item._row is not None: |
||||
|
return item._row |
||||
|
return sys.maxsize |
||||
|
|
||||
|
for _, cmps in groupby(self._children, key=key): |
||||
|
components.extend(c.to_component_dict() for c in cmps) |
||||
|
|
||||
|
base = { |
||||
|
'type': self.type.value, |
||||
|
'components': components, |
||||
|
} |
||||
|
if self.id is not None: |
||||
|
base['id'] = self.id |
||||
|
return base |
||||
|
|
||||
|
def button( |
||||
|
self, |
||||
|
*, |
||||
|
label: Optional[str] = None, |
||||
|
custom_id: Optional[str] = None, |
||||
|
disabled: bool = False, |
||||
|
style: ButtonStyle = ButtonStyle.secondary, |
||||
|
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, |
||||
|
) -> Callable[[ItemCallbackType[Button[V]]], Button[V]]: |
||||
|
"""A decorator that attaches a button to a component. |
||||
|
|
||||
|
The function being decorated should have three parameters, ``self`` representing |
||||
|
the :class:`discord.ui.LayoutView`, the :class:`discord.Interaction` you receive and |
||||
|
the :class:`discord.ui.Button` being pressed. |
||||
|
|
||||
|
.. note:: |
||||
|
|
||||
|
Buttons with a URL or a SKU cannot be created with this function. |
||||
|
Consider creating a :class:`Button` manually and adding it via |
||||
|
:meth:`ActionRow.add_item` instead. This is beacuse these buttons |
||||
|
cannot have a callback associated with them since Discord does not |
||||
|
do any processing with them. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
label: Optional[:class:`str`] |
||||
|
The label of the button, if any. |
||||
|
Can only be up to 80 characters. |
||||
|
custom_id: Optional[:class:`str`] |
||||
|
The ID of the button that gets received during an interaction. |
||||
|
It is recommended to not set this parameters to prevent conflicts. |
||||
|
Can only be up to 100 characters. |
||||
|
style: :class:`.ButtonStyle` |
||||
|
The style of the button. Defaults to :attr:`.ButtonStyle.grey`. |
||||
|
disabled: :class:`bool` |
||||
|
Whether the button is disabled or not. Defaults to ``False``. |
||||
|
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]] |
||||
|
The emoji of the button. This can be in string form or a :class:`.PartialEmoji` |
||||
|
or a full :class:`.Emoji`. |
||||
|
""" |
||||
|
|
||||
|
def decorator(func: ItemCallbackType[Button[V]]) -> ItemCallbackType[Button[V]]: |
||||
|
ret = _button( |
||||
|
label=label, |
||||
|
custom_id=custom_id, |
||||
|
disabled=disabled, |
||||
|
style=style, |
||||
|
emoji=emoji, |
||||
|
row=None, |
||||
|
)(func) |
||||
|
ret.__discord_ui_parent__ = self # type: ignore |
||||
|
return ret # type: ignore |
||||
|
|
||||
|
return decorator # type: ignore |
||||
|
|
||||
|
@overload |
||||
|
def select( |
||||
|
self, |
||||
|
*, |
||||
|
cls: Type[SelectT] = Select[Any], |
||||
|
options: List[SelectOption] = MISSING, |
||||
|
channel_types: List[ChannelType] = ..., |
||||
|
placeholder: Optional[str] = ..., |
||||
|
custom_id: str = ..., |
||||
|
min_values: int = ..., |
||||
|
max_values: int = ..., |
||||
|
disabled: bool = ..., |
||||
|
) -> SelectCallbackDecorator[SelectT]: |
||||
|
... |
||||
|
|
||||
|
@overload |
||||
|
def select( |
||||
|
self, |
||||
|
*, |
||||
|
cls: Type[UserSelectT] = UserSelect[Any], |
||||
|
options: List[SelectOption] = MISSING, |
||||
|
channel_types: List[ChannelType] = ..., |
||||
|
placeholder: Optional[str] = ..., |
||||
|
custom_id: str = ..., |
||||
|
min_values: int = ..., |
||||
|
max_values: int = ..., |
||||
|
disabled: bool = ..., |
||||
|
default_values: Sequence[ValidDefaultValues] = ..., |
||||
|
) -> SelectCallbackDecorator[UserSelectT]: |
||||
|
... |
||||
|
|
||||
|
@overload |
||||
|
def select( |
||||
|
self, |
||||
|
*, |
||||
|
cls: Type[RoleSelectT] = RoleSelect[Any], |
||||
|
options: List[SelectOption] = MISSING, |
||||
|
channel_types: List[ChannelType] = ..., |
||||
|
placeholder: Optional[str] = ..., |
||||
|
custom_id: str = ..., |
||||
|
min_values: int = ..., |
||||
|
max_values: int = ..., |
||||
|
disabled: bool = ..., |
||||
|
default_values: Sequence[ValidDefaultValues] = ..., |
||||
|
) -> SelectCallbackDecorator[RoleSelectT]: |
||||
|
... |
||||
|
|
||||
|
@overload |
||||
|
def select( |
||||
|
self, |
||||
|
*, |
||||
|
cls: Type[ChannelSelectT] = ChannelSelect[Any], |
||||
|
options: List[SelectOption] = MISSING, |
||||
|
channel_types: List[ChannelType] = ..., |
||||
|
placeholder: Optional[str] = ..., |
||||
|
custom_id: str = ..., |
||||
|
min_values: int = ..., |
||||
|
max_values: int = ..., |
||||
|
disabled: bool = ..., |
||||
|
default_values: Sequence[ValidDefaultValues] = ..., |
||||
|
) -> SelectCallbackDecorator[ChannelSelectT]: |
||||
|
... |
||||
|
|
||||
|
@overload |
||||
|
def select( |
||||
|
self, |
||||
|
*, |
||||
|
cls: Type[MentionableSelectT] = MentionableSelect[Any], |
||||
|
options: List[SelectOption] = MISSING, |
||||
|
channel_types: List[ChannelType] = MISSING, |
||||
|
placeholder: Optional[str] = ..., |
||||
|
custom_id: str = ..., |
||||
|
min_values: int = ..., |
||||
|
max_values: int = ..., |
||||
|
disabled: bool = ..., |
||||
|
default_values: Sequence[ValidDefaultValues] = ..., |
||||
|
) -> SelectCallbackDecorator[MentionableSelectT]: |
||||
|
... |
||||
|
|
||||
|
def select( |
||||
|
self, |
||||
|
*, |
||||
|
cls: Type[BaseSelectT] = Select[Any], |
||||
|
options: List[SelectOption] = MISSING, |
||||
|
channel_types: List[ChannelType] = MISSING, |
||||
|
placeholder: Optional[str] = None, |
||||
|
custom_id: str = MISSING, |
||||
|
min_values: int = 1, |
||||
|
max_values: int = 1, |
||||
|
disabled: bool = False, |
||||
|
default_values: Sequence[ValidDefaultValues] = MISSING, |
||||
|
) -> SelectCallbackDecorator[BaseSelectT]: |
||||
|
"""A decorator that attaches a select menu to a component. |
||||
|
|
||||
|
The function being decorated should have three parameters, ``self`` representing |
||||
|
the :class:`discord.ui.LayoutView`, the :class:`discord.Interaction` you receive and |
||||
|
the chosen select class. |
||||
|
|
||||
|
To obtain the selected values inside the callback, you can use the ``values`` attribute of the chosen class in the callback. The list of values |
||||
|
will depend on the type of select menu used. View the table below for more information. |
||||
|
|
||||
|
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ |
||||
|
| Select Type | Resolved Values | |
||||
|
+========================================+=================================================================================================================+ |
||||
|
| :class:`discord.ui.Select` | List[:class:`str`] | |
||||
|
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ |
||||
|
| :class:`discord.ui.UserSelect` | List[Union[:class:`discord.Member`, :class:`discord.User`]] | |
||||
|
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ |
||||
|
| :class:`discord.ui.RoleSelect` | List[:class:`discord.Role`] | |
||||
|
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ |
||||
|
| :class:`discord.ui.MentionableSelect` | List[Union[:class:`discord.Role`, :class:`discord.Member`, :class:`discord.User`]] | |
||||
|
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ |
||||
|
| :class:`discord.ui.ChannelSelect` | List[Union[:class:`~discord.app_commands.AppCommandChannel`, :class:`~discord.app_commands.AppCommandThread`]] | |
||||
|
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ |
||||
|
|
||||
|
Example |
||||
|
--------- |
||||
|
.. code-block:: python3 |
||||
|
|
||||
|
class ActionRow(discord.ui.ActionRow): |
||||
|
|
||||
|
@discord.ui.select(cls=ChannelSelect, channel_types=[discord.ChannelType.text]) |
||||
|
async def select_channels(self, interaction: discord.Interaction, select: ChannelSelect): |
||||
|
return await interaction.response.send_message(f'You selected {select.values[0].mention}') |
||||
|
|
||||
|
Parameters |
||||
|
------------ |
||||
|
cls: Union[Type[:class:`discord.ui.Select`], Type[:class:`discord.ui.UserSelect`], Type[:class:`discord.ui.RoleSelect`], \ |
||||
|
Type[:class:`discord.ui.MentionableSelect`], Type[:class:`discord.ui.ChannelSelect`]] |
||||
|
The class to use for the select menu. Defaults to :class:`discord.ui.Select`. You can use other |
||||
|
select types to display different select menus to the user. See the table above for the different |
||||
|
values you can get from each select type. Subclasses work as well, however the callback in the subclass will |
||||
|
get overridden. |
||||
|
placeholder: Optional[:class:`str`] |
||||
|
The placeholder text that is shown if nothing is selected, if any. |
||||
|
Can only be up to 150 characters. |
||||
|
custom_id: :class:`str` |
||||
|
The ID of the select menu that gets received during an interaction. |
||||
|
It is recommended not to set this parameter to prevent conflicts. |
||||
|
Can only be up to 100 characters. |
||||
|
min_values: :class:`int` |
||||
|
The minimum number of items that must be chosen for this select menu. |
||||
|
Defaults to 1 and must be between 0 and 25. |
||||
|
max_values: :class:`int` |
||||
|
The maximum number of items that must be chosen for this select menu. |
||||
|
Defaults to 1 and must be between 1 and 25. |
||||
|
options: List[:class:`discord.SelectOption`] |
||||
|
A list of options that can be selected in this menu. This can only be used with |
||||
|
:class:`Select` instances. |
||||
|
Can only contain up to 25 items. |
||||
|
channel_types: List[:class:`~discord.ChannelType`] |
||||
|
The types of channels to show in the select menu. Defaults to all channels. This can only be used |
||||
|
with :class:`ChannelSelect` instances. |
||||
|
disabled: :class:`bool` |
||||
|
Whether the select is disabled or not. Defaults to ``False``. |
||||
|
default_values: Sequence[:class:`~discord.abc.Snowflake`] |
||||
|
A list of objects representing the default values for the select menu. This cannot be used with regular :class:`Select` instances. |
||||
|
If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor. |
||||
|
Number of items must be in range of ``min_values`` and ``max_values``. |
||||
|
""" |
||||
|
|
||||
|
def decorator(func: ItemCallbackType[BaseSelectT]) -> ItemCallbackType[BaseSelectT]: |
||||
|
r = _select( # type: ignore |
||||
|
cls=cls, # type: ignore |
||||
|
placeholder=placeholder, |
||||
|
custom_id=custom_id, |
||||
|
min_values=min_values, |
||||
|
max_values=max_values, |
||||
|
options=options, |
||||
|
channel_types=channel_types, |
||||
|
disabled=disabled, |
||||
|
default_values=default_values, |
||||
|
)(func) |
||||
|
r.__discord_ui_parent__ = self |
||||
|
return r |
||||
|
|
||||
|
return decorator # type: ignore |
||||
|
|
||||
|
@classmethod |
||||
|
def from_component(cls, component: ActionRowComponent) -> ActionRow: |
||||
|
from .view import _component_to_item |
||||
|
|
||||
|
self = cls() |
||||
|
for cmp in component.children: |
||||
|
self.add_item(_component_to_item(cmp)) |
||||
|
return self |
@ -0,0 +1,449 @@ |
|||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-present Rapptz |
||||
|
|
||||
|
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 __future__ import annotations |
||||
|
|
||||
|
import copy |
||||
|
import os |
||||
|
import sys |
||||
|
from typing import ( |
||||
|
TYPE_CHECKING, |
||||
|
Any, |
||||
|
ClassVar, |
||||
|
Coroutine, |
||||
|
Dict, |
||||
|
Generator, |
||||
|
List, |
||||
|
Literal, |
||||
|
Optional, |
||||
|
Tuple, |
||||
|
Type, |
||||
|
TypeVar, |
||||
|
Union, |
||||
|
) |
||||
|
|
||||
|
from .item import Item, ItemCallbackType |
||||
|
from .view import _component_to_item, LayoutView |
||||
|
from .dynamic import DynamicItem |
||||
|
from ..enums import ComponentType |
||||
|
from ..utils import MISSING, get as _utils_get |
||||
|
from ..colour import Colour, Color |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from typing_extensions import Self |
||||
|
|
||||
|
from ..components import Container as ContainerComponent |
||||
|
from ..interactions import Interaction |
||||
|
|
||||
|
V = TypeVar('V', bound='LayoutView', covariant=True) |
||||
|
|
||||
|
__all__ = ('Container',) |
||||
|
|
||||
|
|
||||
|
class _ContainerCallback: |
||||
|
__slots__ = ('container', 'callback', 'item') |
||||
|
|
||||
|
def __init__(self, callback: ItemCallbackType[Any], container: Container, item: Item[Any]) -> None: |
||||
|
self.callback: ItemCallbackType[Any] = callback |
||||
|
self.container: Container = container |
||||
|
self.item: Item[Any] = item |
||||
|
|
||||
|
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: |
||||
|
return self.callback(self.container, interaction, self.item) |
||||
|
|
||||
|
|
||||
|
class Container(Item[V]): |
||||
|
"""Represents a UI container. |
||||
|
|
||||
|
This is a top-level layout component that can only be used on :class:`LayoutView` |
||||
|
and can contain :class:`ActionRow` 's, :class:`TextDisplay` 's, :class:`Section` 's, |
||||
|
:class:`MediaGallery` 's, and :class:`File` 's in it. |
||||
|
|
||||
|
This can be inherited. |
||||
|
|
||||
|
|
||||
|
.. versionadded:: 2.6 |
||||
|
|
||||
|
Examples |
||||
|
-------- |
||||
|
|
||||
|
.. code-block:: python3 |
||||
|
|
||||
|
import discord |
||||
|
from discord import ui |
||||
|
|
||||
|
# you can subclass it and add components as you would add them |
||||
|
# in a LayoutView |
||||
|
class MyContainer(ui.Container): |
||||
|
action_row = ui.ActionRow() |
||||
|
|
||||
|
@action_row.button(label='A button in a container!') |
||||
|
async def a_button(self, interaction: discord.Interaction, button: discord.ui.Button): |
||||
|
await interaction.response.send_message('You clicked a button!') |
||||
|
|
||||
|
# or use it directly on LayoutView |
||||
|
class MyView(ui.LayoutView): |
||||
|
container = ui.Container(ui.TextDisplay('I am a text display on a container!')) |
||||
|
# or you can use your subclass: |
||||
|
# container = MyContainer() |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
*children: :class:`Item` |
||||
|
The initial children of this container. |
||||
|
accent_colour: Optional[Union[:class:`.Colour`, :class:`int`]] |
||||
|
The colour of the container. Defaults to ``None``. |
||||
|
accent_color: Optional[Union[:class:`.Colour`, :class:`int`]] |
||||
|
The color of the container. Defaults to ``None``. |
||||
|
spoiler: :class:`bool` |
||||
|
Whether to flag this container as a spoiler. Defaults |
||||
|
to ``False``. |
||||
|
row: Optional[:class:`int`] |
||||
|
The relative row this container belongs to. By default |
||||
|
items are arranged automatically into those rows. If you'd |
||||
|
like to control the relative positioning of the row then |
||||
|
passing an index is advised. For example, row=1 will show |
||||
|
up before row=2. Defaults to ``None``, which is automatic |
||||
|
ordering. The row number must be between 0 and 39 (i.e. zero indexed) |
||||
|
id: Optional[:class:`int`] |
||||
|
The ID of this component. This must be unique across the view. |
||||
|
""" |
||||
|
|
||||
|
__container_children_items__: ClassVar[Dict[str, Union[ItemCallbackType[Any], Item[Any]]]] = {} |
||||
|
__discord_ui_update_view__: ClassVar[bool] = True |
||||
|
__discord_ui_container__: ClassVar[bool] = True |
||||
|
|
||||
|
def __init__( |
||||
|
self, |
||||
|
*children: Item[V], |
||||
|
accent_colour: Optional[Union[Colour, int]] = None, |
||||
|
accent_color: Optional[Union[Color, int]] = None, |
||||
|
spoiler: bool = False, |
||||
|
row: Optional[int] = None, |
||||
|
id: Optional[int] = None, |
||||
|
) -> None: |
||||
|
super().__init__() |
||||
|
self.__dispatchable: List[Item[V]] = [] |
||||
|
self._children: List[Item[V]] = self._init_children() |
||||
|
|
||||
|
if children is not MISSING: |
||||
|
for child in children: |
||||
|
self.add_item(child) |
||||
|
|
||||
|
self.spoiler: bool = spoiler |
||||
|
self._colour = accent_colour if accent_colour is not None else accent_color |
||||
|
|
||||
|
self.row = row |
||||
|
self.id = id |
||||
|
|
||||
|
def _add_dispatchable(self, item: Item[Any]) -> None: |
||||
|
self.__dispatchable.append(item) |
||||
|
|
||||
|
def _remove_dispatchable(self, item: Item[Any]) -> None: |
||||
|
try: |
||||
|
self.__dispatchable.remove(item) |
||||
|
except ValueError: |
||||
|
pass |
||||
|
|
||||
|
def _init_children(self) -> List[Item[Any]]: |
||||
|
children = [] |
||||
|
parents = {} |
||||
|
|
||||
|
for name, raw in self.__container_children_items__.items(): |
||||
|
if isinstance(raw, Item): |
||||
|
item = copy.deepcopy(raw) |
||||
|
item._parent = self |
||||
|
if getattr(item, '__discord_ui_action_row__', False): |
||||
|
if item.is_dispatchable(): |
||||
|
self.__dispatchable.extend(item._children) # type: ignore |
||||
|
if getattr(item, '__discord_ui_section__', False): |
||||
|
if item.accessory.is_dispatchable(): # type: ignore |
||||
|
if item.accessory._provided_custom_id is False: # type: ignore |
||||
|
item.accessory.custom_id = os.urandom(16).hex() # type: ignore |
||||
|
self.__dispatchable.append(item.accessory) # type: ignore |
||||
|
|
||||
|
setattr(self, name, item) |
||||
|
children.append(item) |
||||
|
|
||||
|
parents[raw] = item |
||||
|
else: |
||||
|
# action rows can be created inside containers, and then callbacks can exist here |
||||
|
# so we create items based off them |
||||
|
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__) |
||||
|
item.callback = _ContainerCallback(raw, self, item) # type: ignore |
||||
|
setattr(self, raw.__name__, item) |
||||
|
# this should not fail because in order for a function to be here it should be from |
||||
|
# an action row and must have passed the check in __init_subclass__, but still |
||||
|
# guarding it |
||||
|
parent = getattr(raw, '__discord_ui_parent__', None) |
||||
|
if parent is None: |
||||
|
raise RuntimeError(f'{raw.__name__} is not a valid item for a Container') |
||||
|
parents.get(parent, parent)._children.append(item) |
||||
|
# we donnot append it to the children list because technically these buttons and |
||||
|
# selects are not from the container but the action row itself. |
||||
|
self.__dispatchable.append(item) |
||||
|
|
||||
|
return children |
||||
|
|
||||
|
def is_dispatchable(self) -> bool: |
||||
|
return bool(self.__dispatchable) |
||||
|
|
||||
|
def is_persistent(self) -> bool: |
||||
|
return all(c.is_persistent() for c in self.children) |
||||
|
|
||||
|
def __init_subclass__(cls) -> None: |
||||
|
super().__init_subclass__() |
||||
|
|
||||
|
children: Dict[str, Union[ItemCallbackType[Any], Item[Any]]] = {} |
||||
|
for base in reversed(cls.__mro__): |
||||
|
for name, member in base.__dict__.items(): |
||||
|
if isinstance(member, Item): |
||||
|
children[name] = member |
||||
|
if hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): |
||||
|
children[name] = copy.copy(member) |
||||
|
|
||||
|
cls.__container_children_items__ = children |
||||
|
|
||||
|
def _update_children_view(self, view) -> None: |
||||
|
for child in self._children: |
||||
|
child._view = view |
||||
|
if getattr(child, '__discord_ui_update_view__', False): |
||||
|
# if the item is an action row which child's view can be updated, then update it |
||||
|
child._update_children_view(view) # type: ignore |
||||
|
|
||||
|
@property |
||||
|
def children(self) -> List[Item[V]]: |
||||
|
"""List[:class:`Item`]: The children of this container.""" |
||||
|
return self._children.copy() |
||||
|
|
||||
|
@children.setter |
||||
|
def children(self, value: List[Item[V]]) -> None: |
||||
|
self._children = value |
||||
|
|
||||
|
@property |
||||
|
def accent_colour(self) -> Optional[Union[Colour, int]]: |
||||
|
"""Optional[Union[:class:`discord.Colour`, :class:`int`]]: The colour of the container, or ``None``.""" |
||||
|
return self._colour |
||||
|
|
||||
|
@accent_colour.setter |
||||
|
def accent_colour(self, value: Optional[Union[Colour, int]]) -> None: |
||||
|
if not isinstance(value, (int, Colour)): |
||||
|
raise TypeError(f'expected an int, or Colour, not {value.__class__.__name__!r}') |
||||
|
|
||||
|
self._colour = value |
||||
|
|
||||
|
accent_color = accent_colour |
||||
|
|
||||
|
@property |
||||
|
def type(self) -> Literal[ComponentType.container]: |
||||
|
return ComponentType.container |
||||
|
|
||||
|
@property |
||||
|
def width(self): |
||||
|
return 5 |
||||
|
|
||||
|
def _is_v2(self) -> bool: |
||||
|
return True |
||||
|
|
||||
|
def to_components(self) -> List[Dict[str, Any]]: |
||||
|
components = [] |
||||
|
|
||||
|
def key(item: Item) -> int: |
||||
|
if item._rendered_row is not None: |
||||
|
return item._rendered_row |
||||
|
if item._row is not None: |
||||
|
return item._row |
||||
|
return sys.maxsize |
||||
|
|
||||
|
for i in sorted(self._children, key=key): |
||||
|
components.append(i.to_component_dict()) |
||||
|
return components |
||||
|
|
||||
|
def to_component_dict(self) -> Dict[str, Any]: |
||||
|
components = self.to_components() |
||||
|
|
||||
|
colour = None |
||||
|
if self._colour: |
||||
|
colour = self._colour if isinstance(self._colour, int) else self._colour.value |
||||
|
|
||||
|
base = { |
||||
|
'type': self.type.value, |
||||
|
'accent_color': colour, |
||||
|
'spoiler': self.spoiler, |
||||
|
'components': components, |
||||
|
} |
||||
|
if self.id is not None: |
||||
|
base['id'] = self.id |
||||
|
return base |
||||
|
|
||||
|
def _update_store_data( |
||||
|
self, |
||||
|
dispatch_info: Dict[Tuple[int, str], Item[Any]], |
||||
|
dynamic_items: Dict[Any, Type[DynamicItem]], |
||||
|
) -> bool: |
||||
|
is_fully_dynamic = True |
||||
|
for item in self.__dispatchable: |
||||
|
if isinstance(item, DynamicItem): |
||||
|
pattern = item.__discord_ui_compiled_template__ |
||||
|
dynamic_items[pattern] = item.__class__ |
||||
|
elif item.is_dispatchable(): |
||||
|
dispatch_info[(item.type.value, item.custom_id)] = item |
||||
|
is_fully_dynamic = False |
||||
|
return is_fully_dynamic |
||||
|
|
||||
|
@classmethod |
||||
|
def from_component(cls, component: ContainerComponent) -> Self: |
||||
|
return cls( |
||||
|
*[_component_to_item(c) for c in component.children], |
||||
|
accent_colour=component.accent_colour, |
||||
|
spoiler=component.spoiler, |
||||
|
id=component.id, |
||||
|
) |
||||
|
|
||||
|
def walk_children(self) -> Generator[Item[V], None, None]: |
||||
|
"""An iterator that recursively walks through all the children of this container |
||||
|
and it's children, if applicable. |
||||
|
|
||||
|
Yields |
||||
|
------ |
||||
|
:class:`Item` |
||||
|
An item in the container. |
||||
|
""" |
||||
|
|
||||
|
for child in self.children: |
||||
|
yield child |
||||
|
|
||||
|
if getattr(child, '__discord_ui_update_view__', False): |
||||
|
# if it has this attribute then it can contain children |
||||
|
yield from child.walk_children() # type: ignore |
||||
|
|
||||
|
def add_item(self, item: Item[Any]) -> Self: |
||||
|
"""Adds an item to this container. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
item: :class:`Item` |
||||
|
The item to append. |
||||
|
|
||||
|
Raises |
||||
|
------ |
||||
|
TypeError |
||||
|
An :class:`Item` was not passed. |
||||
|
""" |
||||
|
if not isinstance(item, Item): |
||||
|
raise TypeError(f'expected Item not {item.__class__.__name__}') |
||||
|
|
||||
|
self._children.append(item) |
||||
|
|
||||
|
if item.is_dispatchable(): |
||||
|
if getattr(item, '__discord_ui_section__', False): |
||||
|
self.__dispatchable.append(item.accessory) # type: ignore |
||||
|
elif getattr(item, '__discord_ui_action_row__', False): |
||||
|
self.__dispatchable.extend([i for i in item._children if i.is_dispatchable()]) # type: ignore |
||||
|
else: |
||||
|
self.__dispatchable.append(item) |
||||
|
|
||||
|
is_layout_view = self._view and getattr(self._view, '__discord_ui_layout_view__', False) |
||||
|
|
||||
|
if getattr(item, '__discord_ui_update_view__', False): |
||||
|
item._update_children_view(self.view) # type: ignore |
||||
|
|
||||
|
if is_layout_view: |
||||
|
self._view._total_children += sum(1 for _ in item.walk_children()) # type: ignore |
||||
|
elif is_layout_view: |
||||
|
self._view._total_children += 1 # type: ignore |
||||
|
|
||||
|
item._view = self.view |
||||
|
item._parent = self |
||||
|
return self |
||||
|
|
||||
|
def remove_item(self, item: Item[Any]) -> Self: |
||||
|
"""Removes an item from this container. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
item: :class:`TextDisplay` |
||||
|
The item to remove from the section. |
||||
|
""" |
||||
|
|
||||
|
try: |
||||
|
self._children.remove(item) |
||||
|
except ValueError: |
||||
|
pass |
||||
|
else: |
||||
|
if item.is_dispatchable(): |
||||
|
if getattr(item, '__discord_ui_section__', False): |
||||
|
self._remove_dispatchable(item.accessory) # type: ignore |
||||
|
elif getattr(item, '__discord_ui_action_row__', False): |
||||
|
for c in item._children: # type: ignore |
||||
|
if not c.is_dispatchable(): |
||||
|
continue |
||||
|
self._remove_dispatchable(c) |
||||
|
else: |
||||
|
self._remove_dispatchable(item) |
||||
|
|
||||
|
if self._view and getattr(self._view, '__discord_ui_layout_view__', False): |
||||
|
if getattr(item, '__discord_ui_update_view__', False): |
||||
|
self._view._total_children -= len(tuple(item.walk_children())) # type: ignore |
||||
|
else: |
||||
|
self._view._total_children -= 1 |
||||
|
return self |
||||
|
|
||||
|
def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: |
||||
|
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if |
||||
|
not found. |
||||
|
|
||||
|
.. warning:: |
||||
|
|
||||
|
This is **not the same** as ``custom_id``. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
id: :class:`int` |
||||
|
The ID of the component. |
||||
|
|
||||
|
Returns |
||||
|
------- |
||||
|
Optional[:class:`Item`] |
||||
|
The item found, or ``None``. |
||||
|
""" |
||||
|
return _utils_get(self._children, id=id) |
||||
|
|
||||
|
def clear_items(self) -> Self: |
||||
|
"""Removes all the items from the container. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
""" |
||||
|
|
||||
|
if self._view and getattr(self._view, '__discord_ui_layout_view__', False): |
||||
|
self._view._total_children -= sum(1 for _ in self.walk_children()) |
||||
|
self._children.clear() |
||||
|
self.__dispatchable.clear() |
||||
|
return self |
@ -0,0 +1,145 @@ |
|||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-present Rapptz |
||||
|
|
||||
|
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 __future__ import annotations |
||||
|
|
||||
|
from typing import TYPE_CHECKING, Literal, Optional, TypeVar, Union |
||||
|
|
||||
|
from .item import Item |
||||
|
from ..components import FileComponent, UnfurledMediaItem |
||||
|
from ..enums import ComponentType |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from typing_extensions import Self |
||||
|
|
||||
|
from .view import LayoutView |
||||
|
|
||||
|
V = TypeVar('V', bound='LayoutView', covariant=True) |
||||
|
|
||||
|
__all__ = ('File',) |
||||
|
|
||||
|
|
||||
|
class File(Item[V]): |
||||
|
"""Represents a UI file component. |
||||
|
|
||||
|
This is a top-level layout component that can only be used on :class:`LayoutView`. |
||||
|
|
||||
|
.. versionadded:: 2.6 |
||||
|
|
||||
|
Example |
||||
|
------- |
||||
|
|
||||
|
.. code-block:: python3 |
||||
|
|
||||
|
import discord |
||||
|
from discord import ui |
||||
|
|
||||
|
class MyView(ui.LayoutView): |
||||
|
file = ui.File('attachment://file.txt') |
||||
|
# attachment://file.txt points to an attachment uploaded alongside this view |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
media: Union[:class:`str`, :class:`.UnfurledMediaItem`] |
||||
|
This file's media. If this is a string it must point to a local |
||||
|
file uploaded within the parent view of this item, and must |
||||
|
meet the ``attachment://<filename>`` format. |
||||
|
spoiler: :class:`bool` |
||||
|
Whether to flag this file as a spoiler. Defaults to ``False``. |
||||
|
row: Optional[:class:`int`] |
||||
|
The relative row this file component belongs to. By default |
||||
|
items are arranged automatically into those rows. If you'd |
||||
|
like to control the relative positioning of the row then |
||||
|
passing an index is advised. For example, row=1 will show |
||||
|
up before row=2. Defaults to ``None``, which is automatic |
||||
|
ordering. The row number must be between 0 and 39 (i.e. zero indexed) |
||||
|
id: Optional[:class:`int`] |
||||
|
The ID of this component. This must be unique across the view. |
||||
|
""" |
||||
|
|
||||
|
def __init__( |
||||
|
self, |
||||
|
media: Union[str, UnfurledMediaItem], |
||||
|
*, |
||||
|
spoiler: bool = False, |
||||
|
row: Optional[int] = None, |
||||
|
id: Optional[int] = None, |
||||
|
) -> None: |
||||
|
super().__init__() |
||||
|
self._underlying = FileComponent._raw_construct( |
||||
|
media=UnfurledMediaItem(media) if isinstance(media, str) else media, |
||||
|
spoiler=spoiler, |
||||
|
id=id, |
||||
|
) |
||||
|
|
||||
|
self.row = row |
||||
|
self.id = id |
||||
|
|
||||
|
def _is_v2(self): |
||||
|
return True |
||||
|
|
||||
|
@property |
||||
|
def width(self): |
||||
|
return 5 |
||||
|
|
||||
|
@property |
||||
|
def type(self) -> Literal[ComponentType.file]: |
||||
|
return self._underlying.type |
||||
|
|
||||
|
@property |
||||
|
def media(self) -> UnfurledMediaItem: |
||||
|
""":class:`.UnfurledMediaItem`: Returns this file media.""" |
||||
|
return self._underlying.media |
||||
|
|
||||
|
@media.setter |
||||
|
def media(self, value: UnfurledMediaItem) -> None: |
||||
|
self._underlying.media = value |
||||
|
|
||||
|
@property |
||||
|
def url(self) -> str: |
||||
|
""":class:`str`: Returns this file's url.""" |
||||
|
return self._underlying.media.url |
||||
|
|
||||
|
@url.setter |
||||
|
def url(self, value: str) -> None: |
||||
|
self._underlying.media = UnfurledMediaItem(value) |
||||
|
|
||||
|
@property |
||||
|
def spoiler(self) -> bool: |
||||
|
""":class:`bool`: Returns whether this file should be flagged as a spoiler.""" |
||||
|
return self._underlying.spoiler |
||||
|
|
||||
|
@spoiler.setter |
||||
|
def spoiler(self, value: bool) -> None: |
||||
|
self._underlying.spoiler = value |
||||
|
|
||||
|
def to_component_dict(self): |
||||
|
return self._underlying.to_dict() |
||||
|
|
||||
|
@classmethod |
||||
|
def from_component(cls, component: FileComponent) -> Self: |
||||
|
return cls( |
||||
|
media=component.media, |
||||
|
spoiler=component.spoiler, |
||||
|
id=component.id, |
||||
|
) |
@ -0,0 +1,254 @@ |
|||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-present Rapptz |
||||
|
|
||||
|
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 __future__ import annotations |
||||
|
|
||||
|
from typing import TYPE_CHECKING, List, Literal, Optional, TypeVar, Union |
||||
|
|
||||
|
from .item import Item |
||||
|
from ..enums import ComponentType |
||||
|
from ..components import ( |
||||
|
MediaGalleryItem, |
||||
|
MediaGalleryComponent, |
||||
|
UnfurledMediaItem, |
||||
|
) |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from typing_extensions import Self |
||||
|
|
||||
|
from .view import LayoutView |
||||
|
|
||||
|
V = TypeVar('V', bound='LayoutView', covariant=True) |
||||
|
|
||||
|
__all__ = ('MediaGallery',) |
||||
|
|
||||
|
|
||||
|
class MediaGallery(Item[V]): |
||||
|
"""Represents a UI media gallery. |
||||
|
|
||||
|
Can contain up to 10 :class:`.MediaGalleryItem` 's. |
||||
|
|
||||
|
This is a top-level layout component that can only be used on :class:`LayoutView`. |
||||
|
|
||||
|
.. versionadded:: 2.6 |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
*items: :class:`.MediaGalleryItem` |
||||
|
The initial items of this gallery. |
||||
|
row: Optional[:class:`int`] |
||||
|
The relative row this media gallery belongs to. By default |
||||
|
items are arranged automatically into those rows. If you'd |
||||
|
like to control the relative positioning of the row then |
||||
|
passing an index is advised. For example, row=1 will show |
||||
|
up before row=2. Defaults to ``None``, which is automatic |
||||
|
ordering. The row number must be between 0 and 39 (i.e. zero indexed) |
||||
|
id: Optional[:class:`int`] |
||||
|
The ID of this component. This must be unique across the view. |
||||
|
""" |
||||
|
|
||||
|
def __init__( |
||||
|
self, |
||||
|
*items: MediaGalleryItem, |
||||
|
row: Optional[int] = None, |
||||
|
id: Optional[int] = None, |
||||
|
) -> None: |
||||
|
super().__init__() |
||||
|
|
||||
|
self._underlying = MediaGalleryComponent._raw_construct( |
||||
|
items=list(items), |
||||
|
id=id, |
||||
|
) |
||||
|
|
||||
|
self.row = row |
||||
|
self.id = id |
||||
|
|
||||
|
@property |
||||
|
def items(self) -> List[MediaGalleryItem]: |
||||
|
"""List[:class:`.MediaGalleryItem`]: Returns a read-only list of this gallery's items.""" |
||||
|
return self._underlying.items.copy() |
||||
|
|
||||
|
@items.setter |
||||
|
def items(self, value: List[MediaGalleryItem]) -> None: |
||||
|
if len(value) > 10: |
||||
|
raise ValueError('media gallery only accepts up to 10 items') |
||||
|
|
||||
|
self._underlying.items = value |
||||
|
|
||||
|
def to_component_dict(self): |
||||
|
return self._underlying.to_dict() |
||||
|
|
||||
|
def _is_v2(self) -> bool: |
||||
|
return True |
||||
|
|
||||
|
def add_item( |
||||
|
self, |
||||
|
*, |
||||
|
media: Union[str, UnfurledMediaItem], |
||||
|
description: Optional[str] = None, |
||||
|
spoiler: bool = False, |
||||
|
) -> Self: |
||||
|
"""Adds an item to this gallery. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
media: Union[:class:`str`, :class:`.UnfurledMediaItem`] |
||||
|
The media item data. This can be a string representing a local |
||||
|
file uploaded as an attachment in the message, which can be accessed |
||||
|
using the ``attachment://<filename>`` format, or an arbitrary url. |
||||
|
description: Optional[:class:`str`] |
||||
|
The description to show within this item. Up to 256 characters. Defaults |
||||
|
to ``None``. |
||||
|
spoiler: :class:`bool` |
||||
|
Whether this item should be flagged as a spoiler. Defaults to ``False``. |
||||
|
|
||||
|
Raises |
||||
|
------ |
||||
|
ValueError |
||||
|
Maximum number of items has been exceeded (10). |
||||
|
""" |
||||
|
|
||||
|
if len(self._underlying.items) >= 10: |
||||
|
raise ValueError('maximum number of items has been exceeded') |
||||
|
|
||||
|
item = MediaGalleryItem(media, description=description, spoiler=spoiler) |
||||
|
self._underlying.items.append(item) |
||||
|
return self |
||||
|
|
||||
|
def append_item(self, item: MediaGalleryItem) -> Self: |
||||
|
"""Appends an item to this gallery. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
item: :class:`.MediaGalleryItem` |
||||
|
The item to add to the gallery. |
||||
|
|
||||
|
Raises |
||||
|
------ |
||||
|
TypeError |
||||
|
A :class:`.MediaGalleryItem` was not passed. |
||||
|
ValueError |
||||
|
Maximum number of items has been exceeded (10). |
||||
|
""" |
||||
|
|
||||
|
if len(self._underlying.items) >= 10: |
||||
|
raise ValueError('maximum number of items has been exceeded') |
||||
|
|
||||
|
if not isinstance(item, MediaGalleryItem): |
||||
|
raise TypeError(f'expected MediaGalleryItem not {item.__class__.__name__}') |
||||
|
|
||||
|
self._underlying.items.append(item) |
||||
|
return self |
||||
|
|
||||
|
def insert_item_at( |
||||
|
self, |
||||
|
index: int, |
||||
|
*, |
||||
|
media: Union[str, UnfurledMediaItem], |
||||
|
description: Optional[str] = None, |
||||
|
spoiler: bool = False, |
||||
|
) -> Self: |
||||
|
"""Inserts an item before a specified index to the media gallery. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
index: :class:`int` |
||||
|
The index of where to insert the field. |
||||
|
media: Union[:class:`str`, :class:`.UnfurledMediaItem`] |
||||
|
The media item data. This can be a string representing a local |
||||
|
file uploaded as an attachment in the message, which can be accessed |
||||
|
using the ``attachment://<filename>`` format, or an arbitrary url. |
||||
|
description: Optional[:class:`str`] |
||||
|
The description to show within this item. Up to 256 characters. Defaults |
||||
|
to ``None``. |
||||
|
spoiler: :class:`bool` |
||||
|
Whether this item should be flagged as a spoiler. Defaults to ``False``. |
||||
|
|
||||
|
Raises |
||||
|
------ |
||||
|
ValueError |
||||
|
Maximum number of items has been exceeded (10). |
||||
|
""" |
||||
|
|
||||
|
if len(self._underlying.items) >= 10: |
||||
|
raise ValueError('maximum number of items has been exceeded') |
||||
|
|
||||
|
item = MediaGalleryItem( |
||||
|
media, |
||||
|
description=description, |
||||
|
spoiler=spoiler, |
||||
|
) |
||||
|
self._underlying.items.insert(index, item) |
||||
|
return self |
||||
|
|
||||
|
def remove_item(self, item: MediaGalleryItem) -> Self: |
||||
|
"""Removes an item from the gallery. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
item: :class:`.MediaGalleryItem` |
||||
|
The item to remove from the gallery. |
||||
|
""" |
||||
|
|
||||
|
try: |
||||
|
self._underlying.items.remove(item) |
||||
|
except ValueError: |
||||
|
pass |
||||
|
return self |
||||
|
|
||||
|
def clear_items(self) -> Self: |
||||
|
"""Removes all items from the gallery. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
""" |
||||
|
|
||||
|
self._underlying.items.clear() |
||||
|
return self |
||||
|
|
||||
|
@property |
||||
|
def type(self) -> Literal[ComponentType.media_gallery]: |
||||
|
return self._underlying.type |
||||
|
|
||||
|
@property |
||||
|
def width(self): |
||||
|
return 5 |
||||
|
|
||||
|
@classmethod |
||||
|
def from_component(cls, component: MediaGalleryComponent) -> Self: |
||||
|
return cls( |
||||
|
*component.items, |
||||
|
id=component.id, |
||||
|
) |
@ -0,0 +1,262 @@ |
|||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-present Rapptz |
||||
|
|
||||
|
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 __future__ import annotations |
||||
|
|
||||
|
import sys |
||||
|
from itertools import groupby |
||||
|
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Literal, Optional, TypeVar, Union, ClassVar |
||||
|
|
||||
|
from .item import Item |
||||
|
from .text_display import TextDisplay |
||||
|
from ..enums import ComponentType |
||||
|
from ..utils import MISSING, get as _utils_get |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from typing_extensions import Self |
||||
|
|
||||
|
from .view import LayoutView |
||||
|
from ..components import SectionComponent |
||||
|
|
||||
|
V = TypeVar('V', bound='LayoutView', covariant=True) |
||||
|
|
||||
|
__all__ = ('Section',) |
||||
|
|
||||
|
|
||||
|
class Section(Item[V]): |
||||
|
"""Represents a UI section. |
||||
|
|
||||
|
This is a top-level layout component that can only be used on :class:`LayoutView` |
||||
|
|
||||
|
.. versionadded:: 2.6 |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
*children: Union[:class:`str`, :class:`TextDisplay`] |
||||
|
The text displays of this section. Up to 3. |
||||
|
accessory: :class:`Item` |
||||
|
The section accessory. |
||||
|
row: Optional[:class:`int`] |
||||
|
The relative row this section belongs to. By default |
||||
|
items are arranged automatically into those rows. If you'd |
||||
|
like to control the relative positioning of the row then |
||||
|
passing an index is advised. For example, row=1 will show |
||||
|
up before row=2. Defaults to ``None``, which is automatic |
||||
|
ordering. The row number must be between 0 and 39 (i.e. zero indexed) |
||||
|
id: Optional[:class:`int`] |
||||
|
The ID of this component. This must be unique across the view. |
||||
|
""" |
||||
|
|
||||
|
__discord_ui_section__: ClassVar[bool] = True |
||||
|
__discord_ui_update_view__: ClassVar[bool] = True |
||||
|
|
||||
|
__slots__ = ( |
||||
|
'_children', |
||||
|
'accessory', |
||||
|
) |
||||
|
|
||||
|
def __init__( |
||||
|
self, |
||||
|
*children: Union[Item[V], str], |
||||
|
accessory: Item[V], |
||||
|
row: Optional[int] = None, |
||||
|
id: Optional[int] = None, |
||||
|
) -> None: |
||||
|
super().__init__() |
||||
|
self._children: List[Item[V]] = [] |
||||
|
if children is not MISSING: |
||||
|
if len(children) > 3: |
||||
|
raise ValueError('maximum number of children exceeded') |
||||
|
self._children.extend( |
||||
|
[c if isinstance(c, Item) else TextDisplay(c) for c in children], |
||||
|
) |
||||
|
self.accessory: Item[V] = accessory |
||||
|
self.row = row |
||||
|
self.id = id |
||||
|
|
||||
|
@property |
||||
|
def type(self) -> Literal[ComponentType.section]: |
||||
|
return ComponentType.section |
||||
|
|
||||
|
@property |
||||
|
def children(self) -> List[Item[V]]: |
||||
|
"""List[:class:`Item`]: The list of children attached to this section.""" |
||||
|
return self._children.copy() |
||||
|
|
||||
|
@property |
||||
|
def width(self): |
||||
|
return 5 |
||||
|
|
||||
|
def _is_v2(self) -> bool: |
||||
|
return True |
||||
|
|
||||
|
# Accessory can be a button, and thus it can have a callback so, maybe |
||||
|
# allow for section to be dispatchable and make the callback func |
||||
|
# be accessory component callback, only called if accessory is |
||||
|
# dispatchable? |
||||
|
def is_dispatchable(self) -> bool: |
||||
|
return self.accessory.is_dispatchable() |
||||
|
|
||||
|
def is_persistent(self) -> bool: |
||||
|
return self.accessory.is_persistent() |
||||
|
|
||||
|
def walk_children(self) -> Generator[Item[V], None, None]: |
||||
|
"""An iterator that recursively walks through all the children of this section. |
||||
|
and it's children, if applicable. |
||||
|
|
||||
|
Yields |
||||
|
------ |
||||
|
:class:`Item` |
||||
|
An item in this section. |
||||
|
""" |
||||
|
|
||||
|
for child in self.children: |
||||
|
yield child |
||||
|
yield self.accessory |
||||
|
|
||||
|
def _update_children_view(self, view) -> None: |
||||
|
self.accessory._view = view |
||||
|
|
||||
|
def add_item(self, item: Union[str, Item[Any]]) -> Self: |
||||
|
"""Adds an item to this section. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
item: Union[:class:`str`, :class:`Item`] |
||||
|
The item to append, if it is a string it automatically wrapped around |
||||
|
:class:`TextDisplay`. |
||||
|
|
||||
|
Raises |
||||
|
------ |
||||
|
TypeError |
||||
|
An :class:`Item` or :class:`str` was not passed. |
||||
|
ValueError |
||||
|
Maximum number of children has been exceeded (3). |
||||
|
""" |
||||
|
|
||||
|
if len(self._children) >= 3: |
||||
|
raise ValueError('maximum number of children exceeded') |
||||
|
|
||||
|
if not isinstance(item, (Item, str)): |
||||
|
raise TypeError(f'expected Item or str not {item.__class__.__name__}') |
||||
|
|
||||
|
item = item if isinstance(item, Item) else TextDisplay(item) |
||||
|
item._view = self.view |
||||
|
item._parent = self |
||||
|
self._children.append(item) |
||||
|
|
||||
|
if self._view and getattr(self._view, '__discord_ui_layout_view__', False): |
||||
|
self._view._total_children += 1 |
||||
|
|
||||
|
return self |
||||
|
|
||||
|
def remove_item(self, item: Item[Any]) -> Self: |
||||
|
"""Removes an item from this section. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
item: :class:`TextDisplay` |
||||
|
The item to remove from the section. |
||||
|
""" |
||||
|
|
||||
|
try: |
||||
|
self._children.remove(item) |
||||
|
except ValueError: |
||||
|
pass |
||||
|
else: |
||||
|
if self._view and getattr(self._view, '__discord_ui_layout_view__', False): |
||||
|
self._view._total_children -= 1 |
||||
|
|
||||
|
return self |
||||
|
|
||||
|
def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: |
||||
|
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if |
||||
|
not found. |
||||
|
|
||||
|
.. warning:: |
||||
|
|
||||
|
This is **not the same** as ``custom_id``. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
id: :class:`int` |
||||
|
The ID of the component. |
||||
|
|
||||
|
Returns |
||||
|
------- |
||||
|
Optional[:class:`Item`] |
||||
|
The item found, or ``None``. |
||||
|
""" |
||||
|
return _utils_get(self._children, id=id) |
||||
|
|
||||
|
def clear_items(self) -> Self: |
||||
|
"""Removes all the items from the section. |
||||
|
|
||||
|
This function returns the class instance to allow for fluent-style |
||||
|
chaining. |
||||
|
""" |
||||
|
if self._view and getattr(self._view, '__discord_ui_layout_view__', False): |
||||
|
self._view._total_children -= len(self._children) # we don't count the accessory because it is required |
||||
|
|
||||
|
self._children.clear() |
||||
|
return self |
||||
|
|
||||
|
@classmethod |
||||
|
def from_component(cls, component: SectionComponent) -> Self: |
||||
|
from .view import _component_to_item # >circular import< |
||||
|
|
||||
|
return cls( |
||||
|
*[_component_to_item(c) for c in component.components], |
||||
|
accessory=_component_to_item(component.accessory), |
||||
|
id=component.id, |
||||
|
) |
||||
|
|
||||
|
def to_components(self) -> List[Dict[str, Any]]: |
||||
|
components = [] |
||||
|
|
||||
|
def key(item: Item) -> int: |
||||
|
if item._rendered_row is not None: |
||||
|
return item._rendered_row |
||||
|
if item._row is not None: |
||||
|
return item._row |
||||
|
return sys.maxsize |
||||
|
|
||||
|
for _, comps in groupby(self._children, key=key): |
||||
|
components.extend(c.to_component_dict() for c in comps) |
||||
|
return components |
||||
|
|
||||
|
def to_component_dict(self) -> Dict[str, Any]: |
||||
|
data = { |
||||
|
'type': self.type.value, |
||||
|
'components': self.to_components(), |
||||
|
'accessory': self.accessory.to_component_dict(), |
||||
|
} |
||||
|
if self.id is not None: |
||||
|
data['id'] = self.id |
||||
|
return data |
@ -0,0 +1,127 @@ |
|||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-present Rapptz |
||||
|
|
||||
|
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 __future__ import annotations |
||||
|
|
||||
|
from typing import TYPE_CHECKING, Literal, Optional, TypeVar |
||||
|
|
||||
|
from .item import Item |
||||
|
from ..components import SeparatorComponent |
||||
|
from ..enums import SeparatorSize, ComponentType |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from typing_extensions import Self |
||||
|
|
||||
|
from .view import LayoutView |
||||
|
|
||||
|
V = TypeVar('V', bound='LayoutView', covariant=True) |
||||
|
|
||||
|
__all__ = ('Separator',) |
||||
|
|
||||
|
|
||||
|
class Separator(Item[V]): |
||||
|
"""Represents a UI separator. |
||||
|
|
||||
|
This is a top-level layout component that can only be used on :class:`LayoutView`. |
||||
|
|
||||
|
.. versionadded:: 2.6 |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
visible: :class:`bool` |
||||
|
Whether this separator is visible. On the client side this |
||||
|
is whether a divider line should be shown or not. |
||||
|
spacing: :class:`.SeparatorSize` |
||||
|
The spacing of this separator. |
||||
|
row: Optional[:class:`int`] |
||||
|
The relative row this separator belongs to. By default |
||||
|
items are arranged automatically into those rows. If you'd |
||||
|
like to control the relative positioning of the row then |
||||
|
passing an index is advised. For example, row=1 will show |
||||
|
up before row=2. Defaults to ``None``, which is automatic |
||||
|
ordering. The row number must be between 0 and 39 (i.e. zero indexed) |
||||
|
id: Optional[:class:`int`] |
||||
|
The ID of this component. This must be unique across the view. |
||||
|
""" |
||||
|
|
||||
|
def __init__( |
||||
|
self, |
||||
|
*, |
||||
|
visible: bool = True, |
||||
|
spacing: SeparatorSize = SeparatorSize.small, |
||||
|
row: Optional[int] = None, |
||||
|
id: Optional[int] = None, |
||||
|
) -> None: |
||||
|
super().__init__() |
||||
|
self._underlying = SeparatorComponent._raw_construct( |
||||
|
spacing=spacing, |
||||
|
visible=visible, |
||||
|
id=id, |
||||
|
) |
||||
|
|
||||
|
self.row = row |
||||
|
self.id = id |
||||
|
|
||||
|
def _is_v2(self): |
||||
|
return True |
||||
|
|
||||
|
@property |
||||
|
def visible(self) -> bool: |
||||
|
""":class:`bool`: Whether this separator is visible. |
||||
|
|
||||
|
On the client side this is whether a divider line should |
||||
|
be shown or not. |
||||
|
""" |
||||
|
return self._underlying.visible |
||||
|
|
||||
|
@visible.setter |
||||
|
def visible(self, value: bool) -> None: |
||||
|
self._underlying.visible = value |
||||
|
|
||||
|
@property |
||||
|
def spacing(self) -> SeparatorSize: |
||||
|
""":class:`.SeparatorSize`: The spacing of this separator.""" |
||||
|
return self._underlying.spacing |
||||
|
|
||||
|
@spacing.setter |
||||
|
def spacing(self, value: SeparatorSize) -> None: |
||||
|
self._underlying.spacing = value |
||||
|
|
||||
|
@property |
||||
|
def width(self): |
||||
|
return 5 |
||||
|
|
||||
|
@property |
||||
|
def type(self) -> Literal[ComponentType.separator]: |
||||
|
return self._underlying.type |
||||
|
|
||||
|
def to_component_dict(self): |
||||
|
return self._underlying.to_dict() |
||||
|
|
||||
|
@classmethod |
||||
|
def from_component(cls, component: SeparatorComponent) -> Self: |
||||
|
return cls( |
||||
|
visible=component.visible, |
||||
|
spacing=component.spacing, |
||||
|
id=component.id, |
||||
|
) |
@ -0,0 +1,96 @@ |
|||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-present Rapptz |
||||
|
|
||||
|
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 __future__ import annotations |
||||
|
|
||||
|
from typing import TYPE_CHECKING, Literal, Optional, TypeVar |
||||
|
|
||||
|
from .item import Item |
||||
|
from ..components import TextDisplay as TextDisplayComponent |
||||
|
from ..enums import ComponentType |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from typing_extensions import Self |
||||
|
|
||||
|
from .view import LayoutView |
||||
|
|
||||
|
V = TypeVar('V', bound='LayoutView', covariant=True) |
||||
|
|
||||
|
__all__ = ('TextDisplay',) |
||||
|
|
||||
|
|
||||
|
class TextDisplay(Item[V]): |
||||
|
"""Represents a UI text display. |
||||
|
|
||||
|
This is a top-level layout component that can only be used on :class:`LayoutView`. |
||||
|
|
||||
|
.. versionadded:: 2.6 |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
content: :class:`str` |
||||
|
The content of this text display. Up to 4000 characters. |
||||
|
row: Optional[:class:`int`] |
||||
|
The relative row this text display belongs to. By default |
||||
|
items are arranged automatically into those rows. If you'd |
||||
|
like to control the relative positioning of the row then |
||||
|
passing an index is advised. For example, row=1 will show |
||||
|
up before row=2. Defaults to ``None``, which is automatic |
||||
|
ordering. The row number must be between 0 and 39 (i.e. zero indexed) |
||||
|
id: Optional[:class:`int`] |
||||
|
The ID of this component. This must be unique across the view. |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, content: str, *, row: Optional[int] = None, id: Optional[int] = None) -> None: |
||||
|
super().__init__() |
||||
|
self.content: str = content |
||||
|
|
||||
|
self.row = row |
||||
|
self.id = id |
||||
|
|
||||
|
def to_component_dict(self): |
||||
|
base = { |
||||
|
'type': self.type.value, |
||||
|
'content': self.content, |
||||
|
} |
||||
|
if self.id is not None: |
||||
|
base['id'] = self.id |
||||
|
return base |
||||
|
|
||||
|
@property |
||||
|
def width(self): |
||||
|
return 5 |
||||
|
|
||||
|
@property |
||||
|
def type(self) -> Literal[ComponentType.text_display]: |
||||
|
return ComponentType.text_display |
||||
|
|
||||
|
def _is_v2(self) -> bool: |
||||
|
return True |
||||
|
|
||||
|
@classmethod |
||||
|
def from_component(cls, component: TextDisplayComponent) -> Self: |
||||
|
return cls( |
||||
|
content=component.content, |
||||
|
id=component.id, |
||||
|
) |
@ -0,0 +1,116 @@ |
|||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-present Rapptz |
||||
|
|
||||
|
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 __future__ import annotations |
||||
|
|
||||
|
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, TypeVar, Union |
||||
|
|
||||
|
from .item import Item |
||||
|
from ..enums import ComponentType |
||||
|
from ..components import UnfurledMediaItem |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from typing_extensions import Self |
||||
|
|
||||
|
from .view import LayoutView |
||||
|
from ..components import ThumbnailComponent |
||||
|
|
||||
|
V = TypeVar('V', bound='LayoutView', covariant=True) |
||||
|
|
||||
|
__all__ = ('Thumbnail',) |
||||
|
|
||||
|
|
||||
|
class Thumbnail(Item[V]): |
||||
|
"""Represents a UI Thumbnail. |
||||
|
|
||||
|
.. versionadded:: 2.6 |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
media: Union[:class:`str`, :class:`discord.UnfurledMediaItem`] |
||||
|
The media of the thumbnail. This can be a URL or a reference |
||||
|
to an attachment that matches the ``attachment://filename.extension`` |
||||
|
structure. |
||||
|
description: Optional[:class:`str`] |
||||
|
The description of this thumbnail. Up to 256 characters. Defaults to ``None``. |
||||
|
spoiler: :class:`bool` |
||||
|
Whether to flag this thumbnail as a spoiler. Defaults to ``False``. |
||||
|
row: Optional[:class:`int`] |
||||
|
The relative row this thumbnail belongs to. By default |
||||
|
items are arranged automatically into those rows. If you'd |
||||
|
like to control the relative positioning of the row then |
||||
|
passing an index is advised. For example, row=1 will show |
||||
|
up before row=2. Defaults to ``None``, which is automatic |
||||
|
ordering. The row number must be between 0 and 39 (i.e. zero indexed) |
||||
|
id: Optional[:class:`int`] |
||||
|
The ID of this component. This must be unique across the view. |
||||
|
""" |
||||
|
|
||||
|
def __init__( |
||||
|
self, |
||||
|
media: Union[str, UnfurledMediaItem], |
||||
|
*, |
||||
|
description: Optional[str] = None, |
||||
|
spoiler: bool = False, |
||||
|
row: Optional[int] = None, |
||||
|
id: Optional[int] = None, |
||||
|
) -> None: |
||||
|
super().__init__() |
||||
|
|
||||
|
self.media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media |
||||
|
self.description: Optional[str] = description |
||||
|
self.spoiler: bool = spoiler |
||||
|
|
||||
|
self.row = row |
||||
|
self.id = id |
||||
|
|
||||
|
@property |
||||
|
def width(self): |
||||
|
return 5 |
||||
|
|
||||
|
@property |
||||
|
def type(self) -> Literal[ComponentType.thumbnail]: |
||||
|
return ComponentType.thumbnail |
||||
|
|
||||
|
def _is_v2(self) -> bool: |
||||
|
return True |
||||
|
|
||||
|
def to_component_dict(self) -> Dict[str, Any]: |
||||
|
base = { |
||||
|
'type': self.type.value, |
||||
|
'spoiler': self.spoiler, |
||||
|
'media': self.media.to_dict(), |
||||
|
'description': self.description, |
||||
|
} |
||||
|
if self.id is not None: |
||||
|
base['id'] = self.id |
||||
|
return base |
||||
|
|
||||
|
@classmethod |
||||
|
def from_component(cls, component: ThumbnailComponent) -> Self: |
||||
|
return cls( |
||||
|
media=component.media.url, |
||||
|
description=component.description, |
||||
|
spoiler=component.spoiler, |
||||
|
id=component.id, |
||||
|
) |
@ -0,0 +1,47 @@ |
|||||
|
# This example requires the 'message_content' privileged intent to function. |
||||
|
|
||||
|
from discord.ext import commands |
||||
|
|
||||
|
import discord |
||||
|
|
||||
|
|
||||
|
class Bot(commands.Bot): |
||||
|
def __init__(self): |
||||
|
intents = discord.Intents.default() |
||||
|
intents.message_content = True |
||||
|
|
||||
|
super().__init__(command_prefix=commands.when_mentioned_or('$'), intents=intents) |
||||
|
|
||||
|
async def on_ready(self): |
||||
|
print(f'Logged in as {self.user} (ID: {self.user.id})') |
||||
|
print('------') |
||||
|
|
||||
|
|
||||
|
# Define a LayoutView, which will allow us to add v2 components to it. |
||||
|
class Layout(discord.ui.LayoutView): |
||||
|
# you can add any top-level component (ui.ActionRow, ui.Section, ui.Container, ui.File, etc.) here |
||||
|
|
||||
|
action_row = discord.ui.ActionRow() |
||||
|
|
||||
|
@action_row.button(label='Click Me!') |
||||
|
async def action_row_button(self, interaction: discord.Interaction, button: discord.ui.Button): |
||||
|
await interaction.response.send_message('Hi!', ephemeral=True) |
||||
|
|
||||
|
container = discord.ui.Container( |
||||
|
discord.ui.TextDisplay( |
||||
|
'Click the above button to receive a **very special** message!', |
||||
|
), |
||||
|
accent_colour=discord.Colour.blurple(), |
||||
|
) |
||||
|
|
||||
|
|
||||
|
bot = Bot() |
||||
|
|
||||
|
|
||||
|
@bot.command() |
||||
|
async def layout(ctx: commands.Context): |
||||
|
"""Sends a very special message!""" |
||||
|
await ctx.send(view=Layout()) # sending LayoutView's does not allow for sending any content, embed(s), stickers, or poll |
||||
|
|
||||
|
|
||||
|
bot.run('token') |
Loading…
Reference in new issue