From c3c79b63d40d16cfd76ed752bf01e7d89548ee2c Mon Sep 17 00:00:00 2001 From: chengma Date: Thu, 9 Jul 2026 15:51:48 +0800 Subject: [PATCH] =?UTF-8?q?T-573=20=E4=BF=AE=E5=A4=8D=E5=B0=81=E9=9D=A2?= =?UTF-8?q?=E7=94=BB=E5=BB=8A=E5=80=99=E9=80=89=E5=9B=BE=E5=AE=BD=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/gui/tabs/generate.py | 55 ++++++++++++++++++++++++++++++++-------- docs/tasks/T-573.md | 12 +++++++-- tests/test_gui.py | 54 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 12 deletions(-) diff --git a/app/gui/tabs/generate.py b/app/gui/tabs/generate.py index 2063030..761d4e3 100644 --- a/app/gui/tabs/generate.py +++ b/app/gui/tabs/generate.py @@ -2,7 +2,7 @@ from __future__ import annotations -from PySide6.QtCore import QEvent +from PySide6.QtCore import QEvent, QSize from ..models import GenerateTaskTableModel from ..widgets import * @@ -188,13 +188,14 @@ class CoverGalleryDialog(QDialog): self.status_label.setWordWrap(True) layout.addWidget(self.status_label) - scroll = QScrollArea() - scroll.setObjectName("coverGalleryScrollArea") - scroll.setWidgetResizable(False) - scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - content = QWidget() - self.candidate_layout = QHBoxLayout(content) + self.candidate_scroll = QScrollArea() + self.candidate_scroll.setObjectName("coverGalleryScrollArea") + self.candidate_scroll.setWidgetResizable(False) + self.candidate_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.candidate_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.candidate_content = QWidget() + self.candidate_content.setObjectName("coverGalleryCandidateContent") + self.candidate_layout = QHBoxLayout(self.candidate_content) self.candidate_layout.setContentsMargins(0, 0, 0, 0) self.candidate_layout.setSpacing(12) if self.candidates: @@ -207,8 +208,9 @@ class CoverGalleryDialog(QDialog): empty_label.setMinimumSize(320, self.THUMBNAIL_SIZE) empty_label.setAlignment(Qt.AlignCenter) self.candidate_layout.addWidget(empty_label) - scroll.setWidget(content) - layout.addWidget(scroll, 1) + self._sync_candidate_content_size() + self.candidate_scroll.setWidget(self.candidate_content) + layout.addWidget(self.candidate_scroll, 1) return panel def _candidate_item(self, candidate_path, index): @@ -252,6 +254,38 @@ class CoverGalleryDialog(QDialog): layout.addStretch(1) return item + def _sync_candidate_content_size(self): + layout = getattr(self, "candidate_layout", None) + content = getattr(self, "candidate_content", None) + if layout is None or content is None: + return + layout.activate() + margins = layout.contentsMargins() + spacing = max(0, layout.spacing()) + hint = layout.sizeHint() + if self.candidates: + candidate_count = len(self.candidates) + min_width = ( + margins.left() + + margins.right() + + candidate_count * (self.THUMBNAIL_SIZE + 16) + + max(0, candidate_count - 1) * spacing + ) + else: + min_width = margins.left() + margins.right() + 320 + min_height = max( + self.THUMBNAIL_SIZE, + hint.height(), + content.minimumHeight(), + ) + size = QSize(max(min_width, hint.width()), min_height) + content.setMinimumSize(size) + content.resize(size) + content.updateGeometry() + scroll = getattr(self, "candidate_scroll", None) + if scroll is not None: + scroll.updateGeometry() + def _sync_initial_selection(self): for candidate_path, radio in self.candidate_buttons.items(): if _same_file(candidate_path, self.current_path): @@ -560,6 +594,7 @@ class CoverGalleryDialog(QDialog): empty_label.setMinimumSize(320, self.THUMBNAIL_SIZE) empty_label.setAlignment(Qt.AlignCenter) self.candidate_layout.addWidget(empty_label) + self._sync_candidate_content_size() self._install_navigation_event_filters() def _on_regenerate_progress(self, payload): diff --git a/docs/tasks/T-573.md b/docs/tasks/T-573.md index 01797dd..ee3927b 100644 --- a/docs/tasks/T-573.md +++ b/docs/tasks/T-573.md @@ -3,7 +3,7 @@ id: T-573 title: ②封面画廊候选图横向滚动内容宽度修复 phase: 7 deps: [T-567, T-569] -status: TODO +status: DONE created: 2026-07-09 --- @@ -65,4 +65,12 @@ T-568/T-569 后封面画廊支持切换任务和重新生成,候选图列表 ## 执行记录 -(做完在这里写:改了什么文件、跑了什么验证命令及结果、关键决策。) +- 2026-07-09:已完成。 + - `app/gui/tabs/generate.py`:`CoverGalleryDialog` 保存 `candidate_scroll` / `candidate_content` 引用,新增 `_sync_candidate_content_size()`;候选图初始化和 `_rebuild_candidate_items()` 后按候选数量、item 宽度、spacing 与 layout sizeHint 同步 content 最小尺寸和实际尺寸,保留横向滚动行为。 + - `tests/test_gui.py`:新增多候选图内容宽度测试,以及从少候选任务切换到多候选任务后的宽度重新同步测试。 + - 验证通过: + - `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` diff --git a/tests/test_gui.py b/tests/test_gui.py index c515a57..8bdeb0a 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -2277,6 +2277,60 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_cover_gallery_candidate_content_width_fits_multiple_candidates(self): + with self.make_temp_dir() as temp_dir: + cfg, account, tasks, canonicals, _archives = self._cover_gallery_task_set(temp_dir, count=1) + prefix = os.path.splitext(canonicals[0])[0] + self.write_test_image(f"{prefix}_20260709101100.jpg") + self.write_test_image(f"{prefix}_20260709101200.jpg") + + dialog = CoverGalleryDialog(tasks[0], cfg["image_dir"], cfg["db_path"], account=account) + self.addCleanup(dialog.close) + + self.assertGreaterEqual(len(dialog.candidates), 4) + expected_width = ( + len(dialog.candidates) * (dialog.THUMBNAIL_SIZE + 16) + + (len(dialog.candidates) - 1) * dialog.candidate_layout.spacing() + ) + self.assertIs(dialog.candidate_scroll.widget(), dialog.candidate_content) + self.assertGreaterEqual(dialog.candidate_content.minimumWidth(), expected_width) + self.assertGreaterEqual(dialog.candidate_content.width(), expected_width) + + self.assert_removed(temp_dir) + + def test_cover_gallery_switch_task_resyncs_candidate_content_width(self): + with self.make_temp_dir() as temp_dir: + cfg, account, tasks, canonicals, archives = self._cover_gallery_task_set(temp_dir, count=2) + os.remove(archives[0]) + prefix = os.path.splitext(canonicals[1])[0] + self.write_test_image(f"{prefix}_20260709101100.jpg") + self.write_test_image(f"{prefix}_20260709101200.jpg") + 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) + initial_width = dialog.candidate_content.minimumWidth() + + self.assertTrue(dialog.switch_task(1)) + + expected_width = ( + len(dialog.candidates) * (dialog.THUMBNAIL_SIZE + 16) + + (len(dialog.candidates) - 1) * dialog.candidate_layout.spacing() + ) + self.assertEqual(tasks[1].id, dialog.task.id) + self.assertGreater(len(dialog.candidates), 2) + self.assertGreater(dialog.candidate_content.minimumWidth(), initial_width) + self.assertGreaterEqual(dialog.candidate_content.minimumWidth(), expected_width) + self.assertGreaterEqual(dialog.candidate_content.width(), expected_width) + + self.assert_removed(temp_dir) + def test_cover_gallery_save_switches_current_cover(self): with self.make_temp_dir() as temp_dir: cfg = self.make_config(temp_dir)