T-572 修复封面画廊方向键焦点

This commit is contained in:
chengma
2026-07-09 15:44:14 +08:00
parent 77e9bf0f5d
commit b88f170b8b
3 changed files with 84 additions and 13 deletions
+28 -10
View File
@@ -2,6 +2,8 @@
from __future__ import annotations
from PySide6.QtCore import QEvent
from ..models import GenerateTaskTableModel
from ..widgets import *
from ..workers import GenerateWorker as _RealGenerateWorker
@@ -148,6 +150,7 @@ class CoverGalleryDialog(QDialog):
self._update_navigation_buttons()
self._update_regenerate_button_state()
self._fit_to_screen()
self._install_navigation_event_filters()
def _old_cover_panel(self):
panel = QWidget()
@@ -458,24 +461,34 @@ class CoverGalleryDialog(QDialog):
event.ignore()
def keyPressEvent(self, event):
if event.key() == Qt.Key_Up and self._should_consume_navigation_key():
self.switch_task(-1)
event.accept()
return
if event.key() == Qt.Key_Down and self._should_consume_navigation_key():
self.switch_task(1)
event.accept()
if self._handle_navigation_key(event, QApplication.focusWidget()):
return
super().keyPressEvent(event)
def _should_consume_navigation_key(self):
focus = QApplication.focusWidget()
def eventFilter(self, watched, event):
focus = QApplication.focusWidget() if watched is self else watched
if event.type() == QEvent.KeyPress and self._handle_navigation_key(event, focus):
return True
return super().eventFilter(watched, event)
def _handle_navigation_key(self, event, focus):
if event.key() == Qt.Key_Up and self._should_consume_navigation_key(focus):
self.switch_task(-1)
event.accept()
return True
if event.key() == Qt.Key_Down and self._should_consume_navigation_key(focus):
self.switch_task(1)
event.accept()
return True
return False
def _should_consume_navigation_key(self, focus=None):
focus = focus or QApplication.focusWidget()
if focus is None or focus is self:
return True
return not isinstance(
focus,
(
QPushButton,
QLineEdit,
QPlainTextEdit,
QComboBox,
@@ -483,6 +496,10 @@ class CoverGalleryDialog(QDialog):
),
)
def _install_navigation_event_filters(self):
for widget in [self] + self.findChildren(QWidget):
widget.installEventFilter(self)
def open_original_image(self, image_path):
dialog = OriginalImageDialog(image_path, self)
dialog.exec()
@@ -543,6 +560,7 @@ class CoverGalleryDialog(QDialog):
empty_label.setMinimumSize(320, self.THUMBNAIL_SIZE)
empty_label.setAlignment(Qt.AlignCenter)
self.candidate_layout.addWidget(empty_label)
self._install_navigation_event_filters()
def _on_regenerate_progress(self, payload):
if payload.get("failed"):
+5 -2
View File
@@ -3,7 +3,7 @@ id: T-572
title: ②封面画廊打开后上下键切换记录不受按钮焦点影响
phase: 7
deps: [T-569]
status: TODO
status: DONE
created: 2026-07-09
---
@@ -55,4 +55,7 @@ return not isinstance(focus, (QPushButton, QLineEdit, QPlainTextEdit, QComboBox,
## 执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、关键决策。)
- 2026-07-09:完成 T-572。
- `app/gui/tabs/generate.py`:调整 `CoverGalleryDialog._should_consume_navigation_key()`,不再把 `QPushButton` 作为方向键切换的排除焦点;同时给弹窗和子控件安装 `eventFilter`,处理真实 Qt 运行时按键先发给按钮/radio 等子控件、父弹窗 `keyPressEvent()` 收不到的路径。焦点在上一条/下一条/重新生成/保存/取消等按钮上时,`↑ / ↓` 会切换记录;焦点在输入类控件上仍不抢键。
- `tests/test_gui.py`:新增按钮焦点下通过真实子控件事件分发 `Qt.Key_Down` 可切到下一条的回归测试;新增输入框焦点下 `Qt.Key_Down` 不切换记录的保护测试;保留 radio 焦点不改变候选选择的既有测试。
- 验证通过:`py -3.10 -m unittest tests.test_gui`、`python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`py -3.10 -m unittest discover -s tests`、`git diff --check`。
+51 -1
View File
@@ -2472,7 +2472,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
dialog.candidate_buttons[canonicals[0]].setFocus()
event = QKeyEvent(QKeyEvent.KeyPress, gui.Qt.Key_Down, gui.Qt.NoModifier)
dialog.keyPressEvent(event)
QApplication.sendEvent(dialog.candidate_buttons[canonicals[0]], event)
self.assertTrue(event.isAccepted())
self.assertEqual(tasks[1].id, dialog.task.id)
@@ -2481,6 +2481,56 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_cover_gallery_arrow_keys_switch_when_button_has_focus(self):
with self.make_temp_dir() as temp_dir:
cfg, account, tasks, canonicals, _archives = self._cover_gallery_task_set(temp_dir, count=2)
dialog = CoverGalleryDialog(
tasks[0],
cfg["image_dir"],
cfg["db_path"],
account=account,
visible_tasks=tasks,
task_index=0,
account_by_alias={"alias-a": account},
)
self.addCleanup(dialog.close)
dialog.next_button.setFocus()
event = QKeyEvent(QKeyEvent.KeyPress, gui.Qt.Key_Down, gui.Qt.NoModifier)
QApplication.sendEvent(dialog.next_button, event)
self.assertTrue(event.isAccepted())
self.assertEqual(tasks[1].id, dialog.task.id)
self.assertEqual(canonicals[1], dialog.selected_path)
self.assert_removed(temp_dir)
def test_cover_gallery_arrow_keys_do_not_switch_when_text_input_has_focus(self):
with self.make_temp_dir() as temp_dir:
cfg, account, tasks, _canonicals, _archives = self._cover_gallery_task_set(temp_dir, count=2)
dialog = CoverGalleryDialog(
tasks[0],
cfg["image_dir"],
cfg["db_path"],
account=account,
visible_tasks=tasks,
task_index=0,
account_by_alias={"alias-a": account},
)
self.addCleanup(dialog.close)
input_widget = QLineEdit(dialog)
event = QKeyEvent(QKeyEvent.KeyPress, gui.Qt.Key_Down, gui.Qt.NoModifier)
with mock.patch(
"app.gui.tabs.generate.QApplication.focusWidget",
return_value=input_widget,
):
QApplication.sendEvent(input_widget, event)
self.assertEqual(tasks[0].id, dialog.task.id)
self.assert_removed(temp_dir)
def test_cover_gallery_unsaved_switch_save_discard_and_cancel_paths(self):
with self.make_temp_dir() as temp_dir:
cfg, account, tasks, canonicals, archives = self._cover_gallery_task_set(temp_dir, count=2)