Browse Source

Rework item grouping behaviour to take into consideration weights

This also renames `group` into `row`
pull/6997/head
Rapptz 4 years ago
parent
commit
7bd1211b36
  1. 30
      discord/ui/button.py
  2. 23
      discord/ui/item.py
  3. 32
      discord/ui/select.py
  4. 80
      discord/ui/view.py

30
discord/ui/button.py

@ -66,6 +66,12 @@ class Button(Item[V]):
The label of the button, if any.
emoji: Optional[:class:`PartialEmoji`]
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, ...] = (
@ -74,7 +80,7 @@ class Button(Item[V]):
'disabled',
'label',
'emoji',
'group_id',
'row',
)
def __init__(
@ -86,7 +92,7 @@ class Button(Item[V]):
custom_id: Optional[str] = None,
url: Optional[str] = None,
emoji: Optional[Union[str, PartialEmoji]] = None,
group: Optional[int] = None,
row: Optional[int] = None,
):
super().__init__()
if custom_id is not None and url is not None:
@ -110,7 +116,7 @@ class Button(Item[V]):
style=style,
emoji=emoji,
)
self.group_id = group
self.row = row
@property
def style(self) -> ButtonStyle:
@ -189,7 +195,7 @@ class Button(Item[V]):
custom_id=button.custom_id,
url=button.url,
emoji=button.emoji,
group=None,
row=None,
)
@property
@ -213,7 +219,7 @@ def button(
disabled: bool = False,
style: ButtonStyle = ButtonStyle.secondary,
emoji: Optional[Union[str, PartialEmoji]] = None,
group: Optional[int] = None,
row: Optional[int] = None,
) -> Callable[[ItemCallbackType], ItemCallbackType]:
"""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``.
emoji: Optional[Union[:class:`str`, :class:`PartialEmoji`]]
The emoji of the button. This can be in string form or a :class:`PartialEmoji`.
group: Optional[:class:`int`]
The relative group 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
like to control the relative positioning of the group then passing an index is advised.
For example, group=1 will show up before group=2. Defaults to ``None``, which is automatic
ordering.
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.
"""
def decorator(func: ItemCallbackType) -> ItemCallbackType:
@ -264,7 +270,7 @@ def button(
'disabled': disabled,
'label': label,
'emoji': emoji,
'group': group,
'row': row,
}
return func

23
discord/ui/item.py

@ -50,11 +50,12 @@ class Item(Generic[V]):
- :class:`discord.ui.Button`
"""
__item_repr_attributes__: Tuple[str, ...] = ('group_id',)
__item_repr_attributes__: Tuple[str, ...] = ('row',)
def __init__(self):
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]:
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__)
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
def view(self) -> Optional[V]:
"""Optional[:class:`View`]: The underlying view for this item."""

32
discord/ui/select.py

@ -75,6 +75,12 @@ class Select(Item[V]):
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.
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, ...] = (
@ -92,7 +98,7 @@ class Select(Item[V]):
min_values: int = 1,
max_values: int = 1,
options: List[SelectOption] = MISSING,
group: Optional[int] = None,
row: Optional[int] = None,
) -> None:
self._selected_values: List[str] = []
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,
options=options,
)
self.group_id = group
self.row = row
@property
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."""
return self._selected_values
@property
def width(self) -> int:
return 5
def to_component_dict(self) -> SelectMenuPayload:
return self._underlying.to_dict()
@ -247,7 +257,7 @@ class Select(Item[V]):
min_values=component.min_values,
max_values=component.max_values,
options=component.options,
group=None,
row=None,
)
@property
@ -265,7 +275,7 @@ def select(
min_values: int = 1,
max_values: int = 1,
options: List[SelectOption] = MISSING,
group: Optional[int] = None,
row: Optional[int] = None,
) -> Callable[[ItemCallbackType], ItemCallbackType]:
"""A decorator that attaches a select menu to a component.
@ -281,12 +291,12 @@ def select(
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
group: Optional[:class:`int`]
The relative group 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
like to control the relative positioning of the group then passing an index is advised.
For example, group=1 will show up before group=2. Defaults to ``None``, which is automatic
ordering.
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.
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.
@ -305,7 +315,7 @@ def select(
func.__discord_ui_model_kwargs__ = {
'placeholder': placeholder,
'custom_id': custom_id,
'group': group,
'row': row,
'min_values': min_values,
'max_values': max_values,
'options': options,

80
discord/ui/view.py

@ -67,6 +67,47 @@ def _component_to_item(component: Component) -> Item:
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:
"""Represents a UI view.
@ -112,6 +153,7 @@ class View:
setattr(self, func.__name__, item)
self.children.append(item)
self.__weights = _ViewWeights(self.children)
loop = asyncio.get_running_loop()
self.id = os.urandom(16).hex()
self._cancel_callback: Optional[Callable[[View], None]] = None
@ -120,29 +162,21 @@ class View:
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
if item.group_id is None:
return sys.maxsize
return item.group_id
return item._rendered_row or 0
children = sorted(self.children, key=key)
components: List[Dict[str, Any]] = []
for _, group in groupby(children, key=key):
group = list(group)
if len(group) <= 5:
components.append(
{
'type': 1,
'components': [item.to_component_dict() for item in group],
}
)
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)
)
children = [item.to_component_dict() for item in group]
if not children:
continue
components.append(
{
'type': 1,
'components': children,
}
)
return components
@ -165,7 +199,8 @@ class View:
TypeError
A :class:`Item` was not passed.
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:
@ -174,6 +209,8 @@ class View:
if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__!r}')
self.__weights.add_item(item)
item._view = self
self.children.append(item)
@ -190,10 +227,13 @@ class View:
self.children.remove(item)
except ValueError:
pass
else:
self.__weights.remove_item(item)
def clear_items(self) -> None:
"""Removes all items from the view."""
self.children.clear()
self.__weights.clear()
async def interaction_check(self, interaction: Interaction) -> bool:
"""|coro|

Loading…
Cancel
Save