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

2
discord/http.py

@ -192,6 +192,8 @@ def handle_message_parameters(
if view is not MISSING:
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()
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.
row: Optional[:class:`int`]
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
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 4 (i.e. zero indexed).
rows in a :class:`View`, but up to 10 on a :class:`Container`. 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 4 or 9 (i.e. zero indexed).
sku_id: Optional[:class:`int`]
The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji``
nor ``custom_id``.
@ -304,10 +305,11 @@ def button(
or a full :class:`.Emoji`.
row: Optional[:class:`int`]
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
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 4 (i.e. zero indexed).
rows in a :class:`View`, but up to 10 on a :class:`Container`. 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 4 or 9 (i.e. zero indexed).
"""
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
import sys
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar, Union
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Type, TypeVar
from .item import Item
from .view import View, _component_to_item
from .dynamic import DynamicItem
from ..enums import ComponentType
if TYPE_CHECKING:
@ -48,7 +48,7 @@ class Container(View, Item[V]):
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
items.
accent_colour: Optional[:class:`~discord.Colour`]
@ -58,34 +58,47 @@ class Container(View, Item[V]):
spoiler: :class:`bool`
Whether to flag this container as a spoiler. Defaults
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
def __init__(
self,
children: List[Union[Item[Any], View]],
children: List[Item[Any]],
*,
accent_colour: Optional[Colour] = None,
accent_color: Optional[Color] = None,
spoiler: bool = False,
timeout: Optional[float] = 180,
row: Optional[int] = None,
) -> None:
if len(children) > 10:
super().__init__(timeout=timeout)
if len(children) + len(self._children) > 10:
raise ValueError('maximum number of components exceeded')
self._children: List[Union[Item[Any], View]] = children
self._children.extend(children)
self.spoiler: bool = spoiler
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
def children(self) -> List[Union[Item[Any], View]]:
def children(self) -> List[Item[Self]]:
"""List[:class:`Item`]: The children of this container."""
return self._children.copy()
@children.setter
def children(self, value: List[Union[Item[Any], View]]) -> None:
def children(self, value: List[Item[Any]]) -> None:
self._children = value
@property
@ -105,20 +118,38 @@ class Container(View, Item[V]):
return ComponentType.container
@property
def _views(self) -> List[View]:
return [c for c in self._children if isinstance(c, View)]
def width(self):
return 5
def _is_v2(self) -> bool:
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()
return [{
return {
'type': self.type.value,
'accent_color': self._colour.value if self._colour else None,
'spoiler': self.spoiler,
'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
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,
) -> None:
super().__init__()
if len(children) > 3:
raise ValueError('maximum number of children exceeded')
self._children: List[Item[Any]] = [
@ -73,6 +74,10 @@ class Section(Item[V]):
def type(self) -> Literal[ComponentType.section]:
return ComponentType.section
@property
def width(self):
return 5
def _is_v2(self) -> bool:
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
__slots__ = (
'weights',
'max_weight',
)
# fmt: on
def __init__(self, children: List[Item]):
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
children = sorted(children, key=key)
@ -107,26 +109,21 @@ class _ViewWeights:
for item in group:
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):
if weight + item.width <= 5:
return index
raise ValueError('could not find open space for item')
def add_item(self, item: Union[Item, View]) -> None:
if hasattr(item, '__discord_ui_container__') and item.__discord_ui_container__ is True:
raise TypeError(
'containers cannot be added to views'
)
def add_item(self, item: Item) -> None:
if item._is_v2() and not self.v2_weights():
# v2 components allow up to 10 rows
self.weights.extend([0, 0, 0, 0, 0])
if item.row is not None:
total = self.weights[item.row] + item.width
if total > 10:
raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)')
if total > self.max_weight:
raise ValueError(f'item would not fit at row {item.row} ({total} > {self.max_weight} width)')
self.weights[item.row] = total
item._rendered_row = item.row
else:
@ -134,7 +131,7 @@ class _ViewWeights:
self.weights[index] += item.width
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:
self.weights[item._rendered_row] -= item.width
item._rendered_row = None
@ -185,8 +182,6 @@ class View:
for name, member in base.__dict__.items():
if hasattr(member, '__discord_ui_model_type__'):
children[name] = member
if cls.__discord_ui_container__ and isinstance(member, View):
children[name] = member
if len(children) > 25:
raise TypeError('View cannot have more than 25 children')
@ -203,7 +198,7 @@ class View:
children.append(item)
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._children: List[Item[Self]] = self._init_children()
self.__weights = _ViewWeights(self._children)
@ -213,8 +208,6 @@ class View:
self.__timeout_expiry: Optional[float] = None
self.__timeout_task: Optional[asyncio.Task[None]] = None
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:
return False
@ -257,7 +250,12 @@ class View:
# helper mapping to find action rows for items that are not
# 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():
components.append(child.to_component_dict())
else:
@ -619,21 +617,14 @@ class ViewStore:
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 # type: ignore
is_fully_dynamic = False
# components V2 containers allow for views to exist inside them
# with dispatchable items, so we iterate over it and add it
# to the store
if hasattr(view, '_views'):
for v in view._views:
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
if getattr(item, '__discord_ui_container__', False):
is_fully_dynamic = item._update_store_data( # type: ignore
dispatch_info,
self._dynamic_items,
)
else:
dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore
is_fully_dynamic = False
view._cache_key = message_id
if message_id is not None and not is_fully_dynamic:

Loading…
Cancel
Save