Files

98 lines
8.4 KiB
Markdown
Raw Permalink Normal View History

---
id: T-629
title: ②封面画廊切换记录后候选勾选区自适应布局修复
phase: 7
deps: [T-626]
status: DONE
created: 2026-07-14
---
## 问题 / 背景
②「AI生成」列表双击任务行后会打开封面画廊。当前记录首次显示时,右侧生成图及其勾选框位置正常;按 `↑ / ↓` 切换到上一条或下一条记录后,候选图下方的勾选框可能进入图片显示区域,造成控件与图片重叠。
已核实的代码现状:
- T-626 让候选图片尺寸根据 viewport 和候选数量在 `180~360px` 内变化,但候选 item 仍由“图片 + `QRadioButton` + 说明文字”共同组成,整体高度没有使用各子控件的实际尺寸统一计算。
- `_candidate_thumbnail_target_size()` 使用固定 `CANDIDATE_TEXT_HEIGHT = 72` 预留图片下方区域;实际勾选框和说明文字高度会受候选名称、自动换行、字体及 Windows DPI 影响,固定值不能代表下一条记录的真实高度。
- `_rebuild_candidate_items()` 切换任务时先沿用上一条记录的 `candidate_thumbnail_size` 创建新 item,再依赖约 `60ms` 的 resize timer 延迟重算;切换瞬间可能出现“新内容 + 旧尺寸/旧高度”的布局组合。
- `_update_candidate_thumbnail_layout()` 主要更新 item 宽度和图片 label 的正方形尺寸,没有显式把 item 最小高度同步为本轮布局的真实 `sizeHint()`。
- 候选滚动区关闭垂直滚动;内容高度估算不足时没有垂直空间兜底,控件会被压缩、裁切或表现为进入图片区域。
- 现有 GUI 测试验证了候选图片尺寸、横向内容宽度和任务切换,但测试样本的候选数量较接近,也没有断言图片、勾选框和说明文字之间的几何关系,因此未覆盖本缺陷。
## 方案
### 1. 按完整候选卡片计算布局
- 修改 `app/gui/tabs/generate.py` 的 `CoverGalleryDialog`,将每个候选项作为完整卡片管理:图片、勾选框、说明文字和布局间距必须参与同一次尺寸计算。
- 推荐抽出内部候选卡片组件或等价 helper,提供 `set_thumbnail_size(size)`;更新图片尺寸后主动使内部布局失效并重新计算几何尺寸。
- 不再把固定 `CANDIDATE_TEXT_HEIGHT = 72` 作为图片下方区域的唯一高度依据。图片目标尺寸应根据 viewport 扣除候选卡片实际控制区高度后计算;固定值最多只能作为最小预留值。
- 控制区实际高度至少包含:
- `QRadioButton.sizeHint().height()`;
- 说明文字在当前 item 宽度下的 `heightForWidth()` 或布局实际 `sizeHint()`;
- 布局 margins 和 spacing。
- 每轮更新后,候选 item 的最小高度必须不小于内部布局实际所需高度;`candidate_content` 的最小高度和实际高度必须不小于所有候选 item 的最大所需高度。
- 勾选框必须处于独立的图片下方布局行,不使用绝对坐标或覆盖层放置。
### 2. 切换任务时同步重排
- `switch_task()` / `_reload_task_and_candidates()` 重建候选列表后,应基于新任务的候选数量和当前 viewport 立即执行一次强制尺寸计算及布局同步。
- 新候选 item 不应等待 resize timer 才从上一条记录尺寸切换到当前记录尺寸。现有约 `60ms` timer 可继续用于合并窗口连续 resize 事件,但不能作为任务切换正确性的唯一保障。
- 同步更新候选 item、图片 label、候选内容容器的宽高后,调用必要的 `invalidate()`、`activate()`、`updateGeometry()` 或等价 Qt 布局刷新接口。
- 切换记录后把候选横向滚动位置重置到起点,避免下一条记录打开时停留在上一条的横向偏移;不得改变当前候选选择、未保存确认及 `↑ / ↓` 导航语义。
### 3. 候选文案与防御性布局
- 候选项可见名称保持简短稳定,例如「当前生效」「候选 2」;完整文件名放入中文 tooltip,避免长文件名通过换行无上限撑高控制区。
- 元信息保持简洁,优先单行或受控换行;图片读取失败占位仍占用与正常图片相同的图片区域。
- 正常路径应通过正确的内容高度消除重叠,不以开启垂直滚动条作为主要修复。若保留防御性垂直滚动,仍必须保证每个候选卡片内部不重叠。
- 画廊处于最小支持窗口或 Windows 高 DPI 环境时,可以缩小图片或出现横向滚动,但不能让图片覆盖勾选框和说明文字。
### 4. 回归测试
- 更新 `tests/test_gui.py`,构造候选数量和文案高度明显不同的两条任务,覆盖:
- 单候选切换到 3 张以上候选;
- 多候选切换回单候选;
- 连续按 `↑ / ↓` 往返切换;
- 当前生效候选、历史候选、长文件名及损坏图片占位。
- 测试应在弹窗显示并处理 Qt 事件后检查真实几何关系,至少断言:
- 勾选框顶部位于对应图片底部和布局间距之后;
- 说明文字位于勾选框之后;
- item 最小高度覆盖内部布局 `sizeHint()`;
- `candidate_content` 高度覆盖最高候选 item;
- 所有候选 item 的横向范围不互相重叠。
- 分别检查任务切换同步完成后的状态,以及 resize timer 触发后的状态,防止只在延迟重排后暂时恢复。
- 保留 T-572、T-573、T-574、T-626 的方向键导航、横向滚动、保存和响应式缩放回归测试。
## 验收要点
- 双击②列表任意行打开封面画廊后,按 `↓` 或 `↑` 切换记录,所有候选图的勾选框始终完整位于图片下方,不进入图片、遮挡图片或被裁切。
- 在单候选、多候选、长候选名称、图片读取失败及候选数量变化场景下,图片、勾选框和说明文字均不重叠。
- 切换记录完成后立即获得正确布局,不出现可感知的先重叠再跳回现象。
- 调整弹窗大小后,候选图片继续按 T-626 的范围自适应,候选卡片整体高度同步刷新。
- 在常见 `1366×768`、`1920×1080` 及 Windows 100% / 125% / 150% 显示缩放下,控件无重叠和明显布局抖动。
- 不影响候选选择、保存提示、重置图片、原图查看、未保存确认和 `↑ / ↓` 切换记录。
- 验证命令:
- `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`
## 边界(不改什么)
- 不修改候选图发现、归档、排序、选择或保存语义。
- 不修改 SQLite schema、任务生成状态、提示词、AI/cmhub 请求、生图并发或图片保存格式。
- 不修改 Excel、③更新蝦皮、CDP 或蝦皮网页操作逻辑。
- 不通过增大固定窗口、硬编码更大的文字区高度或隐藏勾选框规避布局问题。
## 执行记录
- 2026-07-14:完成 T-629。
- 复现确认:从单候选切换到 6 张候选时,旧实现把候选内容高度暂时设为图片高度 `360px`,但完整卡片实际需要 `402px`,导致勾选框被压到图片内部;延迟 resize timer 执行后才可能恢复。
- `app/gui/tabs/generate.py`:新增内部候选卡片组件,统一按图片、勾选框、说明文字、margin 和 spacing 的实际尺寸计算固定卡片高度;内容容器按所有卡片真实宽高同步尺寸,候选卡片顶部对齐。
- 任务切换和候选重建后立即按新候选数量及当前 viewport 强制重算图片与完整卡片尺寸,不再依赖约 `60ms` timer 才获得正确布局;切换记录时同时重置横向滚动位置,窗口连续 resize 仍保留合并重绘。
- 非当前候选的可见名称改为「候选 N」,完整文件名保留在中文 tooltip,避免长文件名干扰尺寸;损坏图片继续使用固定图片区和「图片读取失败」占位。
- `tests/test_gui.py`:新增真实 `↓` 键切换回归测试,覆盖单候选到多候选、长文件名、损坏图片、切换同步阶段、Qt 事件处理后、横向滚动复位和往返切换;逐卡断言图片、勾选框、说明文字及横向卡片几何不重叠。
- 当前工作区运行 `tests.test_gui` 时,仅有任务开始前已存在的默认封面提示词 `papa1.txt` 改名造成的 2 项旧断言失败;未回退用户改动。在只包含 T-629 三个文件的隔离 worktree 中验证通过:`py -3.10 -m unittest tests.test_gui`(182 项)、`py -3.10 -m unittest discover -s tests`(462 项)、`python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check`。