Skip to content

Commit b319d13

Browse files
authored
skeleton for actionrow
1 parent 027527d commit b319d13

File tree

1 file changed

+285
-0
lines changed

1 file changed

+285
-0
lines changed

discord/ui/action_row.py

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
from __future__ import annotations
2+
3+
from functools import partial
4+
from typing import TYPE_CHECKING, ClassVar, Iterator, TypeVar
5+
6+
from ..colour import Colour
7+
from ..components import ActionRow as ActionRowComponent
8+
from ..components import Container as ContainerComponent
9+
from ..components import _component_factory
10+
from ..enums import ComponentType, SeparatorSpacingSize
11+
from ..utils import find, get
12+
from .file import File
13+
from .item import Item, ItemCallbackType
14+
from .media_gallery import MediaGallery
15+
from .section import Section
16+
from .separator import Separator
17+
from .text_display import TextDisplay
18+
from .view import _walk_all_components
19+
20+
__all__ = ("Container",)
21+
22+
if TYPE_CHECKING:
23+
from typing_extensions import Self
24+
25+
from ..types.components import ContainerComponent as ContainerComponentPayload
26+
from .view import View
27+
28+
29+
AC = TypeVar("A", bound="ActionRow")
30+
V = TypeVar("V", bound="View", covariant=True)
31+
32+
33+
class ActionRow(Item[V]):
34+
"""Represents a UI Action Row.
35+
36+
The items supported are as follows:
37+
38+
- :class:`discord.ui.Select`
39+
- :class:`discord.ui.Button` (in views)
40+
- :class:`discord.ui.InputText` (in modals)
41+
42+
.. versionadded:: 2.7
43+
44+
Parameters
45+
----------
46+
*items: :class:`Item`
47+
The initial items in this action row.
48+
id: Optional[:class:`int`]
49+
The action's ID.
50+
"""
51+
52+
__item_repr_attributes__: tuple[str, ...] = (
53+
"items",
54+
"id",
55+
)
56+
57+
__row_children_items__: ClassVar[list[ItemCallbackType]] = []
58+
59+
def __init_subclass__(cls) -> None:
60+
children: list[ItemCallbackType] = []
61+
for base in reversed(cls.__mro__):
62+
for member in base.__dict__.values():
63+
if hasattr(member, "__discord_ui_model_type__"):
64+
children.append(member)
65+
66+
cls.__row_children_items__ = children
67+
68+
def __init__(
69+
self,
70+
*items: Item,
71+
id: int | None = None,
72+
):
73+
super().__init__()
74+
75+
self.items: list[Item] = []
76+
77+
self._underlying = ActionRowComponent._raw_construct(
78+
type=ComponentType.action_row,
79+
id=id,
80+
components=[],
81+
)
82+
83+
for func in self.__row_children_items__:
84+
item: Item = func.__discord_ui_model_type__(
85+
**func.__discord_ui_model_kwargs__
86+
)
87+
item.callback = partial(func, self, item)
88+
self.add_item(item)
89+
setattr(self, func.__name__, item)
90+
for i in items:
91+
self.add_item(i)
92+
93+
def _add_component_from_item(self, item: Item):
94+
self._underlying.components.append(item._underlying)
95+
96+
def _set_components(self, items: list[Item]):
97+
self._underlying.components.clear()
98+
for item in items:
99+
self._add_component_from_item(item)
100+
101+
def add_item(self, item: Item) -> Self:
102+
"""Adds an item to the action row.
103+
104+
Parameters
105+
----------
106+
item: :class:`Item`
107+
The item to add to the action row.
108+
109+
Raises
110+
------
111+
TypeError
112+
An :class:`Item` was not passed.
113+
"""
114+
115+
if not isinstance(item, Item):
116+
raise TypeError(f"expected Item not {item.__class__!r}")
117+
118+
item._view = self.view
119+
item.parent = self
120+
121+
self.items.append(item)
122+
self._add_component_from_item(item)
123+
return self
124+
125+
def remove_item(self, item: Item | str | int) -> Self:
126+
"""Removes an item from the action row. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively.
127+
128+
Parameters
129+
----------
130+
item: Union[:class:`Item`, :class:`int`, :class:`str`]
131+
The item, ``id``, or item ``custom_id`` to remove from the action row.
132+
"""
133+
134+
if isinstance(item, (str, int)):
135+
item = self.get_item(item)
136+
try:
137+
self.items.remove(item)
138+
except ValueError:
139+
pass
140+
return self
141+
142+
def get_item(self, id: str | int) -> Item | None:
143+
"""Get an item from this action row. Roughly equivalent to `utils.get(row.items, ...)`.
144+
If an ``int`` is provided, the item will be retrieved by ``id``, otherwise by ``custom_id``.
145+
146+
Parameters
147+
----------
148+
id: Union[:class:`str`, :class:`int`]
149+
The id or custom_id of the item to get.
150+
151+
Returns
152+
-------
153+
Optional[:class:`Item`]
154+
The item with the matching ``id`` or ``custom_id`` if it exists.
155+
"""
156+
if not id:
157+
return None
158+
attr = "id" if isinstance(id, int) else "custom_id"
159+
child = find(lambda i: getattr(i, attr, None) == id, self.items)
160+
return child
161+
162+
def add_button(
163+
self,
164+
*,
165+
accessory: Item,
166+
id: int | None = None,
167+
) -> Self:
168+
"""Adds a :class:`Button` to the action row.
169+
170+
To append a pre-existing :class:`Button`, use the
171+
:meth:`add_item` method, instead.
172+
173+
Parameters
174+
----------
175+
*items: :class:`Item`
176+
The items contained in this section, up to 3.
177+
Currently only supports :class:`~discord.ui.TextDisplay`.
178+
accessory: Optional[:class:`Item`]
179+
The section's accessory. This is displayed in the top right of the section.
180+
Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`.
181+
id: Optional[:class:`int`]
182+
The section's ID.
183+
"""
184+
185+
section = Section(*items, accessory=accessory, id=id)
186+
187+
return self.add_item(section)
188+
189+
def add_select(self, url: str, spoiler: bool = False, id: int | None = None) -> Self:
190+
"""Adds a :class:`TextDisplay` to the container.
191+
192+
Parameters
193+
----------
194+
url: :class:`str`
195+
The URL of this file's media. This must be an ``attachment://`` URL that references a :class:`~discord.File`.
196+
spoiler: Optional[:class:`bool`]
197+
Whether the file has the spoiler overlay. Defaults to ``False``.
198+
id: Optiona[:class:`int`]
199+
The file's ID.
200+
"""
201+
202+
f = File(url, spoiler=spoiler, id=id)
203+
204+
return self.add_item(f)
205+
206+
@Item.view.setter
207+
def view(self, value):
208+
self._view = value
209+
for item in self.items:
210+
item.parent = self
211+
item._view = value
212+
213+
@property
214+
def type(self) -> ComponentType:
215+
return self._underlying.type
216+
217+
@property
218+
def width(self) -> int:
219+
return 5
220+
221+
def is_dispatchable(self) -> bool:
222+
return any(item.is_dispatchable() for item in self.items)
223+
224+
def is_persistent(self) -> bool:
225+
return all(item.is_persistent() for item in self.items)
226+
227+
def refresh_component(self, component: ActionRowComponent) -> None:
228+
self._underlying = component
229+
i = 0
230+
for y in component.components:
231+
x = self.items[i]
232+
x.refresh_component(y)
233+
i += 1
234+
235+
def disable_all_items(self, *, exclusions: list[Item] | None = None) -> Self:
236+
"""
237+
Disables all items in this row.
238+
239+
Parameters
240+
----------
241+
exclusions: Optional[List[:class:`Item`]]
242+
A list of items in `self.items` to not disable from the view.
243+
"""
244+
for item in self.walk_items():
245+
if exclusions is None or item not in exclusions:
246+
item.disabled = True
247+
return self
248+
249+
def enable_all_items(self, *, exclusions: list[Item] | None = None) -> Self:
250+
"""
251+
Enables all buttons and select menus in the container.
252+
253+
Parameters
254+
----------
255+
exclusions: Optional[List[:class:`Item`]]
256+
A list of items in `self.items` to not enable from the view.
257+
"""
258+
for item in self.walk_items():
259+
if hasattr(item, "disabled") and (
260+
exclusions is None or item not in exclusions
261+
):
262+
item.disabled = False
263+
return self
264+
265+
def walk_items(self) -> Iterator[Item]:
266+
for item in self.items:
267+
yield item
268+
269+
def to_component_dict(self) -> ContainerComponentPayload:
270+
self._set_components(self.items)
271+
return self._underlying.to_dict()
272+
273+
@classmethod
274+
def from_component(cls: type[C], component: ActionRowComponent) -> C:
275+
from .view import _component_to_item
276+
277+
items = [
278+
_component_to_item(c) for c in _walk_all_components(component.components)
279+
]
280+
return cls(
281+
*items,
282+
id=component.id,
283+
)
284+
285+
callback = None

0 commit comments

Comments
 (0)