T-572 修复封面画廊方向键焦点
This commit is contained in:
+28
-10
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from PySide6.QtCore import QEvent
|
||||||
|
|
||||||
from ..models import GenerateTaskTableModel
|
from ..models import GenerateTaskTableModel
|
||||||
from ..widgets import *
|
from ..widgets import *
|
||||||
from ..workers import GenerateWorker as _RealGenerateWorker
|
from ..workers import GenerateWorker as _RealGenerateWorker
|
||||||
@@ -148,6 +150,7 @@ class CoverGalleryDialog(QDialog):
|
|||||||
self._update_navigation_buttons()
|
self._update_navigation_buttons()
|
||||||
self._update_regenerate_button_state()
|
self._update_regenerate_button_state()
|
||||||
self._fit_to_screen()
|
self._fit_to_screen()
|
||||||
|
self._install_navigation_event_filters()
|
||||||
|
|
||||||
def _old_cover_panel(self):
|
def _old_cover_panel(self):
|
||||||
panel = QWidget()
|
panel = QWidget()
|
||||||
@@ -458,24 +461,34 @@ class CoverGalleryDialog(QDialog):
|
|||||||
event.ignore()
|
event.ignore()
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
if event.key() == Qt.Key_Up and self._should_consume_navigation_key():
|
if self._handle_navigation_key(event, QApplication.focusWidget()):
|
||||||
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()
|
|
||||||
return
|
return
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
def _should_consume_navigation_key(self):
|
def eventFilter(self, watched, event):
|
||||||
focus = QApplication.focusWidget()
|
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:
|
if focus is None or focus is self:
|
||||||
return True
|
return True
|
||||||
return not isinstance(
|
return not isinstance(
|
||||||
focus,
|
focus,
|
||||||
(
|
(
|
||||||
QPushButton,
|
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
QPlainTextEdit,
|
QPlainTextEdit,
|
||||||
QComboBox,
|
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):
|
def open_original_image(self, image_path):
|
||||||
dialog = OriginalImageDialog(image_path, self)
|
dialog = OriginalImageDialog(image_path, self)
|
||||||
dialog.exec()
|
dialog.exec()
|
||||||
@@ -543,6 +560,7 @@ class CoverGalleryDialog(QDialog):
|
|||||||
empty_label.setMinimumSize(320, self.THUMBNAIL_SIZE)
|
empty_label.setMinimumSize(320, self.THUMBNAIL_SIZE)
|
||||||
empty_label.setAlignment(Qt.AlignCenter)
|
empty_label.setAlignment(Qt.AlignCenter)
|
||||||
self.candidate_layout.addWidget(empty_label)
|
self.candidate_layout.addWidget(empty_label)
|
||||||
|
self._install_navigation_event_filters()
|
||||||
|
|
||||||
def _on_regenerate_progress(self, payload):
|
def _on_regenerate_progress(self, payload):
|
||||||
if payload.get("failed"):
|
if payload.get("failed"):
|
||||||
|
|||||||
+5
-2
@@ -3,7 +3,7 @@ id: T-572
|
|||||||
title: ②封面画廊打开后上下键切换记录不受按钮焦点影响
|
title: ②封面画廊打开后上下键切换记录不受按钮焦点影响
|
||||||
phase: 7
|
phase: 7
|
||||||
deps: [T-569]
|
deps: [T-569]
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-09
|
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
@@ -2472,7 +2472,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
dialog.candidate_buttons[canonicals[0]].setFocus()
|
dialog.candidate_buttons[canonicals[0]].setFocus()
|
||||||
event = QKeyEvent(QKeyEvent.KeyPress, gui.Qt.Key_Down, gui.Qt.NoModifier)
|
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.assertTrue(event.isAccepted())
|
||||||
self.assertEqual(tasks[1].id, dialog.task.id)
|
self.assertEqual(tasks[1].id, dialog.task.id)
|
||||||
@@ -2481,6 +2481,56 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
|
|
||||||
self.assert_removed(temp_dir)
|
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):
|
def test_cover_gallery_unsaved_switch_save_discard_and_cancel_paths(self):
|
||||||
with self.make_temp_dir() as temp_dir:
|
with self.make_temp_dir() as temp_dir:
|
||||||
cfg, account, tasks, canonicals, archives = self._cover_gallery_task_set(temp_dir, count=2)
|
cfg, account, tasks, canonicals, archives = self._cover_gallery_task_set(temp_dir, count=2)
|
||||||
|
|||||||
Reference in New Issue
Block a user