Browse Source

chore: more components v2 things and finished danny's suggested impl

pull/10166/head
DA-344 3 months ago
parent
commit
8f59216e68
  1. 2
      discord/components.py
  2. 16
      discord/ui/action_row.py
  3. 5
      discord/ui/button.py
  4. 8
      discord/ui/container.py
  5. 8
      discord/ui/dynamic.py
  6. 8
      discord/ui/file.py
  7. 16
      discord/ui/item.py
  8. 15
      discord/ui/media_gallery.py
  9. 8
      discord/ui/section.py
  10. 5
      discord/ui/select.py
  11. 8
      discord/ui/separator.py
  12. 9
      discord/ui/text_display.py
  13. 8
      discord/ui/thumbnail.py
  14. 397
      discord/ui/view.py

2
discord/components.py

@ -967,7 +967,7 @@ class MediaGalleryItem:
def _from_data(cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState]) -> MediaGalleryItem:
media = data['media']
self = cls(
media=media['url'],
media=UnfurledMediaItem._from_data(media, state),
description=data.get('description'),
spoiler=data.get('spoiler', False),
)

16
discord/ui/action_row.py

@ -45,7 +45,8 @@ from typing import (
from .item import Item, ItemCallbackType
from .button import Button
from .dynamic import DynamicItem
from .select import select as _select, Select, SelectCallbackDecorator, UserSelect, RoleSelect, ChannelSelect, MentionableSelect
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
@ -61,7 +62,8 @@ if TYPE_CHECKING:
ChannelSelectT,
RoleSelectT,
UserSelectT,
SelectT
SelectT,
SelectCallbackDecorator,
)
from ..emoji import Emoji
from ..components import SelectOption
@ -125,7 +127,7 @@ class ActionRow(Item[V]):
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)
item.callback = _ActionRowCallback(func, self, item) # type: ignore
item._parent = getattr(func, '__discord_ui_parent__', self) # type: ignore
setattr(self, func.__name__, item)
children.append(item)
@ -478,3 +480,11 @@ class ActionRow(Item[V]):
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

5
discord/ui/button.py

@ -42,12 +42,12 @@ __all__ = (
if TYPE_CHECKING:
from typing_extensions import Self
from .view import View
from .view import BaseView
from .action_row import ActionRow
from ..emoji import Emoji
from ..types.components import ButtonComponent as ButtonComponentPayload
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='BaseView', covariant=True)
class Button(Item[V]):
@ -147,6 +147,7 @@ class Button(Item[V]):
)
self._parent: Optional[ActionRow] = None
self.row = row
self.id = custom_id
@property
def style(self) -> ButtonStyle:

8
discord/ui/container.py

@ -26,7 +26,7 @@ from __future__ import annotations
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 .view import View, _component_to_item, LayoutView
from .dynamic import DynamicItem
from ..enums import ComponentType
from ..utils import MISSING
@ -37,7 +37,7 @@ if TYPE_CHECKING:
from ..colour import Colour, Color
from ..components import Container as ContainerComponent
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Container',)
@ -69,6 +69,8 @@ class Container(View, Item[V]):
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 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
"""
__discord_ui_container__ = True
@ -82,6 +84,7 @@ class Container(View, Item[V]):
spoiler: bool = False,
timeout: Optional[float] = 180,
row: Optional[int] = None,
id: Optional[str] = None,
) -> None:
super().__init__(timeout=timeout)
if children is not MISSING:
@ -95,6 +98,7 @@ class Container(View, Item[V]):
self._row: Optional[int] = None
self._rendered_row: Optional[int] = None
self.row: Optional[int] = row
self.id: Optional[str] = id
@property
def children(self) -> List[Item[Self]]:

8
discord/ui/dynamic.py

@ -38,14 +38,14 @@ if TYPE_CHECKING:
from ..interactions import Interaction
from ..components import Component
from ..enums import ComponentType
from .view import View
from .view import BaseView
V = TypeVar('V', bound='View', covariant=True, default=View)
V = TypeVar('V', bound='BaseView', covariant=True, default=BaseView)
else:
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='BaseView', covariant=True)
class DynamicItem(Generic[BaseT], Item['View']):
class DynamicItem(Generic[BaseT], Item['BaseView']):
"""Represents an item with a dynamic ``custom_id`` that can be used to store state within
that ``custom_id``.

8
discord/ui/file.py

@ -32,9 +32,9 @@ from ..enums import ComponentType
if TYPE_CHECKING:
from typing_extensions import Self
from .view import View
from .view import LayoutView
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('File',)
@ -59,6 +59,8 @@ class File(Item[V]):
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 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
"""
def __init__(
@ -67,6 +69,7 @@ class File(Item[V]):
*,
spoiler: bool = False,
row: Optional[int] = None,
id: Optional[str] = None,
) -> None:
super().__init__()
self._underlying = FileComponent._raw_construct(
@ -75,6 +78,7 @@ class File(Item[V]):
)
self.row = row
self.id = id
def _is_v2(self):
return True

16
discord/ui/item.py

@ -37,11 +37,11 @@ __all__ = (
if TYPE_CHECKING:
from ..enums import ComponentType
from .view import View
from .view import BaseView
from ..components import Component
I = TypeVar('I', bound='Item[Any]')
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='BaseView', covariant=True)
ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]]
@ -70,6 +70,7 @@ class Item(Generic[V]):
# actually affect the intended purpose of this check because from_component is
# only called upon edit and we're mainly interested during initial creation time.
self._provided_custom_id: bool = False
self._id: Optional[str] = None
self._max_row: int = 5 if not self._is_v2() else 10
def to_component_dict(self) -> Dict[str, Any]:
@ -124,6 +125,17 @@ class Item(Generic[V]):
"""Optional[:class:`View`]: The underlying view for this item."""
return self._view
@property
def id(self) -> Optional[str]:
"""Optional[:class:`str`]: The ID of this component. For non v2 components this is the
equivalent to ``custom_id``.
"""
return self._id
@id.setter
def id(self, value: Optional[str]) -> None:
self._id = value
async def callback(self, interaction: Interaction[ClientT]) -> Any:
"""|coro|

15
discord/ui/media_gallery.py

@ -35,9 +35,9 @@ from ..components import (
if TYPE_CHECKING:
from typing_extensions import Self
from .view import View
from .view import LayoutView
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('MediaGallery',)
@ -60,9 +60,17 @@ class MediaGallery(Item[V]):
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 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
"""
def __init__(self, items: List[MediaGalleryItem], *, row: Optional[int] = None) -> None:
def __init__(
self,
items: List[MediaGalleryItem],
*,
row: Optional[int] = None,
id: Optional[str] = None,
) -> None:
super().__init__()
self._underlying = MediaGalleryComponent._raw_construct(
@ -70,6 +78,7 @@ class MediaGallery(Item[V]):
)
self.row = row
self.id = id
@property
def items(self) -> List[MediaGalleryItem]:

8
discord/ui/section.py

@ -33,10 +33,10 @@ from ..utils import MISSING
if TYPE_CHECKING:
from typing_extensions import Self
from .view import View
from .view import LayoutView
from ..components import SectionComponent
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Section',)
@ -59,6 +59,8 @@ class Section(Item[V]):
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 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
"""
__slots__ = (
@ -72,6 +74,7 @@ class Section(Item[V]):
*,
accessory: Item[Any],
row: Optional[int] = None,
id: Optional[str] = None,
) -> None:
super().__init__()
self._children: List[Item[Any]] = []
@ -84,6 +87,7 @@ class Section(Item[V]):
self.accessory: Item[Any] = accessory
self.row = row
self.id = id
@property
def type(self) -> Literal[ComponentType.section]:

5
discord/ui/select.py

@ -72,7 +72,7 @@ __all__ = (
if TYPE_CHECKING:
from typing_extensions import TypeAlias, TypeGuard
from .view import View
from .view import BaseView
from .action_row import ActionRow
from ..types.components import SelectMenu as SelectMenuPayload
from ..types.interactions import SelectMessageComponentInteractionData
@ -102,7 +102,7 @@ if TYPE_CHECKING:
Thread,
]
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='BaseView', covariant=True)
BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]')
SelectT = TypeVar('SelectT', bound='Select[Any]')
UserSelectT = TypeVar('UserSelectT', bound='UserSelect[Any]')
@ -259,6 +259,7 @@ class BaseSelect(Item[V]):
)
self.row = row
self.id = custom_id if custom_id is not MISSING else None
self._parent: Optional[ActionRow] = None
self._values: List[PossibleValue] = []

8
discord/ui/separator.py

@ -32,9 +32,9 @@ from ..enums import SeparatorSize, ComponentType
if TYPE_CHECKING:
from typing_extensions import Self
from .view import View
from .view import LayoutView
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Separator',)
@ -58,6 +58,8 @@ class Separator(Item[V]):
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 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
"""
def __init__(
@ -66,6 +68,7 @@ class Separator(Item[V]):
visible: bool = True,
spacing: SeparatorSize = SeparatorSize.small,
row: Optional[int] = None,
id: Optional[str] = None,
) -> None:
super().__init__()
self._underlying = SeparatorComponent._raw_construct(
@ -74,6 +77,7 @@ class Separator(Item[V]):
)
self.row = row
self.id = id
def _is_v2(self):
return True

9
discord/ui/text_display.py

@ -32,9 +32,9 @@ from ..enums import ComponentType
if TYPE_CHECKING:
from typing_extensions import Self
from .view import View
from .view import LayoutView
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('TextDisplay',)
@ -55,13 +55,16 @@ class TextDisplay(Item[V]):
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 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
"""
def __init__(self, content: str, *, row: Optional[int] = None) -> None:
def __init__(self, content: str, *, row: Optional[int] = None, id: Optional[str] = None) -> None:
super().__init__()
self.content: str = content
self.row = row
self.id = id
def to_component_dict(self):
return {

8
discord/ui/thumbnail.py

@ -32,10 +32,10 @@ from ..components import UnfurledMediaItem
if TYPE_CHECKING:
from typing_extensions import Self
from .view import View
from .view import LayoutView
from ..components import ThumbnailComponent
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Thumbnail',)
@ -62,6 +62,8 @@ class Thumbnail(Item[V]):
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 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
"""
def __init__(
@ -71,6 +73,7 @@ class Thumbnail(Item[V]):
description: Optional[str] = None,
spoiler: bool = False,
row: Optional[int] = None,
id: Optional[str] = None,
) -> None:
super().__init__()
@ -79,6 +82,7 @@ class Thumbnail(Item[V]):
self.spoiler: bool = spoiler
self.row = row
self.id = id
@property
def width(self):

397
discord/ui/view.py

@ -36,6 +36,7 @@ from typing import (
TYPE_CHECKING,
Tuple,
Type,
Union,
)
from functools import partial
from itertools import groupby
@ -46,7 +47,6 @@ import sys
import time
import os
from .item import Item, ItemCallbackType
from .action_row import ActionRow
from .dynamic import DynamicItem
from ..components import (
Component,
@ -61,10 +61,12 @@ from ..components import (
SeparatorComponent,
ThumbnailComponent,
)
from ..utils import get as _utils_get
# fmt: off
__all__ = (
'View',
'LayoutView',
)
# fmt: on
@ -80,6 +82,8 @@ if TYPE_CHECKING:
from ..state import ConnectionState
from .modal import Modal
ItemLike = Union[ItemCallbackType[Any, Any], Item[Any]]
_log = logging.getLogger(__name__)
@ -188,57 +192,18 @@ class _ViewCallback:
return self.callback(self.view, interaction, self.item)
class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
"""Represents a UI view.
This object must be inherited to create a UI within Discord.
.. versionadded:: 2.0
Parameters
-----------
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_view__: ClassVar[bool] = True
class BaseView:
__discord_ui_view__: ClassVar[bool] = False
__discord_ui_modal__: ClassVar[bool] = False
__discord_ui_container__: ClassVar[bool] = False
__view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = []
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, ItemCallbackType[Any, 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) > 25:
raise TypeError('View cannot have more than 25 children')
cls.__view_children_items__ = list(children.values())
def _init_children(self) -> List[Item[Self]]:
children = []
for func in self.__view_children_items__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = _ViewCallback(func, self, item) # type: ignore
item._view = self
setattr(self, func.__name__, item)
children.append(item)
return children
__view_children_items__: ClassVar[List[ItemLike]] = []
def __init__(self, *, timeout: Optional[float] = 180.0):
def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
self.__timeout = timeout
self._children: List[Item[Self]] = self._init_children()
self.__weights = _ViewWeights(self._children)
self.id: str = os.urandom(16).hex()
self._cache_key: Optional[int] = None
self.__cancel_callback: Optional[Callable[[View], None]] = None
self.__cancel_callback: Optional[Callable[[BaseView], None]] = None
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()
@ -246,12 +211,32 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
def _is_v2(self) -> bool:
return False
@property
def width(self):
return 5
def __repr__(self) -> str:
return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>'
return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}'
def _init_children(self) -> List[Item[Self]]:
children = []
for raw in self.__view_children_items__:
if isinstance(raw, Item):
raw._view = self
parent = getattr(raw, '__discord_ui_parent__', None)
if parent and parent._view is None:
parent._view = self
item = raw
else:
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
item.callback = _ViewCallback(raw, self, item) # type: ignore
item._view = self
setattr(self, raw.__name__, item)
parent = getattr(raw, '__discord_ui_parent__', None)
if parent:
if not self._is_v2():
raise RuntimeError('This view cannot have v2 items')
parent._children.append(item)
children.append(item)
return children
async def __timeout_task_impl(self) -> None:
while True:
@ -279,24 +264,7 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
return any(c._is_v2() for c in self.children)
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
return item._rendered_row or 0
children = sorted(self._children, key=key)
components: List[Dict[str, Any]] = []
for _, group in groupby(children, key=key):
children = [item.to_component_dict() for item in group]
if not children:
continue
components.append(
{
'type': 1,
'components': children,
}
)
return components
return NotImplemented
def _refresh_timeout(self) -> None:
if self.__timeout:
@ -327,7 +295,7 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
return self._children.copy()
@classmethod
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View:
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> Any:
"""Converts a message's components into a :class:`View`.
The :attr:`.Message.components` of a message are read-only
@ -341,28 +309,8 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
The message with components to convert into a view.
timeout: Optional[:class:`float`]
The timeout of the converted view.
Returns
--------
:class:`View`
The converted view. This always returns a :class:`View` and not
one of its subclasses.
"""
view = View(timeout=timeout)
row = 0
for component in message.components:
if isinstance(component, ActionRowComponent):
for child in component.children:
item = _component_to_item(child)
item.row = row
view.add_item(item)
row += 1
else:
item = _component_to_item(component)
item.row = row
view.add_item(item)
return view
pass
def add_item(self, item: Item[Any]) -> Self:
"""Adds an item to the view.
@ -385,18 +333,10 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
you tried to add is not allowed in this View.
"""
if len(self._children) >= 25:
raise ValueError('maximum number of children exceeded')
if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__.__name__}')
if item._is_v2() and not self._is_v2():
raise ValueError(
'The item can only be added on LayoutView'
)
self.__weights.add_item(item)
raise ValueError('v2 items cannot be added to this view')
item._view = self
self._children.append(item)
@ -418,8 +358,6 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
self._children.remove(item)
except ValueError:
pass
else:
self.__weights.remove_item(item)
return self
def clear_items(self) -> Self:
@ -429,9 +367,30 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
chaining.
"""
self._children.clear()
self.__weights.clear()
return self
def get_item_by_id(self, id: str, /) -> Optional[Item[Self]]:
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
not found.
.. warning::
This is **not the same** as ``custom_id``.
.. versionadded:: 2.6
Parameters
----------
id: :class:`str`
The ID of the component.
Returns
-------
Optional[:class:`Item`]
The item found, or ``None``.
"""
return _utils_get(self._children, id=id)
async def interaction_check(self, interaction: Interaction, /) -> bool:
"""|coro|
@ -599,61 +558,167 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
return await self.__stopped
class LayoutView(View):
__view_children_items__: ClassVar[List[Item[Any]]] = []
__view_pending_children__: ClassVar[List[ItemCallbackType[Any, Any]]] = []
class View(BaseView): # NOTE: maybe add a deprecation warning in favour of LayoutView?
"""Represents a UI view.
This object must be inherited to create a UI within Discord.
.. versionadded:: 2.0
Parameters
-----------
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_view__: ClassVar[bool] = True
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, ItemCallbackType[Any, 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) > 25:
raise TypeError('View cannot have more than 25 children')
cls.__view_children_items__ = list(children.values())
def __init__(self, *, timeout: Optional[float] = 180.0):
super().__init__(timeout=timeout)
self.__weights = _ViewWeights(self._children)
@property
def width(self):
return 5
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
return item._rendered_row or 0
children = sorted(self._children, key=key)
components: List[Dict[str, Any]] = []
for _, group in groupby(children, key=key):
children = [item.to_component_dict() for item in group]
if not children:
continue
components.append(
{
'type': 1,
'components': children,
}
)
return components
@classmethod
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View:
"""Converts a message's components into a :class:`View`.
The :attr:`.Message.components` of a message are read-only
and separate types from those in the ``discord.ui`` namespace.
In order to modify and edit message components they must be
converted into a :class:`View` first.
Parameters
-----------
message: :class:`discord.Message`
The message with components to convert into a view.
timeout: Optional[:class:`float`]
The timeout of the converted view.
Returns
--------
:class:`View`
The converted view. This always returns a :class:`View` and not
one of its subclasses.
"""
view = View(timeout=timeout)
row = 0
for component in message.components:
if isinstance(component, ActionRowComponent):
for child in component.children:
item = _component_to_item(child)
item.row = row
if item._is_v2():
raise RuntimeError('v2 components cannot be added to this View')
view.add_item(item)
row += 1
else:
item = _component_to_item(component)
item.row = row
if item._is_v2():
raise RuntimeError('v2 components cannot be added to this View')
view.add_item(item)
return view
def __init__(self, *, timeout: Optional[float] = 180) -> None:
def add_item(self, item: Item[Any]) -> Self:
if len(self._children) >= 25:
raise ValueError('maximum number of children exceeded')
super().add_item(item)
try:
self.__weights.add_item(item)
except ValueError as e:
# if the item has no space left then remove it from _children
self._children.remove(item)
raise e
return self
def remove_item(self, item: Item[Any]) -> Self:
try:
self._children.remove(item)
except ValueError:
pass
else:
self.__weights.remove_item(item)
return self
def clear_items(self) -> Self:
super().clear_items()
self.__weights.clear()
return self
class LayoutView(BaseView):
"""Represents a layout view for components v2.
Unline :class:`View` this allows for components v2 to exist
within it.
.. versionadded:: 2.6
Parameters
----------
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.
"""
def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
super().__init__(timeout=timeout)
self.__weights.weights.extend([0, 0, 0, 0, 0])
def __init_subclass__(cls) -> None:
children: Dict[str, Item[Any]] = {}
pending: Dict[str, ItemCallbackType[Any, Any]] = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if isinstance(member, Item):
children[name] = member
elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None):
pending[name] = member
children[name] = member
if len(children) > 10:
raise TypeError('LayoutView cannot have more than 10 top-level children')
cls.__view_children_items__ = list(children.values())
cls.__view_pending_children__ = list(pending.values())
def _init_children(self) -> List[Item[Self]]:
children = []
for func in self.__view_pending_children__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = _ViewCallback(func, self, item)
item._view = self
setattr(self, func.__name__, item)
parent: ActionRow = func.__discord_ui_parent__
parent.add_item(item)
for i in self.__view_children_items__:
if isinstance(i, Item):
if getattr(i, '_parent', None):
# this is for ActionRows which have decorators such as
# @action_row.button and @action_row.select that will convert
# those callbacks into their types but will have a _parent
# attribute which is checked here so the item is not added twice
continue
i._view = self
if getattr(i, '__discord_ui_action_row__', False):
i._update_children_view(self) # type: ignore
children.append(i)
else:
# guard just in case
raise TypeError(
'LayoutView can only have items'
)
return children
def _is_v2(self) -> bool:
return True
@ -670,11 +735,49 @@ class LayoutView(View):
return child
def add_item(self, item: Item[Any]) -> Self:
if len(self._children) >= 10:
raise ValueError('maximum number of children exceeded')
super().add_item(item)
return self
@classmethod
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> LayoutView:
"""Converts a message's components into a :class:`LayoutView`.
The :attr:`.Message.components` of a message are read-only
and separate types from those in the ``discord.ui`` namespace.
In order to modify and edit message components they must be
converted into a :class:`LayoutView` first.
Unlike :meth:`View.from_message` this works for
Parameters
-----------
message: :class:`discord.Message`
The message with components to convert into a view.
timeout: Optional[:class:`float`]
The timeout of the converted view.
Returns
--------
:class:`LayoutView`
The converted view. This always returns a :class:`LayoutView` and not
one of its subclasses.
"""
view = LayoutView(timeout=timeout)
for component in message.components:
item = _component_to_item(component)
item.row = 0
view.add_item(item)
return view
class ViewStore:
def __init__(self, state: ConnectionState):
# entity_id: {(component_type, custom_id): Item}
self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[View]]] = {}
self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[BaseView]]] = {}
# message_id: View
self._synced_message_views: Dict[int, View] = {}
# custom_id: Modal
@ -684,7 +787,7 @@ class ViewStore:
self._state: ConnectionState = state
@property
def persistent_views(self) -> Sequence[View]:
def persistent_views(self) -> Sequence[BaseView]:
# fmt: off
views = {
item.view.id: item.view
@ -722,7 +825,7 @@ class ViewStore:
is_fully_dynamic = item._update_store_data( # type: ignore
dispatch_info,
self._dynamic_items,
)
) or is_fully_dynamic
elif getattr(item, '__discord_ui_action_row__', False):
is_fully_dynamic = item._update_store_data( # type: ignore
dispatch_info,
@ -784,7 +887,7 @@ class ViewStore:
return
# Swap the item in the view with our new dynamic item
view._children[base_item_index] = item
view._children[base_item_index] = item # type: ignore
item._view = view
item._rendered_row = base_item._rendered_row
item._refresh_state(interaction, interaction.data) # type: ignore
@ -826,7 +929,7 @@ class ViewStore:
key = (component_type, custom_id)
# The entity_id can either be message_id, interaction_id, or None in that priority order.
item: Optional[Item[View]] = None
item: Optional[Item[BaseView]] = None
if message_id is not None:
item = self._views.get(message_id, {}).get(key)
@ -878,7 +981,7 @@ class ViewStore:
def is_message_tracked(self, message_id: int) -> bool:
return message_id in self._synced_message_views
def remove_message_tracking(self, message_id: int) -> Optional[View]:
def remove_message_tracking(self, message_id: int) -> Optional[BaseView]:
return self._synced_message_views.pop(message_id, None)
def update_from_message(self, message_id: int, data: List[ComponentBasePayload]) -> None:

Loading…
Cancel
Save