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