from __future__ import annotations
import abc
import copy
import typing as t
import attr
import hikari
from miru import HandlerFullError, ItemAlreadyAttachedError
from .items import DecoratedScreenItem, ScreenItem
if t.TYPE_CHECKING:
import typing_extensions as te
from miru import ViewContext
from .menu import Menu
__all__ = ("ScreenContent", "Screen")
[docs]
@attr.define(slots=True)
class ScreenContent:
"""The content payload of an individual menu screen."""
content: hikari.UndefinedOr[t.Any] = hikari.UNDEFINED
"""The content of the message. Anything passed here will be cast to str."""
attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED
"""An attachment to add to this page."""
attachments: hikari.UndefinedOr[t.Sequence[hikari.Resourceish]] = hikari.UNDEFINED
"""A sequence of attachments to add to this page."""
embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED
"""An embed to add to this page."""
embeds: hikari.UndefinedOr[t.Sequence[hikari.Embed]] = hikari.UNDEFINED
"""A sequence of embeds to add to this page."""
mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED
"""If True, mentioning @everyone will be allowed in this page's message."""
user_mentions: hikari.UndefinedOr[t.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]] = hikari.UNDEFINED
"""The set of allowed user mentions in this page's message. Set to True to allow all."""
role_mentions: hikari.UndefinedOr[t.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]] = hikari.UNDEFINED
"""The set of allowed role mentions in this page's message. Set to True to allow all."""
def _build_payload(self) -> t.Dict[str, t.Any]:
d: t.Dict[str, t.Any] = {
"content": self.content or None,
"attachments": self.attachments or None,
"embeds": self.embeds or None,
"mentions_everyone": self.mentions_everyone or False,
"user_mentions": self.user_mentions or False,
"role_mentions": self.role_mentions or False,
}
if not d["attachments"] and self.attachment:
d["attachments"] = [self.attachment]
if not d["embeds"] and self.embed:
d["embeds"] = [self.embed]
return d
[docs]
class Screen(abc.ABC):
"""A screen in a menu. Acts similarly to a View, although it is not a subclass of it.
Parameters
----------
menu : Menu
The menu that this screen belongs to.
"""
_screen_children: t.Sequence[
DecoratedScreenItem[Screen, ScreenItem, ViewContext]
] = [] # Decorated callbacks that need to be turned into items
def __init_subclass__(cls) -> None:
"""Get decorated callbacks."""
children: t.MutableSequence[DecoratedScreenItem[Screen, ScreenItem, ViewContext]] = []
for base_cls in reversed(cls.mro()):
for value in base_cls.__dict__.values():
if isinstance(value, DecoratedScreenItem):
children.append(value)
if len(children) > 25:
raise HandlerFullError("View cannot have more than 25 components attached.")
cls._screen_children = children
def __init__(self, menu: Menu) -> None:
self._menu = menu
self._children: t.MutableSequence[ScreenItem] = []
for decorated_item in self._screen_children:
# Must deepcopy, otherwise multiple views will have the same item reference
decorated_item = copy.deepcopy(decorated_item)
item = decorated_item.build(self)
self.add_item(item)
setattr(self, decorated_item.name, item)
@property
def menu(self) -> Menu:
"""The menu that this screen belongs to."""
return self._menu
@property
def children(self) -> t.Sequence[ScreenItem]:
"""The items contained in this screen."""
return self._children
[docs]
@abc.abstractmethod
async def build_content(self) -> ScreenContent:
"""Build the content payload for this screen.
This function is called whenever the screen comes into view.
Returns
-------
ScreenContent
The content payload for this screen.
"""
[docs]
async def on_dispose(self) -> None:
"""Called when this screen is disposed.
This happens when the menu navigates away from this screen.
Note that this is not the same as the screen being
removed from the menu, although it can be.
"""
[docs]
async def on_error(self, error: Exception) -> None:
"""Called when an exception is raised in build_content or on_dispose.
Parameters
----------
error : Exception
The exception that was raised.
"""
raise error
[docs]
def add_item(self, item: ScreenItem) -> te.Self:
"""Adds a new item to the screen.
Parameters
----------
item : ViewItem
The item to be added.
Raises
------
ValueError
ItemHandler already has 25 components attached.
TypeError
Parameter item is not an instance of ViewItem.
ItemAlreadyAttachedError
The item is already attached to this item handler.
Returns
-------
Screen
The item handler the item was added to.
"""
if len(self.children) > 25:
raise HandlerFullError("Screen cannot have more than 25 components attached.")
if not isinstance(item, ScreenItem):
raise TypeError(f"Expected ScreenItem not {type(item).__name__} for parameter item.")
if item in self.children:
raise ItemAlreadyAttachedError(f"Item {type(item).__name__} is already attached to this screen.")
self._children.append(item)
item._screen = self
return self
[docs]
def remove_item(self, item: ScreenItem) -> te.Self:
"""Removes the specified item from the screen.
Parameters
----------
item : ViewItem
The item to be removed.
Returns
-------
Screen
The item handler the item was removed from.
"""
try:
self._children.remove(item)
item._screen = None
except ValueError:
pass
return self
[docs]
def clear_items(self) -> te.Self:
"""Removes all items from this item handler.
Returns
-------
Screen
The item handler items were cleared from.
"""
for item in self.children:
item._screen = None
self._children.clear()
return self
[docs]
def get_item_by(self, predicate: t.Callable[[ScreenItem], bool]) -> t.Optional[ScreenItem]:
"""Get the first item that matches the given predicate.
Parameters
----------
predicate : Callable[[ScreenItem], bool]
A predicate to match the item.
Returns
-------
Optional[ScreenItem]
The item that matched the predicate or None.
"""
for item in self.children:
if predicate(item):
return item
return None
[docs]
def get_item_by_id(self, custom_id: str) -> t.Optional[ScreenItem]:
"""Get the first item with the given custom ID.
Parameters
----------
custom_id : str
The custom_id of the component.
Returns
-------
Optional[ScreenItem]
The item with the given custom ID or None.
"""
return self.get_item_by(lambda item: item.custom_id == custom_id)
# MIT License
#
# Copyright (c) 2022-present hypergonial
#
# 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.