diff --git a/CHANGES/1522.feature.rst b/CHANGES/1522.feature.rst new file mode 100644 index 00000000..a593173d --- /dev/null +++ b/CHANGES/1522.feature.rst @@ -0,0 +1,28 @@ +==== +Added functionality for negative numbers in keyboard adjust. +==== +If the function gets a negative number in `adjust()`, it applies them to the lower buttons in the same way that positive numbers affect the upper buttons. + +Negative numbers have a higher priority in formatting. + +You can make that: + +.. image:: https://github.com/aiogram/aiogram/assets/104172267/4d55a4c6-23fa-4a73-824b-0f2d7c879431 + + +With this code: + + .. code-block:: python + + def test_markup(): + builder = ReplyKeyboardBuilder() + builder.row(*(KeyboardButton(text=f"test-{index}") for index in range(6))) + builder.button(text="Add", callback_data="add") + builder.button(text="Cancel", callback_data="cancel") + builder.adjust(3, -1, -1) + return builder.as_markup() + + @router.message(Command('test')) + async def test_handler(msg: Message) -> None: + await msg.answer("This is TEST!!!", reply_markup=test_markup()) + ... diff --git a/aiogram/utils/keyboard.py b/aiogram/utils/keyboard.py index 8caa02b5..e8404428 100644 --- a/aiogram/utils/keyboard.py +++ b/aiogram/utils/keyboard.py @@ -211,12 +211,39 @@ class KeyboardBuilder(Generic[ButtonType], ABC): ) return self + def _markup_constructor(self, sizes: list, buttons: list, repeat: bool) -> list: + """ + Constructs the keyboard according to the specified parameters. + + :param sizes: + :param buttons: + :param repeat: + :return: + """ + markup = [] + row: List[ButtonType] = [] + validated_sizes = map(self._validate_size, sizes) + sizes_iter = repeat_all(validated_sizes) if repeat else repeat_last(validated_sizes) + size = next(sizes_iter) + + for button in buttons: + if len(row) >= size: + markup.append(row) + size = next(sizes_iter) + row = [] + row.append(button) + + if row: + markup.append(row) + return markup + def adjust(self, *sizes: int, repeat: bool = False) -> "KeyboardBuilder[ButtonType]": """ Adjust previously added buttons to specific row sizes. By default, when the sum of passed sizes is lower than buttons count the last one size will be used for tail of the markup. + If size is negative - it will be used for the last buttons. Negative values are more important than positive ones. If repeat=True is passed - all sizes will be cycled when available more buttons count than all sizes @@ -224,23 +251,31 @@ class KeyboardBuilder(Generic[ButtonType], ABC): :param repeat: :return: """ - if not sizes: - sizes = (self.max_width,) - - validated_sizes = map(self._validate_size, sizes) - sizes_iter = repeat_all(validated_sizes) if repeat else repeat_last(validated_sizes) - size = next(sizes_iter) - + if sizes: + negative_sizes = [-size for size in sizes if size < 0] + positive_sizes = ( + [size for size in sizes if size >= 0] + if len(negative_sizes) != len(sizes) + else (self.max_width,) + ) + else: + positive_sizes = (self.max_width,) + negative_sizes = [] markup = [] - row: List[ButtonType] = [] - for button in self.buttons: - if len(row) >= size: - markup.append(row) - size = next(sizes_iter) - row = [] - row.append(button) - if row: - markup.append(row) + buttons = list(self.buttons) + + if positive_sizes: + markup.extend( + self._markup_constructor( + positive_sizes, + buttons[: -sum(negative_sizes)] if negative_sizes else buttons, + repeat, + ) + ) + if negative_sizes: + markup.extend( + self._markup_constructor(negative_sizes, buttons[-sum(negative_sizes) :], repeat) + ) self._markup = markup return self diff --git a/tests/test_utils/test_keyboard.py b/tests/test_utils/test_keyboard.py index 1bc7063a..c5a71360 100644 --- a/tests/test_utils/test_keyboard.py +++ b/tests/test_utils/test_keyboard.py @@ -193,11 +193,17 @@ class TestKeyboardBuilder: [ [0, False, [], []], [0, False, [2], []], + [0, False, [-2], []], [1, False, [2], [1]], + [1, False, [-2], [1]], [3, False, [2], [2, 1]], + [3, False, [-2], [1, 2]], [10, False, [3, 2, 1], [3, 2, 1, 1, 1, 1, 1]], + [10, False, [3, 2, -1], [3, 2, 2, 2, 1]], + [10, False, [3, -2, 1], [3, 1, 1, 1, 1, 1, 2]], [12, False, [], [10, 2]], [12, True, [3, 2, 1], [3, 2, 1, 3, 2, 1]], + [12, True, [3, -1, -4], [3, 3, 1, 1, 4]], ], ) def test_adjust(self, count, repeat, sizes, shape):