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. 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

23
discord/ui/item.py

@ -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."""

32
discord/ui/select.py

@ -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,

80
discord/ui/view.py

@ -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…
Cancel
Save