Source code for discord.ui.container

from __future__ import annotations

from functools import partial
from typing import TYPE_CHECKING, ClassVar, Iterator, TypeVar

from ..colour import Colour
from ..components import ActionRow
from ..components import Container as ContainerComponent
from ..components import _component_factory
from ..enums import ComponentType, SeparatorSpacingSize
from ..utils import find
from .file import File
from .item import Item, ItemCallbackType
from .media_gallery import MediaGallery
from .section import Section
from .separator import Separator
from .text_display import TextDisplay
from .view import _walk_all_components

__all__ = ("Container",)

if TYPE_CHECKING:
    from typing_extensions import Self

    from ..types.components import ContainerComponent as ContainerComponentPayload
    from .view import View


C = TypeVar("C", bound="Container")
V = TypeVar("V", bound="View", covariant=True)


[docs] class Container(Item[V]): """Represents a UI Container. The current items supported are as follows: - :class:`discord.ui.Button` - :class:`discord.ui.Select` - :class:`discord.ui.Section` - :class:`discord.ui.TextDisplay` - :class:`discord.ui.MediaGallery` - :class:`discord.ui.File` - :class:`discord.ui.Separator` .. versionadded:: 2.7 Parameters ---------- *items: :class:`Item` The initial items in this container. colour: Union[:class:`Colour`, :class:`int`] The accent colour of the container. Aliased to ``color`` as well. spoiler: Optional[:class:`bool`] Whether this container has the spoiler overlay. id: Optional[:class:`int`] The container's ID. """ __item_repr_attributes__: tuple[str, ...] = ( "items", "colour", "spoiler", "id", ) __container_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.__container_children_items__ = children def __init__( self, *items: Item, colour: int | Colour | None = None, color: int | Colour | None = None, spoiler: bool = False, id: int | None = None, ): super().__init__() self.items: list[Item] = [] self._underlying = ContainerComponent._raw_construct( type=ComponentType.container, id=id, components=[], accent_color=None, spoiler=spoiler, ) self.color = colour or color for func in self.__container_children_items__: item: Item = 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) def _add_component_from_item(self, item: Item): if item._underlying.is_v2(): self._underlying.components.append(item._underlying) else: found = False for row in reversed(self._underlying.components): if isinstance(row, ActionRow) and row.width + item.width <= 5: # If a valid ActionRow exists row.children.append(item._underlying) found = True elif not isinstance(row, ActionRow): # create new row if last component is v2 break if not found: row = ActionRow.with_components(item._underlying) self._underlying.components.append(row) def _set_components(self, items: list[Item]): self._underlying.components.clear() for item in items: self._add_component_from_item(item)
[docs] def add_item(self, item: Item) -> Self: """Adds an item to the container. Parameters ---------- item: :class:`Item` The item to add to the container. Raises ------ TypeError An :class:`Item` was not passed. """ if not isinstance(item, Item): raise TypeError(f"expected Item not {item.__class__!r}") item._view = self.view if hasattr(item, "items"): item.view = self item.parent = self self.items.append(item) self._add_component_from_item(item) return self
[docs] def remove_item(self, item: Item | str | int) -> Self: """Removes an item from the container. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively. Parameters ---------- item: Union[:class:`Item`, :class:`int`, :class:`str`] The item, ``id``, or item ``custom_id`` to remove from the container. """ if isinstance(item, (str, int)): item = self.get_item(item) try: self.items.remove(item) except ValueError: pass return self
[docs] def get_item(self, id: str | int) -> Item | None: """Get an item from this container. Roughly equivalent to `utils.get(container.items, ...)`. If an ``int`` is provided, the item will be retrieved by ``id``, otherwise by ``custom_id``. This method will also search for nested items. Parameters ---------- id: Union[:class:`str`, :class:`int`] The id or custom_id of the item to get. Returns ------- Optional[:class:`Item`] 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.items) if not child: for i in self.items: if hasattr(i, "get_item"): if child := i.get_item(id): return child return child
[docs] def add_section( self, *items: Item, accessory: Item, id: int | None = None, ) -> Self: """Adds a :class:`Section` to the container. To append a pre-existing :class:`Section`, use the :meth:`add_item` method, instead. Parameters ---------- *items: :class:`Item` The items contained in this section, up to 3. Currently only supports :class:`~discord.ui.TextDisplay`. accessory: Optional[:class:`Item`] The section's accessory. This is displayed in the top right of the section. Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`. id: Optional[:class:`int`] The section's ID. """ section = Section(*items, accessory=accessory, id=id) return self.add_item(section)
[docs] def add_text(self, content: str, id: int | None = None) -> Self: """Adds a :class:`TextDisplay` to the container. Parameters ---------- content: :class:`str` The content of the TextDisplay id: Optiona[:class:`int`] The text displays' ID. """ text = TextDisplay(content, id=id) return self.add_item(text)
[docs] def add_file(self, url: str, spoiler: bool = False, id: int | None = None) -> Self: """Adds a :class:`TextDisplay` to the container. Parameters ---------- url: :class:`str` The URL of this file's media. This must be an ``attachment://`` URL that references a :class:`~discord.File`. spoiler: Optional[:class:`bool`] Whether the file has the spoiler overlay. Defaults to ``False``. id: Optiona[:class:`int`] The file's ID. """ f = File(url, spoiler=spoiler, id=id) return self.add_item(f)
[docs] def add_separator( self, *, divider: bool = True, spacing: SeparatorSpacingSize = SeparatorSpacingSize.small, id: int | None = None, ) -> Self: """Adds a :class:`Separator` to the container. Parameters ---------- divider: :class:`bool` Whether the separator is a divider. Defaults to ``True``. spacing: :class:`~discord.SeparatorSpacingSize` The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`. id: Optional[:class:`int`] The separator's ID. """ s = Separator(divider=divider, spacing=spacing, id=id) return self.add_item(s)
[docs] def copy_text(self) -> str: """Returns the text of all :class:`~discord.ui.TextDisplay` items in this container. Equivalent to the `Copy Text` option on Discord clients. """ return "\n".join(t for i in self.items if (t := i.copy_text()))
@property def spoiler(self) -> bool: """Whether the container has the spoiler overlay. Defaults to ``False``.""" return self._underlying.spoiler @spoiler.setter def spoiler(self, spoiler: bool) -> None: self._underlying.spoiler = spoiler @property def colour(self) -> Colour | None: return self._underlying.accent_color @colour.setter def colour(self, value: int | Colour | None): # type: ignore if value is None or isinstance(value, Colour): self._underlying.accent_color = value elif isinstance(value, int): self._underlying.accent_color = Colour(value=value) else: raise TypeError(f"Expected discord.Colour, int, or None but received {value.__class__.__name__} instead.") color = colour @Item.view.setter def view(self, value): self._view = value for item in self.items: item.parent = self item._view = value if hasattr(item, "items"): item.view = value @property def type(self) -> ComponentType: return self._underlying.type @property def width(self) -> int: return 5 def is_dispatchable(self) -> bool: return any(item.is_dispatchable() for item in self.items) def is_persistent(self) -> bool: return all(item.is_persistent() for item in self.items) def refresh_component(self, component: ContainerComponent) -> None: self._underlying = component flattened = [] for c in component.components: if isinstance(c, ActionRow): flattened += c.children else: flattened.append(c) for i, y in enumerate(flattened): x = self.items[i] x.refresh_component(y)
[docs] def disable_all_items(self, *, exclusions: list[Item] | None = None) -> Self: """ Disables all buttons and select menus in the container. Parameters ---------- exclusions: Optional[List[:class:`Item`]] A list of items in `self.items` to not disable from the view. """ for item in self.walk_items(): if hasattr(item, "disabled") and (exclusions is None or item not in exclusions): item.disabled = True return self
[docs] def enable_all_items(self, *, exclusions: list[Item] | None = None) -> Self: """ Enables all buttons and select menus in the container. Parameters ---------- exclusions: Optional[List[:class:`Item`]] A list of items in `self.items` to not enable from the view. """ for item in self.walk_items(): if hasattr(item, "disabled") and (exclusions is None or item not in exclusions): item.disabled = False return self
def walk_items(self) -> Iterator[Item]: for item in self.items: if hasattr(item, "walk_items"): yield from item.walk_items() else: yield item def to_component_dict(self) -> ContainerComponentPayload: self._set_components(self.items) return self._underlying.to_dict() @classmethod def from_component(cls: type[C], component: ContainerComponent) -> C: from .view import _component_to_item # noqa: PLC0415 items = [_component_to_item(c) for c in _walk_all_components(component.components)] return cls( *items, colour=component.accent_color, spoiler=component.spoiler, id=component.id, ) callback = None