Skip to content

Commit a9671ae

Browse files
authored
container
1 parent 88c65fb commit a9671ae

File tree

7 files changed

+281
-5
lines changed

7 files changed

+281
-5
lines changed

discord/components.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,27 @@ def __init__(self, data: ComponentPayload):
169169
_component_factory(d) for d in data.get("components", [])
170170
]
171171

172+
@property
173+
def width(self):
174+
"""Return the total item width used by this action row."""
175+
t = 0
176+
for item in self.children:
177+
t += 1 if item.type is ComponentType.button else 5
178+
return t
179+
172180
def to_dict(self) -> ActionRowPayload:
173181
return {
174182
"type": int(self.type),
175183
"components": [child.to_dict() for child in self.children],
176184
} # type: ignore
177185

186+
@classmethod
187+
def with_components(cls, *components):
188+
return cls._raw_construct(
189+
type=ComponentType.action_row,
190+
id=None,
191+
children=[c for c in components]
192+
)
178193

179194
class InputText(Component):
180195
"""Represents an Input Text field from the Discord Bot UI Kit.

discord/ui/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@
1818
from .text_display import *
1919
from .thumbnail import *
2020
from .view import *
21+
from .file import *
22+
from .separator import *
23+
from .container import *

discord/ui/container.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, TypeVar
4+
5+
from ..colour import Colour
6+
from ..components import Container as ContainerComponent
7+
from ..components import _component_factory, ActionRow
8+
from ..enums import ComponentType, SeparatorSpacingSize
9+
from .item import Item
10+
from .text_display import TextDisplay
11+
from .section import Section
12+
from .media_gallery import MediaGallery
13+
from .separator import Separator
14+
from .file import File
15+
16+
__all__ = ("Container",)
17+
18+
if TYPE_CHECKING:
19+
from ..types.components import ContainerComponent as ContainerComponentPayload
20+
from .view import View
21+
22+
23+
C = TypeVar("C", bound="Container")
24+
V = TypeVar("V", bound="View", covariant=True)
25+
26+
27+
class Container(Item[V]):
28+
"""Represents a UI Container. Containers may contain up to 10 items.
29+
30+
The current items supported are:
31+
32+
- :class:`discord.ui.Button`
33+
- :class:`discord.ui.Select`
34+
- :class:`discord.ui.Section`
35+
- :class:`discord.ui.TextDisplay`
36+
- :class:`discord.ui.MediaGallery`
37+
- :class:`discord.ui.File`
38+
- :class:`discord.ui.Separator`
39+
40+
41+
.. versionadded:: 2.7
42+
43+
Parameters
44+
----------
45+
*items: :class:`Item`
46+
The initial items in this container, up to 10.
47+
colour: Union[:class:`Colour`, :class:`int`]
48+
The accent colour of the container. Aliased to ``color`` as well.
49+
"""
50+
51+
def __init__(
52+
self,
53+
*items: Item,
54+
colour: int | Colour | None = None,
55+
color: int | Colour | None = None,
56+
spoiler: bool = False
57+
):
58+
super().__init__()
59+
60+
self.items = [i for i in items]
61+
components = [i._underlying for i in items]
62+
self._color = colour
63+
64+
self._underlying = ContainerComponent._raw_construct(
65+
type=ComponentType.section,
66+
id=None,
67+
components=components,
68+
accent_color=colour,
69+
spoiler=spoiler
70+
)
71+
72+
def add_item(self, item: Item) -> None:
73+
"""Adds an item to the container.
74+
75+
Parameters
76+
----------
77+
item: :class:`Item`
78+
The item to add to the container.
79+
80+
Raises
81+
------
82+
TypeError
83+
An :class:`Item` was not passed.
84+
ValueError
85+
Maximum number of items has been exceeded (10).
86+
"""
87+
88+
if len(self.items) >= 10:
89+
raise ValueError("maximum number of children exceeded")
90+
91+
if not isinstance(item, Item):
92+
raise TypeError(f"expected Item not {item.__class__!r}")
93+
94+
self.items.append(item)
95+
96+
# reuse weight system?
97+
98+
if item._underlying.is_v2():
99+
self._underlying.components.append(item._underlying)
100+
else:
101+
found = False
102+
for i in range(len(self._underlying.components) - 1, 0, -1):
103+
row = self._underlying.components[i]
104+
if isinstance(row, ActionRow) and row.width + item.width <= 5: # If an actionRow exists
105+
row.children.append(item._underlying)
106+
found = True
107+
if not found:
108+
row = ActionRow.with_components(item._underlying)
109+
self._underlying.components.append(row)
110+
111+
def add_section(
112+
self,
113+
*items: Item, accessory: Item = None
114+
):
115+
"""Adds a :class:`Section` to the container.
116+
117+
To append a pre-existing :class:`Section` use the
118+
:meth:`add_item` method instead.
119+
120+
Parameters
121+
----------
122+
*items: :class:`Item`
123+
The items contained in this section, up to 3.
124+
Currently only supports :class:`~discord.ui.TextDisplay`.
125+
accessory: Optional[:class:`Item`]
126+
The section's accessory. This is displayed in the top right of the section.
127+
Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`.
128+
"""
129+
# accept raw strings?
130+
131+
section = Section(
132+
*items, accessory=accessory
133+
)
134+
135+
self.add_item(section)
136+
137+
def add_text(self, content: str) -> None:
138+
"""Adds a :class:`TextDisplay` to the container.
139+
140+
Parameters
141+
----------
142+
content: :class:`str`
143+
The content of the TextDisplay
144+
"""
145+
146+
text = TextDisplay(content)
147+
148+
self.add_item(text)
149+
150+
def add_gallery(
151+
self,
152+
*items: Item,
153+
):
154+
"""Adds a :class:`MediaGallery` to the container.
155+
156+
To append a pre-existing :class:`MediaGallery` use the
157+
:meth:`add_item` method instead.
158+
159+
Parameters
160+
----------
161+
*items: List[:class:`MediaGalleryItem`]
162+
The media this gallery contains.
163+
"""
164+
# accept raw urls?
165+
166+
g = MediaGallery(
167+
*items
168+
)
169+
170+
self.add_item(g)
171+
172+
def add_file(self, url: str, spoiler: bool = False) -> None:
173+
"""Adds a :class:`TextDisplay` to the container.
174+
175+
Parameters
176+
----------
177+
url: :class:`str`
178+
The URL of this file's media. This must be an ``attachment://`` URL that references a :class:`~discord.File`.
179+
spoiler: Optional[:class:`bool`]
180+
Whether the file is a spoiler. Defaults to ``False``.
181+
"""
182+
183+
f = File(url, spoiler=spoiler)
184+
185+
self.add_item(f)
186+
187+
def add_separator(self, *, divider: bool = True, spacing: SeparatorSpacingSize = SeparatorSpacingSize.small) -> None:
188+
"""Adds a :class:`Separator` to the container.
189+
190+
Parameters
191+
----------
192+
divider: :class:`bool`
193+
Whether the separator is a divider. Defaults to ``True``.
194+
spacing: :class:`~discord.SeparatorSpacingSize`
195+
The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`.
196+
"""
197+
198+
s = Separator(divider=divider, spacing=spacing)
199+
200+
self.add_item(s)
201+
202+
@property
203+
def spoiler(self) -> bool:
204+
"""Whether the container is a spoiler. Defaults to ``False``."""
205+
return self._spoiler
206+
207+
@spoiler.setter
208+
def spoiler(self, spoiler: bool) -> None:
209+
self._spoiler = spoiler
210+
self._underlying.spoiler = spoiler
211+
212+
@property
213+
def colour(self) -> Colour | None:
214+
return getattr(self, "_colour", None)
215+
216+
@colour.setter
217+
def colour(self, value: int | Colour | None): # type: ignore
218+
if value is None or isinstance(value, Colour):
219+
self._colour = value
220+
elif isinstance(value, int):
221+
self._colour = Colour(value=value)
222+
else:
223+
raise TypeError(
224+
"Expected discord.Colour, int, or None but received"
225+
f" {value.__class__.__name__} instead."
226+
)
227+
self._underlying.accent_color = self.colour
228+
229+
color = colour
230+
231+
@property
232+
def type(self) -> ComponentType:
233+
return self._underlying.type
234+
235+
@property
236+
def width(self) -> int:
237+
return 5
238+
239+
def to_component_dict(self) -> ContainerComponentPayload:
240+
return self._underlying.to_dict()
241+
242+
@classmethod
243+
def from_component(cls: type[C], component: ContainerComponent) -> C:
244+
from .view import _component_to_item
245+
246+
items = [_component_to_item(c) for c in component.components]
247+
return cls(*items, colour=component.accent_color, spoiler=component.spoiler)
248+
249+
callback = None

discord/ui/file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def url(self, value: str) -> None:
6161
@property
6262
def spoiler(self) -> bool:
6363
"""Whether the file is a spoiler. Defaults to ``False``."""
64-
return self.spoiler
64+
return self._spoiler
6565

6666
@spoiler.setter
6767
def spoiler(self, spoiler: bool) -> None:

discord/ui/section.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class Section(Item[V]):
3030
The initial items contained in this section, up to 3.
3131
Currently only supports :class:`~discord.ui.TextDisplay`.
3232
accessory: Optional[:class:`Item`]
33-
This section's accessory. This is displayed in the top right of the section.
33+
The section's accessory. This is displayed in the top right of the section.
3434
Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`.
3535
"""
3636

discord/ui/thumbnail.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def description(self, description: str | None) -> None:
7474
@property
7575
def spoiler(self) -> bool:
7676
"""Whether the thumbnail is a spoiler. Defaults to ``False``."""
77-
return self.spoiler
77+
return self._spoiler
7878

7979
@spoiler.setter
8080
def spoiler(self, spoiler: bool) -> None:

discord/ui/view.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@
3636

3737
from ..components import ActionRow as ActionRowComponent
3838
from ..components import Button as ButtonComponent
39-
from ..components import Component, FileComponent
4039
from ..components import MediaGallery as MediaGalleryComponent
4140
from ..components import Section as SectionComponent
4241
from ..components import SelectMenu as SelectComponent
4342
from ..components import TextDisplay as TextDisplayComponent
4443
from ..components import Thumbnail as ThumbnailComponent
45-
from ..components import _component_factory
44+
from ..components import Separator as SeparatorComponent
45+
from ..components import Container as ContainerComponent
46+
from ..components import _component_factory, Component, FileComponent
4647
from ..utils import get
4748
from .item import Item, ItemCallbackType
4849

@@ -93,6 +94,14 @@ def _component_to_item(component: Component) -> Item:
9394
from .file import File
9495

9596
return File.from_component(component)
97+
if isinstance(component, SeparatorComponent):
98+
from .separator import Separator
99+
100+
return Separator.from_component(component)
101+
if isinstance(component, ContainerComponent):
102+
from .container import Container
103+
104+
return Container.from_component(component)
96105
return Item.from_component(component)
97106

98107

0 commit comments

Comments
 (0)