"""
The MIT License (MIT)
Copyright (c) 2021-present Pycord Development
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 collections.abc import Sequence
from functools import partial
from typing import TYPE_CHECKING, ClassVar, Iterator, Literal, TypeVar, overload
from ..components import ActionRow as ActionRowComponent
from ..components import SelectDefaultValue, SelectOption, _component_factory
from ..enums import ButtonStyle, ChannelType, ComponentType
from ..utils import find, get
from .button import Button
from .file import File
from .item import ItemCallbackType, ViewItem
from .select import Select
__all__ = ("ActionRow",)
if TYPE_CHECKING:
from typing_extensions import Self
from ..emoji import AppEmoji, GuildEmoji
from ..partial_emoji import PartialEmoji, _EmojiTag
from ..types.components import ActionRow as ActionRowPayload
from .view import DesignerView
A = TypeVar("A", bound="ActionRow")
V = TypeVar("V", bound="DesignerView", covariant=True)
[docs]
class ActionRow(ViewItem[V]):
"""Represents a UI Action Row used in :class:`discord.ui.DesignerView`.
The items supported are as follows:
- :class:`discord.ui.Select`
- :class:`discord.ui.Button`
.. versionadded:: 2.7
Parameters
----------
*items: :class:`ViewItem`
The initial items in this action row.
id: Optional[:class:`int`]
The action's ID.
"""
__item_repr_attributes__: tuple[str, ...] = (
"children",
"id",
)
__row_children_items__: ClassVar[list[ItemCallbackType]] = []
def __init_subclass__(cls) -> None:
children: list[ItemCallbackType] = []
for base in reversed(cls.__mro__):
for member in base.__dict__.values():
if hasattr(member, "__discord_ui_model_type__"):
children.append(member)
cls.__row_children_items__ = children
def __init__(
self,
*items: ViewItem,
id: int | None = None,
):
super().__init__()
self.children: list[ViewItem] = []
self._underlying = self._generate_underlying(id=id)
for func in self.__row_children_items__:
item: ViewItem = func.__discord_ui_model_type__(
**func.__discord_ui_model_kwargs__
)
item.callback = partial(func, self, item)
self.add_item(item)
setattr(self, func.__name__, item)
for i in items:
self.add_item(i)
@property
def items(self) -> list[ViewItem]:
return self.children
@items.setter
def items(self, value: list[ViewItem]) -> None:
self.children = value
def _add_component_from_item(self, item: ViewItem):
self.underlying.children.append(item._generate_underlying())
def _set_components(self, items: list[ViewItem]):
self.underlying.children.clear()
for item in items:
self._add_component_from_item(item)
def _generate_underlying(self, id: int | None = None) -> ActionRowComponent:
super()._generate_underlying(ActionRowComponent)
row = ActionRowComponent._raw_construct(
type=ComponentType.action_row,
id=id or self.id,
children=[],
)
for i in self.children:
row.children.append(i._generate_underlying())
return row
[docs]
def add_item(self, item: ViewItem) -> Self:
"""Adds an item to the action row.
Parameters
----------
item: :class:`ViewItem`
The item to add to the action row.
Raises
------
TypeError
A :class:`ViewItem` was not passed.
"""
if not isinstance(item, (Select, Button)):
raise TypeError(f"expected Select or Button, not {item.__class__!r}")
if item.row:
raise ValueError(f"{item.__class__!r}.row is not supported in ActionRow")
if self.width + item.width > 5:
raise ValueError(f"Not enough space left on this ActionRow")
item.parent = self
self.children.append(item)
self._add_component_from_item(item)
return self
[docs]
def remove_item(self, item: ViewItem | str | int) -> Self:
"""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.
Parameters
----------
item: Union[:class:`ViewItem`, :class:`int`, :class:`str`]
The item, ``id``, or item ``custom_id`` to remove from the action row.
"""
if isinstance(item, (str, int)):
item = self.get_item(item)
try:
self.children.remove(item)
item.parent = None
except ValueError:
pass
return self
[docs]
def get_item(self, id: str | int) -> ViewItem | None:
"""Get an item from this action row. Roughly equivalent to `utils.get(row.children, ...)`.
If an ``int`` is provided, the item will be retrieved by ``id``, otherwise by ``custom_id``.
Parameters
----------
id: Union[:class:`str`, :class:`int`]
The id or custom_id of the item to get.
Returns
-------
Optional[:class:`ViewItem`]
The item with the matching ``id`` or ``custom_id`` if it exists.
"""
if not id:
return None
attr = "id" if isinstance(id, int) else "custom_id"
child = find(lambda i: getattr(i, attr, None) == id, self.children)
return child
@overload
def add_select(
self,
select_type: Literal[ComponentType.string_select] = ...,
*,
custom_id: str | None = ...,
placeholder: str | None = ...,
min_values: int = ...,
max_values: int = ...,
options: list[SelectOption] | None = ...,
disabled: bool = ...,
id: int | None = ...,
) -> None: ...
@overload
def add_select(
self,
select_type: Literal[ComponentType.channel_select] = ...,
*,
custom_id: str | None = ...,
placeholder: str | None = ...,
min_values: int = ...,
max_values: int = ...,
channel_types: list[ChannelType] | None = ...,
disabled: bool = ...,
id: int | None = ...,
default_values: Sequence[SelectDefaultValue] | None = ...,
) -> None: ...
@overload
def add_select(
self,
select_type: Literal[
ComponentType.user_select,
ComponentType.role_select,
ComponentType.mentionable_select,
] = ...,
*,
custom_id: str | None = ...,
placeholder: str | None = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
id: int | None = ...,
default_values: Sequence[SelectDefaultValue] | None = ...,
) -> None: ...
[docs]
def add_select(
self,
select_type: ComponentType = ComponentType.string_select,
*,
custom_id: str | None = None,
placeholder: str | None = None,
min_values: int = 1,
max_values: int = 1,
options: list[SelectOption] | None = None,
channel_types: list[ChannelType] | None = None,
disabled: bool = False,
id: int | None = None,
default_values: Sequence[SelectDefaultValue] | None = None,
) -> Self:
"""Adds a :class:`Select` to the action row.
To append a pre-existing :class:`Select`, use the
:meth:`add_item` method instead.
Parameters
----------
select_type: :class:`discord.ComponentType`
The type of select to create. Must be one of
:attr:`discord.ComponentType.string_select`, :attr:`discord.ComponentType.user_select`,
:attr:`discord.ComponentType.role_select`, :attr:`discord.ComponentType.mentionable_select`,
or :attr:`discord.ComponentType.channel_select`.
custom_id: :class:`str`
The custom ID of the select menu that gets received during an interaction.
If not given then one is generated for you.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
The maximum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
options: List[:class:`discord.SelectOption`]
A list of options that can be selected in this menu.
Only valid for selects of type :attr:`discord.ComponentType.string_select`.
channel_types: List[:class:`discord.ChannelType`]
A list of channel types that can be selected in this menu.
Only valid for selects of type :attr:`discord.ComponentType.channel_select`.
disabled: :class:`bool`
Whether the select is disabled or not. Defaults to ``False``.
id: Optional[:class:`int`]
The select menu's ID.
default_values: Optional[Sequence[Union[:class:`discord.SelectDefaultValue`, :class:`discord.abc.Snowflake`]]]
The default values of this select. Only applicable if :attr:`.select_type` is not :attr:`discord.ComponentType.string_select`.
These can be either :class:`discord.SelectDefaultValue` instances or models, which will be converted into :class:`discord.SelectDefaultValue`
instances.
"""
select = Select(
select_type=select_type,
custom_id=custom_id,
placeholder=placeholder,
min_values=min_values,
max_values=max_values,
options=options or [],
channel_types=channel_types or [],
disabled=disabled,
id=id,
default_values=default_values,
)
return self.add_item(select)
def is_dispatchable(self) -> bool:
return any(item.is_dispatchable() for item in self.children)
def is_persistent(self) -> bool:
return all(item.is_persistent() for item in self.children)
def refresh_component(self, component: ActionRowComponent) -> None:
self.underlying = component
for i, y in enumerate(component.components):
x = self.children[i]
x.refresh_component(y)
[docs]
def disable_all_items(self, *, exclusions: list[ViewItem] | None = None) -> Self:
"""
Disables all items in the row.
Parameters
----------
exclusions: Optional[List[:class:`ViewItem`]]
A list of items in `self.children` to not disable.
"""
for item in self.walk_items():
if exclusions is None or item not in exclusions:
item.disabled = True
return self
[docs]
def enable_all_items(self, *, exclusions: list[ViewItem] | None = None) -> Self:
"""
Enables all items in the row.
Parameters
----------
exclusions: Optional[List[:class:`ViewItem`]]
A list of items in `self.children` to not enable.
"""
for item in self.walk_items():
if exclusions is None or item not in exclusions:
item.disabled = False
return self
@property
def width(self):
"""Return the sum of the items' widths."""
t = 0
for item in self.children:
t += 1 if item.underlying.type is ComponentType.button else 5
return t
def walk_items(self) -> Iterator[ViewItem]:
yield from self.children
def to_component_dict(self) -> ActionRowPayload:
self._underlying = self._generate_underlying()
return super().to_component_dict()
@classmethod
def from_component(cls: type[A], component: ActionRowComponent) -> A:
from .view import _component_to_item, _walk_all_components
items = [
_component_to_item(c) for c in _walk_all_components(component.components)
]
return cls(
*items,
id=component.id,
)
callback = None