Browse Source

chore: More v2 components on UI and idk some changes

pull/10166/head
DA-344 3 months ago
parent
commit
76e2028118
  1. 13
      discord/abc.py
  2. 2
      discord/http.py
  3. 18
      discord/ui/button.py
  4. 59
      discord/ui/container.py
  5. 5
      discord/ui/section.py
  6. 71
      discord/ui/text_display.py
  7. 53
      discord/ui/view.py

13
discord/abc.py

@ -1389,6 +1389,7 @@ class Messageable:
reference: Union[Message, MessageReference, PartialMessage] = ..., reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ..., mention_author: bool = ...,
view: View = ..., view: View = ...,
views: Sequence[View] = ...,
suppress_embeds: bool = ..., suppress_embeds: bool = ...,
silent: bool = ..., silent: bool = ...,
poll: Poll = ..., poll: Poll = ...,
@ -1410,6 +1411,7 @@ class Messageable:
reference: Union[Message, MessageReference, PartialMessage] = ..., reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ..., mention_author: bool = ...,
view: View = ..., view: View = ...,
views: Sequence[View] = ...,
suppress_embeds: bool = ..., suppress_embeds: bool = ...,
silent: bool = ..., silent: bool = ...,
poll: Poll = ..., poll: Poll = ...,
@ -1431,6 +1433,7 @@ class Messageable:
reference: Union[Message, MessageReference, PartialMessage] = ..., reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ..., mention_author: bool = ...,
view: View = ..., view: View = ...,
views: Sequence[View] = ...,
suppress_embeds: bool = ..., suppress_embeds: bool = ...,
silent: bool = ..., silent: bool = ...,
poll: Poll = ..., poll: Poll = ...,
@ -1452,6 +1455,7 @@ class Messageable:
reference: Union[Message, MessageReference, PartialMessage] = ..., reference: Union[Message, MessageReference, PartialMessage] = ...,
mention_author: bool = ..., mention_author: bool = ...,
view: View = ..., view: View = ...,
views: Sequence[View] = ...,
suppress_embeds: bool = ..., suppress_embeds: bool = ...,
silent: bool = ..., silent: bool = ...,
poll: Poll = ..., poll: Poll = ...,
@ -1474,6 +1478,7 @@ class Messageable:
reference: Optional[Union[Message, MessageReference, PartialMessage]] = None, reference: Optional[Union[Message, MessageReference, PartialMessage]] = None,
mention_author: Optional[bool] = None, mention_author: Optional[bool] = None,
view: Optional[View] = None, view: Optional[View] = None,
views: Optional[Sequence[View]] = None,
suppress_embeds: bool = False, suppress_embeds: bool = False,
silent: bool = False, silent: bool = False,
poll: Optional[Poll] = None, poll: Optional[Poll] = None,
@ -1550,6 +1555,10 @@ class Messageable:
A Discord UI View to add to the message. A Discord UI View to add to the message.
.. versionadded:: 2.0 .. versionadded:: 2.0
views: Sequence[:class:`discord.ui.View`]
A sequence of Discord UI Views to add to the message.
.. versionadded:: 2.6
stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]]
A list of stickers to upload. Must be a maximum of 3. A list of stickers to upload. Must be a maximum of 3.
@ -1580,7 +1589,8 @@ class Messageable:
You specified both ``file`` and ``files``, You specified both ``file`` and ``files``,
or you specified both ``embed`` and ``embeds``, or you specified both ``embed`` and ``embeds``,
or the ``reference`` object is not a :class:`~discord.Message`, or the ``reference`` object is not a :class:`~discord.Message`,
:class:`~discord.MessageReference` or :class:`~discord.PartialMessage`. :class:`~discord.MessageReference` or :class:`~discord.PartialMessage`,
or you specified both ``view`` and ``views``.
Returns Returns
--------- ---------
@ -1635,6 +1645,7 @@ class Messageable:
mention_author=mention_author, mention_author=mention_author,
stickers=sticker_ids, stickers=sticker_ids,
view=view, view=view,
views=views if views is not None else MISSING,
flags=flags, flags=flags,
poll=poll, poll=poll,
) as params: ) as params:

2
discord/http.py

@ -192,6 +192,8 @@ def handle_message_parameters(
if view is not MISSING: if view is not MISSING:
if view is not None: if view is not None:
if getattr(view, '__discord_ui_container__', False):
raise TypeError('Containers must be wrapped around Views')
payload['components'] = view.to_components() payload['components'] = view.to_components()
if view.has_components_v2(): if view.has_components_v2():

18
discord/ui/button.py

@ -73,10 +73,11 @@ class Button(Item[V]):
The emoji of the button, if available. The emoji of the button, if available.
row: Optional[:class:`int`] row: Optional[:class:`int`]
The relative row this button belongs to. A Discord component can only have 5 The relative row this button belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd rows in a :class:`View`, but up to 10 on a :class:`Container`. By default,
like to control the relative positioning of the row then passing an index is advised. items are arranged automatically into those rows. If you'd like to control the
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic relative positioning of the row then passing an index is advised. For example,
ordering. The row number must be between 0 and 4 (i.e. zero indexed). row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 or 9 (i.e. zero indexed).
sku_id: Optional[:class:`int`] sku_id: Optional[:class:`int`]
The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji`` The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji``
nor ``custom_id``. nor ``custom_id``.
@ -304,10 +305,11 @@ def button(
or a full :class:`.Emoji`. or a full :class:`.Emoji`.
row: Optional[:class:`int`] row: Optional[:class:`int`]
The relative row this button belongs to. A Discord component can only have 5 The relative row this button belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd rows in a :class:`View`, but up to 10 on a :class:`Container`. By default,
like to control the relative positioning of the row then passing an index is advised. items are arranged automatically into those rows. If you'd like to control the
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic relative positioning of the row then passing an index is advised. For example,
ordering. The row number must be between 0 and 4 (i.e. zero indexed). row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 or 9 (i.e. zero indexed).
""" """
def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]:

59
discord/ui/container.py

@ -23,11 +23,11 @@ DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
import sys from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Type, TypeVar
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar, Union
from .item import Item from .item import Item
from .view import View, _component_to_item from .view import View, _component_to_item
from .dynamic import DynamicItem
from ..enums import ComponentType from ..enums import ComponentType
if TYPE_CHECKING: if TYPE_CHECKING:
@ -48,7 +48,7 @@ class Container(View, Item[V]):
Parameters Parameters
---------- ----------
children: List[Union[:class:`Item`, :class:`View`]] children: List[:class:`Item`]
The initial children or :class:`View`s of this container. Can have up to 10 The initial children or :class:`View`s of this container. Can have up to 10
items. items.
accent_colour: Optional[:class:`~discord.Colour`] accent_colour: Optional[:class:`~discord.Colour`]
@ -58,34 +58,47 @@ class Container(View, Item[V]):
spoiler: :class:`bool` spoiler: :class:`bool`
Whether to flag this container as a spoiler. Defaults Whether to flag this container as a spoiler. Defaults
to ``False``. to ``False``.
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
""" """
__discord_ui_container__ = True __discord_ui_container__ = True
def __init__( def __init__(
self, self,
children: List[Union[Item[Any], View]], children: List[Item[Any]],
*, *,
accent_colour: Optional[Colour] = None, accent_colour: Optional[Colour] = None,
accent_color: Optional[Color] = None, accent_color: Optional[Color] = None,
spoiler: bool = False, spoiler: bool = False,
timeout: Optional[float] = 180, timeout: Optional[float] = 180,
row: Optional[int] = None,
) -> None: ) -> None:
if len(children) > 10: super().__init__(timeout=timeout)
if len(children) + len(self._children) > 10:
raise ValueError('maximum number of components exceeded') raise ValueError('maximum number of components exceeded')
self._children: List[Union[Item[Any], View]] = children self._children.extend(children)
self.spoiler: bool = spoiler self.spoiler: bool = spoiler
self._colour = accent_colour or accent_color self._colour = accent_colour or accent_color
super().__init__(timeout=timeout) self._view: Optional[V] = None
self._row: Optional[int] = None
self._rendered_row: Optional[int] = None
self.row: Optional[int] = row
def _init_children(self) -> List[Item[Self]]:
if self.__weights.max_weight != 10:
self.__weights.max_weight = 10
return super()._init_children()
@property @property
def children(self) -> List[Union[Item[Any], View]]: def children(self) -> List[Item[Self]]:
"""List[:class:`Item`]: The children of this container.""" """List[:class:`Item`]: The children of this container."""
return self._children.copy() return self._children.copy()
@children.setter @children.setter
def children(self, value: List[Union[Item[Any], View]]) -> None: def children(self, value: List[Item[Any]]) -> None:
self._children = value self._children = value
@property @property
@ -105,20 +118,38 @@ class Container(View, Item[V]):
return ComponentType.container return ComponentType.container
@property @property
def _views(self) -> List[View]: def width(self):
return [c for c in self._children if isinstance(c, View)] return 5
def _is_v2(self) -> bool: def _is_v2(self) -> bool:
return True return True
def to_components(self) -> List[Dict[str, Any]]: def is_dispatchable(self) -> bool:
return any(c.is_dispatchable() for c in self.children)
def to_component_dict(self) -> Dict[str, Any]:
components = super().to_components() components = super().to_components()
return [{ return {
'type': self.type.value, 'type': self.type.value,
'accent_color': self._colour.value if self._colour else None, 'accent_color': self._colour.value if self._colour else None,
'spoiler': self.spoiler, 'spoiler': self.spoiler,
'components': components, 'components': components,
}] }
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._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 # type: ignore
is_fully_dynamic = False
return is_fully_dynamic
@classmethod @classmethod
def from_component(cls, component: ContainerComponent) -> Self: def from_component(cls, component: ContainerComponent) -> Self:

5
discord/ui/section.py

@ -62,6 +62,7 @@ class Section(Item[V]):
*, *,
accessory: Optional[Item[Any]] = None, accessory: Optional[Item[Any]] = None,
) -> None: ) -> None:
super().__init__()
if len(children) > 3: if len(children) > 3:
raise ValueError('maximum number of children exceeded') raise ValueError('maximum number of children exceeded')
self._children: List[Item[Any]] = [ self._children: List[Item[Any]] = [
@ -73,6 +74,10 @@ class Section(Item[V]):
def type(self) -> Literal[ComponentType.section]: def type(self) -> Literal[ComponentType.section]:
return ComponentType.section return ComponentType.section
@property
def width(self):
return 5
def _is_v2(self) -> bool: def _is_v2(self) -> bool:
return True return True

71
discord/ui/text_display.py

@ -0,0 +1,71 @@
"""
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, TypeVar
from .item import Item
from ..components import TextDisplay as TextDisplayComponent
from ..enums import ComponentType
if TYPE_CHECKING:
from .view import View
V = TypeVar('V', bound='View', covariant=True)
__all__ = ('TextDisplay',)
class TextDisplay(Item[V]):
"""Represents a UI text display.
.. versionadded:: 2.6
Parameters
----------
content: :class:`str`
The content of this text display.
"""
def __init__(self, content: str) -> None:
super().__init__()
self.content: str = content
self._underlying = TextDisplayComponent._raw_construct(
content=content,
)
def to_component_dict(self):
return self._underlying.to_dict()
@property
def width(self):
return 5
@property
def type(self) -> Literal[ComponentType.text_display]:
return self._underlying.type
def _is_v2(self) -> bool:
return True

53
discord/ui/view.py

@ -95,11 +95,13 @@ class _ViewWeights:
# fmt: off # fmt: off
__slots__ = ( __slots__ = (
'weights', 'weights',
'max_weight',
) )
# fmt: on # fmt: on
def __init__(self, children: List[Item]): def __init__(self, children: List[Item]):
self.weights: List[int] = [0, 0, 0, 0, 0] self.weights: List[int] = [0, 0, 0, 0, 0]
self.max_weight: int = 5
key = lambda i: sys.maxsize if i.row is None else i.row key = lambda i: sys.maxsize if i.row is None else i.row
children = sorted(children, key=key) children = sorted(children, key=key)
@ -107,26 +109,21 @@ class _ViewWeights:
for item in group: for item in group:
self.add_item(item) self.add_item(item)
def find_open_space(self, item: Union[Item, View]) -> int: def find_open_space(self, item: Item) -> int:
for index, weight in enumerate(self.weights): for index, weight in enumerate(self.weights):
if weight + item.width <= 5: if weight + item.width <= 5:
return index return index
raise ValueError('could not find open space for item') raise ValueError('could not find open space for item')
def add_item(self, item: Union[Item, View]) -> None: def add_item(self, item: Item) -> None:
if hasattr(item, '__discord_ui_container__') and item.__discord_ui_container__ is True:
raise TypeError(
'containers cannot be added to views'
)
if item._is_v2() and not self.v2_weights(): if item._is_v2() and not self.v2_weights():
# v2 components allow up to 10 rows # v2 components allow up to 10 rows
self.weights.extend([0, 0, 0, 0, 0]) self.weights.extend([0, 0, 0, 0, 0])
if item.row is not None: if item.row is not None:
total = self.weights[item.row] + item.width total = self.weights[item.row] + item.width
if total > 10: if total > self.max_weight:
raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)') raise ValueError(f'item would not fit at row {item.row} ({total} > {self.max_weight} width)')
self.weights[item.row] = total self.weights[item.row] = total
item._rendered_row = item.row item._rendered_row = item.row
else: else:
@ -134,7 +131,7 @@ class _ViewWeights:
self.weights[index] += item.width self.weights[index] += item.width
item._rendered_row = index item._rendered_row = index
def remove_item(self, item: Union[Item, View]) -> None: def remove_item(self, item: Item) -> None:
if item._rendered_row is not None: if item._rendered_row is not None:
self.weights[item._rendered_row] -= item.width self.weights[item._rendered_row] -= item.width
item._rendered_row = None item._rendered_row = None
@ -185,8 +182,6 @@ class View:
for name, member in base.__dict__.items(): for name, member in base.__dict__.items():
if hasattr(member, '__discord_ui_model_type__'): if hasattr(member, '__discord_ui_model_type__'):
children[name] = member children[name] = member
if cls.__discord_ui_container__ and isinstance(member, View):
children[name] = member
if len(children) > 25: if len(children) > 25:
raise TypeError('View cannot have more than 25 children') raise TypeError('View cannot have more than 25 children')
@ -203,7 +198,7 @@ class View:
children.append(item) children.append(item)
return children return children
def __init__(self, *, timeout: Optional[float] = 180.0, row: Optional[int] = None): def __init__(self, *, timeout: Optional[float] = 180.0):
self.__timeout = timeout self.__timeout = timeout
self._children: List[Item[Self]] = self._init_children() self._children: List[Item[Self]] = self._init_children()
self.__weights = _ViewWeights(self._children) self.__weights = _ViewWeights(self._children)
@ -213,8 +208,6 @@ class View:
self.__timeout_expiry: Optional[float] = None self.__timeout_expiry: Optional[float] = None
self.__timeout_task: Optional[asyncio.Task[None]] = None self.__timeout_task: Optional[asyncio.Task[None]] = None
self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
self.row: Optional[int] = row
self._rendered_row: Optional[int] = None
def _is_v2(self) -> bool: def _is_v2(self) -> bool:
return False return False
@ -257,7 +250,12 @@ class View:
# helper mapping to find action rows for items that are not # helper mapping to find action rows for items that are not
# v2 components # v2 components
for child in self._children: def key(item: Item) -> int:
return item._rendered_row or 0
# instead of grouping by row we will sort it so it is added
# in order and should work as the original implementation
for child in sorted(self._children, key=key):
if child._is_v2(): if child._is_v2():
components.append(child.to_component_dict()) components.append(child.to_component_dict())
else: else:
@ -619,21 +617,14 @@ class ViewStore:
pattern = item.__discord_ui_compiled_template__ pattern = item.__discord_ui_compiled_template__
self._dynamic_items[pattern] = item.__class__ self._dynamic_items[pattern] = item.__class__
elif item.is_dispatchable(): elif item.is_dispatchable():
dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore if getattr(item, '__discord_ui_container__', False):
is_fully_dynamic = False is_fully_dynamic = item._update_store_data( # type: ignore
dispatch_info,
# components V2 containers allow for views to exist inside them self._dynamic_items,
# with dispatchable items, so we iterate over it and add it )
# to the store else:
if hasattr(view, '_views'): dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore
for v in view._views: is_fully_dynamic = False
for item in v._children:
if isinstance(item, DynamicItem):
pattern = item.__discord_ui_compiled_template__
self._dynamic_items[pattern] = item.__class__
elif item.is_dispatchable():
dispatch_info[(item.type.value, item.custom_id)] = item
is_fully_dynamic = False
view._cache_key = message_id view._cache_key = message_id
if message_id is not None and not is_fully_dynamic: if message_id is not None and not is_fully_dynamic:

Loading…
Cancel
Save