All checks were successful
Zapret source guards / validate (push) Successful in 10s
Список: плитка пресета шириной под имена (а не на треть окна), без отступа слева и без повтора названия папки в имени — на широком экране 5-6 столбцов. Включённый пресет отмечен галочкой, оценка и булавка — пометки справа, под мышью кнопки «закрепить» и «меню». Дата и полное имя — в подсказке. Шапка: заголовок, кнопки, «Получить конфиги» и поиск стоят в одном ряду; отдельная полоса про обмен пресетами убрана, её текст стал подсказкой кнопки. Поиск встаёт в последний ряд кнопок, если помещается; исправлена обрезка нижнего ряда панели при переносе кнопок. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
319 lines
13 KiB
Python
319 lines
13 KiB
Python
from __future__ import annotations
|
||
|
||
import os
|
||
import unittest
|
||
|
||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||
|
||
from PyQt6.QtCore import QEvent, Qt
|
||
from PyQt6.QtGui import QKeyEvent
|
||
from PyQt6.QtWidgets import QApplication, QLabel, QWidget
|
||
from qfluentwidgets import FluentIcon, LineEdit, PrimaryToolButton, PushButton, SettingCardGroup
|
||
|
||
from ui.fluent_widgets import (
|
||
QuickActionsBar,
|
||
SettingsCard,
|
||
_SettingCardGroupAutoHeightFilter,
|
||
enable_setting_card_group_auto_height,
|
||
insert_widget_into_setting_card_group,
|
||
)
|
||
from ui.presets_menu.toolbar import PresetsToolbarLayout
|
||
from ui.widgets.win11_controls import Win11ToggleRow
|
||
|
||
|
||
class QuickActionsBarLayoutTests(unittest.TestCase):
|
||
@classmethod
|
||
def setUpClass(cls) -> None:
|
||
cls._app = QApplication.instance() or QApplication([])
|
||
|
||
def test_setting_card_group_height_keeps_quick_action_buttons_visible(self) -> None:
|
||
group = SettingCardGroup("Действия")
|
||
actions = QuickActionsBar()
|
||
actions.add_buttons(
|
||
[
|
||
PushButton("Открыть файл", icon=FluentIcon.LINK),
|
||
PushButton("Сбросить файл", icon=FluentIcon.RETURN),
|
||
PushButton("Очистить всё", icon=FluentIcon.DELETE),
|
||
]
|
||
)
|
||
|
||
insert_widget_into_setting_card_group(group, 1, actions)
|
||
|
||
needed_height = group.vBoxLayout.minimumSize().height()
|
||
self.assertGreaterEqual(group.minimumHeight(), needed_height)
|
||
self.assertGreaterEqual(group.maximumHeight(), needed_height)
|
||
self.assertGreaterEqual(actions.sizeHint().height(), actions.actions_layout.minimumSize().height())
|
||
|
||
def test_presets_toolbar_places_search_on_the_right_when_width_allows(self) -> None:
|
||
parent = QWidget()
|
||
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
|
||
first = PushButton("Импорт")
|
||
second = PushButton("Папка")
|
||
first.setFixedWidth(80)
|
||
second.setFixedWidth(80)
|
||
search = LineEdit()
|
||
|
||
toolbar.set_buttons([first, second])
|
||
toolbar.set_trailing_widget(search, minimum_width=180)
|
||
toolbar.refresh_layout(420)
|
||
|
||
row_layout = toolbar._rows[0][1]
|
||
widgets = [row_layout.itemAt(index).widget() for index in range(row_layout.count())]
|
||
|
||
self.assertEqual(widgets[:2], [first, second])
|
||
self.assertIn(search, widgets)
|
||
|
||
def test_presets_toolbar_action_button_is_named_and_keyboard_clickable(self) -> None:
|
||
parent = QWidget()
|
||
toolbar = PresetsToolbarLayout(parent)
|
||
button = toolbar.create_action_button("Импорт", FluentIcon.DOWNLOAD)
|
||
clicked: list[bool] = []
|
||
button.clicked.connect(lambda: clicked.append(True))
|
||
|
||
event = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Return, Qt.KeyboardModifier.NoModifier)
|
||
button.keyPressEvent(event)
|
||
|
||
self.assertEqual(button.accessibleName(), "Импорт")
|
||
self.assertEqual(button.property("screenReaderStateText"), "Импорт")
|
||
self.assertEqual(button.focusPolicy(), Qt.FocusPolicy.StrongFocus)
|
||
self.assertTrue(event.isAccepted())
|
||
self.assertEqual(clicked, [True])
|
||
|
||
def test_presets_toolbar_icon_button_can_be_named_for_screen_reader(self) -> None:
|
||
parent = QWidget()
|
||
toolbar = PresetsToolbarLayout(parent)
|
||
button = toolbar.create_primary_tool_button(
|
||
PrimaryToolButton,
|
||
FluentIcon.ADD,
|
||
accessible_name="Создать пресет",
|
||
accessible_description="Создаёт новый пользовательский пресет.",
|
||
)
|
||
|
||
self.assertEqual(button.accessibleName(), "Создать пресет")
|
||
self.assertEqual(button.property("screenReaderStateText"), "Создать пресет")
|
||
self.assertIn("новый пользовательский пресет", button.accessibleDescription())
|
||
self.assertEqual(button.focusPolicy(), Qt.FocusPolicy.StrongFocus)
|
||
|
||
def test_presets_toolbar_moves_search_to_next_row_when_width_is_tight(self) -> None:
|
||
parent = QWidget()
|
||
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
|
||
first = PushButton("Импорт")
|
||
second = PushButton("Папка")
|
||
first.setFixedWidth(80)
|
||
second.setFixedWidth(80)
|
||
search = LineEdit()
|
||
|
||
toolbar.set_buttons([first, second])
|
||
toolbar.set_trailing_widget(search, minimum_width=180)
|
||
toolbar.refresh_layout(230)
|
||
|
||
first_row_widgets = [
|
||
toolbar._rows[0][1].itemAt(index).widget()
|
||
for index in range(toolbar._rows[0][1].count())
|
||
]
|
||
second_row_widgets = [
|
||
toolbar._rows[1][1].itemAt(index).widget()
|
||
for index in range(toolbar._rows[1][1].count())
|
||
]
|
||
|
||
self.assertIn(first, first_row_widgets)
|
||
self.assertIn(second, first_row_widgets)
|
||
self.assertIn(search, second_row_widgets)
|
||
|
||
def test_presets_toolbar_reserves_height_when_search_moves_to_next_row(self) -> None:
|
||
parent = QWidget()
|
||
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
|
||
first = PushButton("Импорт")
|
||
second = PushButton("Папка")
|
||
first.setFixedWidth(80)
|
||
second.setFixedWidth(80)
|
||
search = LineEdit()
|
||
|
||
toolbar.set_buttons([first, second])
|
||
toolbar.set_trailing_widget(search, minimum_width=180)
|
||
toolbar.refresh_layout(230)
|
||
|
||
needed_height = toolbar.container.layout().sizeHint().height()
|
||
|
||
self.assertGreater(needed_height, toolbar._rows[0][0].sizeHint().height())
|
||
self.assertGreaterEqual(toolbar.container.minimumHeight(), needed_height)
|
||
self.assertGreaterEqual(toolbar.container.maximumHeight(), needed_height)
|
||
|
||
def test_presets_toolbar_uses_visible_container_width_for_wrapping(self) -> None:
|
||
parent = QWidget()
|
||
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
|
||
first = PushButton("Импорт")
|
||
second = PushButton("Папка")
|
||
first.setFixedWidth(80)
|
||
second.setFixedWidth(80)
|
||
search = LineEdit()
|
||
|
||
toolbar.set_buttons([first, second])
|
||
toolbar.set_trailing_widget(search, minimum_width=180)
|
||
toolbar.container.setFixedWidth(230)
|
||
parent.show()
|
||
self._app.processEvents()
|
||
|
||
toolbar.refresh_layout(420)
|
||
|
||
first_row_widgets = [
|
||
toolbar._rows[0][1].itemAt(index).widget()
|
||
for index in range(toolbar._rows[0][1].count())
|
||
]
|
||
second_row_widgets = [
|
||
toolbar._rows[1][1].itemAt(index).widget()
|
||
for index in range(toolbar._rows[1][1].count())
|
||
]
|
||
|
||
self.assertIn(first, first_row_widgets)
|
||
self.assertIn(second, first_row_widgets)
|
||
self.assertNotIn(search, first_row_widgets)
|
||
self.assertIn(search, second_row_widgets)
|
||
|
||
def test_presets_toolbar_skips_relayout_when_state_is_unchanged(self) -> None:
|
||
parent = QWidget()
|
||
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
|
||
first = PushButton("Импорт")
|
||
second = PushButton("Папка")
|
||
search = LineEdit()
|
||
|
||
toolbar.set_buttons([first, second])
|
||
toolbar.set_trailing_widget(search, minimum_width=180)
|
||
toolbar.refresh_layout(420)
|
||
|
||
first_row_layout = toolbar._rows[0][1]
|
||
original_clear_row = toolbar._clear_row
|
||
clear_calls: list[int] = []
|
||
|
||
def _count_clear(row_layout):
|
||
clear_calls.append(row_layout.count())
|
||
original_clear_row(row_layout)
|
||
|
||
toolbar._clear_row = _count_clear
|
||
toolbar.refresh_layout(420)
|
||
|
||
self.assertEqual(clear_calls, [])
|
||
self.assertIs(first_row_layout.itemAt(0).widget(), first)
|
||
self.assertIs(first_row_layout.itemAt(1).widget(), second)
|
||
|
||
def test_presets_toolbar_puts_page_title_before_the_buttons(self) -> None:
|
||
parent = QWidget()
|
||
self.addCleanup(parent.deleteLater)
|
||
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
|
||
title = QLabel("Мои пресеты")
|
||
button = PushButton("Импорт")
|
||
search = LineEdit()
|
||
toolbar.set_buttons([button])
|
||
toolbar.set_trailing_widget(search, minimum_width=120)
|
||
toolbar.set_leading_widget(title)
|
||
|
||
toolbar.refresh_layout(900)
|
||
|
||
first_row = toolbar._rows[0][1]
|
||
self.assertIs(first_row.itemAt(0).widget(), title)
|
||
self.assertIs(first_row.itemAt(1).widget(), button)
|
||
self.assertIs(first_row.itemAt(first_row.count() - 1).widget(), search)
|
||
|
||
def test_presets_toolbar_keeps_search_in_the_last_button_row_when_it_fits(self) -> None:
|
||
parent = QWidget()
|
||
self.addCleanup(parent.deleteLater)
|
||
toolbar = PresetsToolbarLayout(parent, row_count=4, button_spacing=8)
|
||
buttons = [PushButton("Вернуть встроенные пресеты") for _ in range(3)]
|
||
search = LineEdit()
|
||
toolbar.set_buttons(buttons)
|
||
toolbar.set_trailing_widget(search, minimum_width=120)
|
||
one_button = buttons[0].sizeHint().width()
|
||
|
||
# Влезают две кнопки в ряд: третья уходит на второй, поиск встаёт рядом с ней.
|
||
toolbar.refresh_layout(2 * one_button + 8 + 60)
|
||
|
||
second_row = toolbar._rows[1][1]
|
||
self.assertIs(second_row.itemAt(0).widget(), buttons[2])
|
||
self.assertIs(second_row.itemAt(second_row.count() - 1).widget(), search)
|
||
self.assertEqual(toolbar._rows[2][1].count(), 0)
|
||
|
||
def test_presets_toolbar_height_grows_with_every_new_row(self) -> None:
|
||
parent = QWidget()
|
||
self.addCleanup(parent.deleteLater)
|
||
parent.resize(1200, 300)
|
||
toolbar = PresetsToolbarLayout(parent, row_count=4, button_spacing=8)
|
||
buttons = [PushButton("Вернуть встроенные пресеты") for _ in range(3)]
|
||
toolbar.set_buttons(buttons)
|
||
parent.show()
|
||
QApplication.processEvents()
|
||
one_button = buttons[0].sizeHint().width()
|
||
|
||
toolbar.refresh_layout(3 * one_button + 16)
|
||
one_row_height = toolbar.container.maximumHeight()
|
||
toolbar.refresh_layout(one_button)
|
||
|
||
# Раньше высота считалась до показа новых рядов, и нижний ряд обрезался.
|
||
self.assertGreaterEqual(toolbar.container.maximumHeight(), 3 * one_row_height)
|
||
|
||
def test_setting_card_group_height_updates_after_late_card_addition(self) -> None:
|
||
group = SettingCardGroup("Управление")
|
||
actions = QuickActionsBar()
|
||
actions.add_buttons(
|
||
[
|
||
PushButton("Открыть", icon=FluentIcon.FOLDER),
|
||
PushButton("Перестроить", icon=FluentIcon.SYNC),
|
||
]
|
||
)
|
||
|
||
insert_widget_into_setting_card_group(group, 1, actions)
|
||
|
||
info_card = SettingsCard()
|
||
info_card.add_widget(QLabel("Загрузка информации..."))
|
||
group.addSettingCard(info_card)
|
||
|
||
group.resize(638, 200)
|
||
group.show()
|
||
self._app.processEvents()
|
||
|
||
needed_info_bottom = info_card.geometry().y() + info_card.sizeHint().height()
|
||
self.assertGreaterEqual(group.minimumHeight(), needed_info_bottom)
|
||
self.assertGreaterEqual(group.maximumHeight(), needed_info_bottom)
|
||
self.assertLessEqual(group.minimumHeight(), needed_info_bottom + 1)
|
||
self.assertLessEqual(group.maximumHeight(), needed_info_bottom + 1)
|
||
|
||
def test_setting_card_group_auto_height_keeps_fluent_setting_card_height(self) -> None:
|
||
group = SettingCardGroup("Настройки программы")
|
||
rows = [
|
||
Win11ToggleRow(
|
||
"fa5s.bolt",
|
||
"Автозапуск DPI после старта программы",
|
||
"После запуска ZapretGUI автоматически запускать текущий DPI-режим",
|
||
),
|
||
Win11ToggleRow(
|
||
"fa5s.shield-alt",
|
||
"Отключить Windows Defender",
|
||
"Требуются права администратора",
|
||
),
|
||
]
|
||
original_heights = [row.minimumHeight() for row in rows]
|
||
|
||
for row in rows:
|
||
group.addSettingCard(row)
|
||
|
||
group.resize(710, 300)
|
||
group.show()
|
||
self._app.processEvents()
|
||
|
||
enable_setting_card_group_auto_height(group)
|
||
self._app.processEvents()
|
||
|
||
for row, original_height in zip(rows, original_heights):
|
||
self.assertGreaterEqual(row.minimumHeight(), original_height)
|
||
self.assertGreaterEqual(row.maximumHeight(), original_height)
|
||
|
||
def test_setting_card_group_auto_height_does_not_refresh_on_own_layout_requests(self) -> None:
|
||
events = _SettingCardGroupAutoHeightFilter._EVENTS
|
||
|
||
self.assertNotIn(QEvent.Type.LayoutRequest, events)
|
||
self.assertNotIn(QEvent.Type.Resize, events)
|
||
self.assertIn(QEvent.Type.ChildAdded, events)
|
||
self.assertIn(QEvent.Type.ChildRemoved, events)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|