Rework item grouping behaviour to take into consideration weights
This also renames `group` into `row`
This commit is contained in:
parent
695662416a
commit
7bd1211b36
@ -66,6 +66,12 @@ class Button(Item[V]):
|
|||||||
The label of the button, if any.
|
The label of the button, if any.
|
||||||
emoji: Optional[:class:`PartialEmoji`]
|
emoji: Optional[:class:`PartialEmoji`]
|
||||||
The emoji of the button, if available.
|
The emoji of the button, if available.
|
||||||
|
row: Optional[:class:`int`]
|
||||||
|
The relative row this button belongs to. A Discord component can only have 5
|
||||||
|
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||||
|
like to control the relative positioning of the row then passing an index is advised.
|
||||||
|
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||||
|
ordering. The row number cannot be negative or greater than 5.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__item_repr_attributes__: Tuple[str, ...] = (
|
__item_repr_attributes__: Tuple[str, ...] = (
|
||||||
@ -74,7 +80,7 @@ class Button(Item[V]):
|
|||||||
'disabled',
|
'disabled',
|
||||||
'label',
|
'label',
|
||||||
'emoji',
|
'emoji',
|
||||||
'group_id',
|
'row',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -86,7 +92,7 @@ class Button(Item[V]):
|
|||||||
custom_id: Optional[str] = None,
|
custom_id: Optional[str] = None,
|
||||||
url: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
emoji: Optional[Union[str, PartialEmoji]] = None,
|
emoji: Optional[Union[str, PartialEmoji]] = None,
|
||||||
group: Optional[int] = None,
|
row: Optional[int] = None,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if custom_id is not None and url is not None:
|
if custom_id is not None and url is not None:
|
||||||
@ -110,7 +116,7 @@ class Button(Item[V]):
|
|||||||
style=style,
|
style=style,
|
||||||
emoji=emoji,
|
emoji=emoji,
|
||||||
)
|
)
|
||||||
self.group_id = group
|
self.row = row
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def style(self) -> ButtonStyle:
|
def style(self) -> ButtonStyle:
|
||||||
@ -189,7 +195,7 @@ class Button(Item[V]):
|
|||||||
custom_id=button.custom_id,
|
custom_id=button.custom_id,
|
||||||
url=button.url,
|
url=button.url,
|
||||||
emoji=button.emoji,
|
emoji=button.emoji,
|
||||||
group=None,
|
row=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -213,7 +219,7 @@ def button(
|
|||||||
disabled: bool = False,
|
disabled: bool = False,
|
||||||
style: ButtonStyle = ButtonStyle.secondary,
|
style: ButtonStyle = ButtonStyle.secondary,
|
||||||
emoji: Optional[Union[str, PartialEmoji]] = None,
|
emoji: Optional[Union[str, PartialEmoji]] = None,
|
||||||
group: Optional[int] = None,
|
row: Optional[int] = None,
|
||||||
) -> Callable[[ItemCallbackType], ItemCallbackType]:
|
) -> Callable[[ItemCallbackType], ItemCallbackType]:
|
||||||
"""A decorator that attaches a button to a component.
|
"""A decorator that attaches a button to a component.
|
||||||
|
|
||||||
@ -242,12 +248,12 @@ def button(
|
|||||||
Whether the button is disabled or not. Defaults to ``False``.
|
Whether the button is disabled or not. Defaults to ``False``.
|
||||||
emoji: Optional[Union[:class:`str`, :class:`PartialEmoji`]]
|
emoji: Optional[Union[:class:`str`, :class:`PartialEmoji`]]
|
||||||
The emoji of the button. This can be in string form or a :class:`PartialEmoji`.
|
The emoji of the button. This can be in string form or a :class:`PartialEmoji`.
|
||||||
group: Optional[:class:`int`]
|
row: Optional[:class:`int`]
|
||||||
The relative group this button belongs to. A Discord component can only have 5
|
The relative row this button belongs to. A Discord component can only have 5
|
||||||
groups. By default, items are arranged automatically into those 5 groups. If you'd
|
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||||
like to control the relative positioning of the group then passing an index is advised.
|
like to control the relative positioning of the row then passing an index is advised.
|
||||||
For example, group=1 will show up before group=2. Defaults to ``None``, which is automatic
|
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||||
ordering.
|
ordering. The row number cannot be negative or greater than 5.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func: ItemCallbackType) -> ItemCallbackType:
|
def decorator(func: ItemCallbackType) -> ItemCallbackType:
|
||||||
@ -264,7 +270,7 @@ def button(
|
|||||||
'disabled': disabled,
|
'disabled': disabled,
|
||||||
'label': label,
|
'label': label,
|
||||||
'emoji': emoji,
|
'emoji': emoji,
|
||||||
'group': group,
|
'row': row,
|
||||||
}
|
}
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
@ -50,11 +50,12 @@ class Item(Generic[V]):
|
|||||||
- :class:`discord.ui.Button`
|
- :class:`discord.ui.Button`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__item_repr_attributes__: Tuple[str, ...] = ('group_id',)
|
__item_repr_attributes__: Tuple[str, ...] = ('row',)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._view: Optional[V] = None
|
self._view: Optional[V] = None
|
||||||
self.group_id: Optional[int] = None
|
self._row: Optional[int] = None
|
||||||
|
self._rendered_row: Optional[int] = None
|
||||||
|
|
||||||
def to_component_dict(self) -> Dict[str, Any]:
|
def to_component_dict(self) -> Dict[str, Any]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -80,6 +81,24 @@ class Item(Generic[V]):
|
|||||||
attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__)
|
attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__)
|
||||||
return f'<{self.__class__.__name__} {attrs}>'
|
return f'<{self.__class__.__name__} {attrs}>'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def row(self) -> Optional[int]:
|
||||||
|
return self._row
|
||||||
|
|
||||||
|
@row.setter
|
||||||
|
def row(self, value: Optional[int]):
|
||||||
|
if value is None:
|
||||||
|
self._row = None
|
||||||
|
elif 5 > value >= 0:
|
||||||
|
self._row = value
|
||||||
|
else:
|
||||||
|
raise ValueError('row cannot be negative or greater than or equal to 5')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
""":class:`int`: The width of the item."""
|
||||||
|
return 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def view(self) -> Optional[V]:
|
def view(self) -> Optional[V]:
|
||||||
"""Optional[:class:`View`]: The underlying view for this item."""
|
"""Optional[:class:`View`]: The underlying view for this item."""
|
||||||
|
@ -75,6 +75,12 @@ class Select(Item[V]):
|
|||||||
Defaults to 1 and must be between 1 and 25.
|
Defaults to 1 and must be between 1 and 25.
|
||||||
options: List[:class:`discord.SelectOption`]
|
options: List[:class:`discord.SelectOption`]
|
||||||
A list of options that can be selected in this menu.
|
A list of options that can be selected in this menu.
|
||||||
|
row: Optional[:class:`int`]
|
||||||
|
The relative row this select menu belongs to. A Discord component can only have 5
|
||||||
|
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||||
|
like to control the relative positioning of the row then passing an index is advised.
|
||||||
|
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||||
|
ordering. The row number cannot be negative or greater than 5.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__item_repr_attributes__: Tuple[str, ...] = (
|
__item_repr_attributes__: Tuple[str, ...] = (
|
||||||
@ -92,7 +98,7 @@ class Select(Item[V]):
|
|||||||
min_values: int = 1,
|
min_values: int = 1,
|
||||||
max_values: int = 1,
|
max_values: int = 1,
|
||||||
options: List[SelectOption] = MISSING,
|
options: List[SelectOption] = MISSING,
|
||||||
group: Optional[int] = None,
|
row: Optional[int] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._selected_values: List[str] = []
|
self._selected_values: List[str] = []
|
||||||
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
|
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
|
||||||
@ -105,7 +111,7 @@ class Select(Item[V]):
|
|||||||
max_values=max_values,
|
max_values=max_values,
|
||||||
options=options,
|
options=options,
|
||||||
)
|
)
|
||||||
self.group_id = group
|
self.row = row
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def custom_id(self) -> str:
|
def custom_id(self) -> str:
|
||||||
@ -229,6 +235,10 @@ class Select(Item[V]):
|
|||||||
"""List[:class:`str`]: A list of values that have been selected by the user."""
|
"""List[:class:`str`]: A list of values that have been selected by the user."""
|
||||||
return self._selected_values
|
return self._selected_values
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
return 5
|
||||||
|
|
||||||
def to_component_dict(self) -> SelectMenuPayload:
|
def to_component_dict(self) -> SelectMenuPayload:
|
||||||
return self._underlying.to_dict()
|
return self._underlying.to_dict()
|
||||||
|
|
||||||
@ -247,7 +257,7 @@ class Select(Item[V]):
|
|||||||
min_values=component.min_values,
|
min_values=component.min_values,
|
||||||
max_values=component.max_values,
|
max_values=component.max_values,
|
||||||
options=component.options,
|
options=component.options,
|
||||||
group=None,
|
row=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -265,7 +275,7 @@ def select(
|
|||||||
min_values: int = 1,
|
min_values: int = 1,
|
||||||
max_values: int = 1,
|
max_values: int = 1,
|
||||||
options: List[SelectOption] = MISSING,
|
options: List[SelectOption] = MISSING,
|
||||||
group: Optional[int] = None,
|
row: Optional[int] = None,
|
||||||
) -> Callable[[ItemCallbackType], ItemCallbackType]:
|
) -> Callable[[ItemCallbackType], ItemCallbackType]:
|
||||||
"""A decorator that attaches a select menu to a component.
|
"""A decorator that attaches a select menu to a component.
|
||||||
|
|
||||||
@ -281,12 +291,12 @@ def select(
|
|||||||
custom_id: :class:`str`
|
custom_id: :class:`str`
|
||||||
The ID of the select menu that gets received during an interaction.
|
The ID of the select menu that gets received during an interaction.
|
||||||
It is recommended not to set this parameter to prevent conflicts.
|
It is recommended not to set this parameter to prevent conflicts.
|
||||||
group: Optional[:class:`int`]
|
row: Optional[:class:`int`]
|
||||||
The relative group this select menu belongs to. A Discord component can only have 5
|
The relative row this select menu belongs to. A Discord component can only have 5
|
||||||
groups. By default, items are arranged automatically into those 5 groups. If you'd
|
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||||
like to control the relative positioning of the group then passing an index is advised.
|
like to control the relative positioning of the row then passing an index is advised.
|
||||||
For example, group=1 will show up before group=2. Defaults to ``None``, which is automatic
|
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||||
ordering.
|
ordering. The row number cannot be negative or greater than 5.
|
||||||
min_values: :class:`int`
|
min_values: :class:`int`
|
||||||
The minimum number of items that must be chosen for this select menu.
|
The minimum number of items that must be chosen for this select menu.
|
||||||
Defaults to 1 and must be between 1 and 25.
|
Defaults to 1 and must be between 1 and 25.
|
||||||
@ -305,7 +315,7 @@ def select(
|
|||||||
func.__discord_ui_model_kwargs__ = {
|
func.__discord_ui_model_kwargs__ = {
|
||||||
'placeholder': placeholder,
|
'placeholder': placeholder,
|
||||||
'custom_id': custom_id,
|
'custom_id': custom_id,
|
||||||
'group': group,
|
'row': row,
|
||||||
'min_values': min_values,
|
'min_values': min_values,
|
||||||
'max_values': max_values,
|
'max_values': max_values,
|
||||||
'options': options,
|
'options': options,
|
||||||
|
@ -67,6 +67,47 @@ def _component_to_item(component: Component) -> Item:
|
|||||||
return Item.from_component(component)
|
return Item.from_component(component)
|
||||||
|
|
||||||
|
|
||||||
|
class _ViewWeights:
|
||||||
|
__slots__ = (
|
||||||
|
'weights',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, children: List[Item]):
|
||||||
|
self.weights: List[int] = [0, 0, 0, 0, 0]
|
||||||
|
|
||||||
|
key = lambda i: sys.maxsize if i.row is None else i.row
|
||||||
|
children = sorted(children, key=key)
|
||||||
|
for row, group in groupby(children, key=key):
|
||||||
|
for item in group:
|
||||||
|
self.add_item(item)
|
||||||
|
|
||||||
|
def find_open_space(self, item: Item) -> int:
|
||||||
|
for index, weight in enumerate(self.weights):
|
||||||
|
if weight + item.width <= 5:
|
||||||
|
return index
|
||||||
|
|
||||||
|
raise ValueError('could not find open space for item')
|
||||||
|
|
||||||
|
def add_item(self, item: Item) -> None:
|
||||||
|
if item.row is not None:
|
||||||
|
total = self.weights[item.row] + item.width
|
||||||
|
if total > 5:
|
||||||
|
raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)')
|
||||||
|
self.weights[item.row] = total
|
||||||
|
item._rendered_row = item.row
|
||||||
|
else:
|
||||||
|
index = self.find_open_space(item)
|
||||||
|
self.weights[index] += item.width
|
||||||
|
item._rendered_row = index
|
||||||
|
|
||||||
|
def remove_item(self, item: Item) -> None:
|
||||||
|
if item._rendered_row is not None:
|
||||||
|
self.weights[item._rendered_row] -= item.width
|
||||||
|
item._rendered_row = None
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
self.weights = [0, 0, 0, 0, 0]
|
||||||
|
|
||||||
class View:
|
class View:
|
||||||
"""Represents a UI view.
|
"""Represents a UI view.
|
||||||
|
|
||||||
@ -112,6 +153,7 @@ class View:
|
|||||||
setattr(self, func.__name__, item)
|
setattr(self, func.__name__, item)
|
||||||
self.children.append(item)
|
self.children.append(item)
|
||||||
|
|
||||||
|
self.__weights = _ViewWeights(self.children)
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
self.id = os.urandom(16).hex()
|
self.id = os.urandom(16).hex()
|
||||||
self._cancel_callback: Optional[Callable[[View], None]] = None
|
self._cancel_callback: Optional[Callable[[View], None]] = None
|
||||||
@ -120,29 +162,21 @@ class View:
|
|||||||
|
|
||||||
def to_components(self) -> List[Dict[str, Any]]:
|
def to_components(self) -> List[Dict[str, Any]]:
|
||||||
def key(item: Item) -> int:
|
def key(item: Item) -> int:
|
||||||
if item.group_id is None:
|
return item._rendered_row or 0
|
||||||
return sys.maxsize
|
|
||||||
return item.group_id
|
|
||||||
|
|
||||||
children = sorted(self.children, key=key)
|
children = sorted(self.children, key=key)
|
||||||
components: List[Dict[str, Any]] = []
|
components: List[Dict[str, Any]] = []
|
||||||
for _, group in groupby(children, key=key):
|
for _, group in groupby(children, key=key):
|
||||||
group = list(group)
|
children = [item.to_component_dict() for item in group]
|
||||||
if len(group) <= 5:
|
if not children:
|
||||||
components.append(
|
continue
|
||||||
{
|
|
||||||
'type': 1,
|
components.append(
|
||||||
'components': [item.to_component_dict() for item in group],
|
{
|
||||||
}
|
'type': 1,
|
||||||
)
|
'components': children,
|
||||||
else:
|
}
|
||||||
components.extend(
|
)
|
||||||
{
|
|
||||||
'type': 1,
|
|
||||||
'components': [item.to_component_dict() for item in group[index : index + 5]],
|
|
||||||
}
|
|
||||||
for index in range(0, len(group), 5)
|
|
||||||
)
|
|
||||||
|
|
||||||
return components
|
return components
|
||||||
|
|
||||||
@ -165,7 +199,8 @@ class View:
|
|||||||
TypeError
|
TypeError
|
||||||
A :class:`Item` was not passed.
|
A :class:`Item` was not passed.
|
||||||
ValueError
|
ValueError
|
||||||
Maximum number of children has been exceeded (25).
|
Maximum number of children has been exceeded (25)
|
||||||
|
or the row the item is trying to be added to is full.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if len(self.children) > 25:
|
if len(self.children) > 25:
|
||||||
@ -174,6 +209,8 @@ class View:
|
|||||||
if not isinstance(item, Item):
|
if not isinstance(item, Item):
|
||||||
raise TypeError(f'expected Item not {item.__class__!r}')
|
raise TypeError(f'expected Item not {item.__class__!r}')
|
||||||
|
|
||||||
|
self.__weights.add_item(item)
|
||||||
|
|
||||||
item._view = self
|
item._view = self
|
||||||
self.children.append(item)
|
self.children.append(item)
|
||||||
|
|
||||||
@ -190,10 +227,13 @@ class View:
|
|||||||
self.children.remove(item)
|
self.children.remove(item)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
self.__weights.remove_item(item)
|
||||||
|
|
||||||
def clear_items(self) -> None:
|
def clear_items(self) -> None:
|
||||||
"""Removes all items from the view."""
|
"""Removes all items from the view."""
|
||||||
self.children.clear()
|
self.children.clear()
|
||||||
|
self.__weights.clear()
|
||||||
|
|
||||||
async def interaction_check(self, interaction: Interaction) -> bool:
|
async def interaction_check(self, interaction: Interaction) -> bool:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user