From 2ca0655c90bd3b1226f1802760bbe7b59f4acfed Mon Sep 17 00:00:00 2001 From: ila Date: Sat, 4 Jul 2026 17:30:10 +0800 Subject: [PATCH] feat: hide preview controls with esc --- src/app/widgets/image_canvas.py | 32 ++++++++++ tasks.md | 28 ++++----- tests/test_image_canvas.py | 107 ++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 tests/test_image_canvas.py diff --git a/src/app/widgets/image_canvas.py b/src/app/widgets/image_canvas.py index 1f4f622..b9434b7 100644 --- a/src/app/widgets/image_canvas.py +++ b/src/app/widgets/image_canvas.py @@ -54,6 +54,7 @@ class _CanvasView(QGraphicsView): self.setResizeAnchor(QGraphicsView.AnchorViewCenter) self.setBackgroundBrush(QColor('#9a9da3')) self.setStyleSheet('border: none; background-color: #9a9da3;') + self.setFocusPolicy(Qt.StrongFocus) def drawBackground(self, painter: QPainter, rect): super().drawBackground(painter, rect) @@ -80,6 +81,7 @@ class _CanvasView(QGraphicsView): def mousePressEvent(self, event): if event.button() == Qt.LeftButton: + self.setFocus(Qt.MouseFocusReason) self._canvas._on_press(self.mapToScene(event.pos())) super().mousePressEvent(event) @@ -94,6 +96,12 @@ class _CanvasView(QGraphicsView): self._canvas._on_release(self.mapToScene(event.pos())) super().mouseReleaseEvent(event) + def keyPressEvent(self, event): + if event.key() == Qt.Key_Escape and self._canvas._hide_controls(): + event.accept() + return + super().keyPressEvent(event) + # ── public widget ───────────────────────────────────────────────────────────── @@ -117,6 +125,7 @@ class ImageCanvas(QWidget): self._corner_handles: List[QGraphicsEllipseItem] = [] self._rot_line: Optional[QGraphicsLineItem] = None self._rot_handle: Optional[QGraphicsEllipseItem] = None + self._controls_visible = True # Current transform (authoritative) self._state: Optional[TransformState] = None @@ -270,6 +279,7 @@ class ImageCanvas(QWidget): self._corner_handles = [] self._rot_line = None self._rot_handle = None + self._controls_visible = True self._state = None self._garment_item = QGraphicsPixmapItem(pix) @@ -380,6 +390,7 @@ class ImageCanvas(QWidget): self._rot_handle.setZValue(3) self._rot_handle.setData(0, _T_ROT) self._scene.addItem(self._rot_handle) + self._set_controls_visible(True) def _refresh_print_display(self): """Reposition all print-layer items from self._state.""" @@ -421,6 +432,25 @@ class ImageCanvas(QWidget): self._rot_line.setLine(top_cx, top_cy, rot_sx, rot_sy) self._rot_handle.setPos(rot_sx, rot_sy) + def _control_items(self): + items = [] + for item in (self._sel_rect, self._rot_line, self._rot_handle): + if item is not None: + items.append(item) + items.extend(self._corner_handles) + return items + + def _set_controls_visible(self, visible: bool): + self._controls_visible = visible + for item in self._control_items(): + item.setVisible(visible) + + def _hide_controls(self) -> bool: + if self._print_item is None or not self._controls_visible: + return False + self._set_controls_visible(False) + return True + # ── coordinate helpers ──────────────────────────────────────────────────── @staticmethod @@ -479,6 +509,8 @@ class ImageCanvas(QWidget): tag = self._hit_test(scene_pos) if tag is None: return + if tag == _T_PRINT and not self._controls_visible: + self._set_controls_visible(True) self._drag_tag = tag self._drag_start_scene = scene_pos diff --git a/tasks.md b/tasks.md index 25bc155..40644ba 100644 --- a/tasks.md +++ b/tasks.md @@ -1658,20 +1658,20 @@ 任务: -- [ ] `image_canvas.py`:让 `_CanvasView` 可以获得键盘焦点,并在点击预览区时获取焦点 -- [ ] `image_canvas.py`:新增控制层显隐状态,例如 `_controls_visible` -- [ ] `image_canvas.py`:新增统一方法显示 / 隐藏 `_sel_rect`、`_corner_handles`、`_rot_line`、`_rot_handle` -- [ ] `image_canvas.py`:按 `Esc` 时只隐藏交互控制层,不隐藏 `_print_item` -- [ ] `image_canvas.py`:按 `Esc` 不修改 `TransformState`,不触发导出参数变化 -- [ ] `image_canvas.py`:控制层隐藏后,点击印花图片先恢复控制层,再沿用现有拖动逻辑 -- [ ] `image_canvas.py`:隐藏期间缩放控制点和旋转控制点不需要响应命中测试;恢复后原有缩放、旋转行为保持不变 -- [ ] `image_canvas.py`:重新加载印花图片或创建新的印花图层时,控制层默认显示 -- [ ] 测试或离屏验证:覆盖 `Esc` 隐藏、点击印花恢复、`TransformState` 不变 +- [x] `image_canvas.py`:让 `_CanvasView` 可以获得键盘焦点,并在点击预览区时获取焦点 +- [x] `image_canvas.py`:新增控制层显隐状态,例如 `_controls_visible` +- [x] `image_canvas.py`:新增统一方法显示 / 隐藏 `_sel_rect`、`_corner_handles`、`_rot_line`、`_rot_handle` +- [x] `image_canvas.py`:按 `Esc` 时只隐藏交互控制层,不隐藏 `_print_item` +- [x] `image_canvas.py`:按 `Esc` 不修改 `TransformState`,不触发导出参数变化 +- [x] `image_canvas.py`:控制层隐藏后,点击印花图片先恢复控制层,再沿用现有拖动逻辑 +- [x] `image_canvas.py`:隐藏期间缩放控制点和旋转控制点不需要响应命中测试;恢复后原有缩放、旋转行为保持不变 +- [x] `image_canvas.py`:重新加载印花图片或创建新的印花图层时,控制层默认显示 +- [x] 测试或离屏验证:覆盖 `Esc` 隐藏、点击印花恢复、`TransformState` 不变 验收: -- [ ] 点击预览区后按 `Esc`,蓝色选中框、缩放控制点、旋转连接线和旋转控制点隐藏 -- [ ] 按 `Esc` 后印花图片仍然显示,预览不变为空白 -- [ ] 控制层隐藏后再次点击印花,控制层恢复,并可继续拖动、缩放、旋转 -- [ ] 控制层显隐不改变 X/Y、宽高、角度,也不影响单张导出和批量导出 -- [ ] 通过必要语法检查和离屏 GUI 验证 +- [x] 点击预览区后按 `Esc`,蓝色选中框、缩放控制点、旋转连接线和旋转控制点隐藏 +- [x] 按 `Esc` 后印花图片仍然显示,预览不变为空白 +- [x] 控制层隐藏后再次点击印花,控制层恢复,并可继续拖动、缩放、旋转 +- [x] 控制层显隐不改变 X/Y、宽高、角度,也不影响单张导出和批量导出 +- [x] 通过必要语法检查和离屏 GUI 验证:`python -m py_compile src/app/widgets/image_canvas.py tests/test_image_canvas.py`、`python tests/test_image_canvas.py` diff --git a/tests/test_image_canvas.py b/tests/test_image_canvas.py new file mode 100644 index 0000000..f08ca5f --- /dev/null +++ b/tests/test_image_canvas.py @@ -0,0 +1,107 @@ +"""Offscreen tests for image canvas interaction controls.""" +import os +import shutil +import sys +import tempfile +import unittest +from pathlib import Path + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from PIL import Image + + +class TestImageCanvasControls(unittest.TestCase): + def setUp(self): + self.tmp = Path(tempfile.mkdtemp()) + self.app = self._app() + + def tearDown(self): + shutil.rmtree(str(self.tmp), ignore_errors=True) + + def _app(self): + from PySide6.QtWidgets import QApplication + + app = QApplication.instance() + if app is None: + app = QApplication([]) + return app + + def _write_image(self, name, size, color): + path = self.tmp / name + Image.new("RGBA", size, color).save(str(path), "PNG") + return path + + def _canvas(self): + from app.widgets.image_canvas import ImageCanvas + + garment = self._write_image("garment.png", (320, 240), (240, 240, 240, 255)) + print_path = self._write_image("print.png", (80, 40), (0, 120, 255, 180)) + canvas = ImageCanvas() + canvas.resize(640, 480) + canvas.show() + canvas.load_garment(garment) + canvas.load_print(print_path) + self.app.processEvents() + return canvas + + def _controls_visible(self, canvas): + return all(item.isVisible() for item in canvas._control_items()) + + def _state_tuple(self, canvas): + state = canvas.get_transform() + return ( + state.x, + state.y, + state.width, + state.height, + state.rotation, + state.keep_aspect_ratio, + ) + + def test_escape_hides_controls_without_changing_transform(self): + from PySide6.QtCore import Qt + from PySide6.QtTest import QTest + + canvas = self._canvas() + before = self._state_tuple(canvas) + + self.assertEqual(canvas._view.focusPolicy(), Qt.StrongFocus) + self.assertTrue(self._controls_visible(canvas)) + canvas._view.setFocus() + QTest.keyClick(canvas._view, Qt.Key_Escape) + self.app.processEvents() + + self.assertFalse(canvas._controls_visible) + self.assertFalse(self._controls_visible(canvas)) + self.assertTrue(canvas._print_item.isVisible()) + self.assertEqual(self._state_tuple(canvas), before) + + def test_clicking_print_restores_controls_and_keeps_drag_behavior(self): + from PySide6.QtCore import QPointF + + canvas = self._canvas() + canvas._hide_controls() + state = canvas.get_transform() + + canvas._on_press(QPointF(state.x + state.width / 2.0, state.y + state.height / 2.0)) + + self.assertTrue(canvas._controls_visible) + self.assertTrue(self._controls_visible(canvas)) + self.assertEqual(canvas._drag_tag, "print") + + def test_loading_print_restores_controls_visible_by_default(self): + canvas = self._canvas() + canvas._hide_controls() + self.assertFalse(canvas._controls_visible) + + next_print = self._write_image("print2.png", (48, 48), (255, 80, 0, 200)) + canvas.load_print(next_print) + + self.assertTrue(canvas._controls_visible) + self.assertTrue(self._controls_visible(canvas)) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file