refactor(panels): 浮动面板抽出唯一"面板特征层",消除 3 处分类/滚动几何/滚动设置的重复
背景(用户观察):7 个实验性功能都有自己的浮动面板,观感与骨架高度一致
(标题栏 = 折起三角 + GX 徽标 + 标题 + ✕,内容区可滚动 + 滚动条,底部按钮行),
但每新增一种面板或改动布局,总会出"意料之外"的缺陷。
审计结论:骨架绘制本来只有一份,真正的问题是**同一套"按种类分支"的代码被抄了多份**:
| 重复项 | 位置 | 份数 |
| --- | --- | --- |
| 分类布尔量(listStyle / countTable / hasButton…) | EdgePanelWidth / EdgePanelLayoutFor / PaintEdgeCursor | 3 |
| 滚动条几何 + 滚动量夹取 | 布局里表格 / 列表 / 自由标记三个分支 | 3 |
| 滚动条绘制(6 行) | 绘制里同样三个分支 | 3 |
| SetLane*Scroll 实现 | PlotViewFeatures.cpp | 7 |
| 按种类转发滚动写入 | 滚轮 / 拖滚动条 | 2 |
| 标题文本 | EdgePanelWidth 的三元式链 / 绘制里的三元式链 | 2(内容还不一致!) |
一、新增"面板特征层"(PlotViewInternal.hpp,pvdetail)
* `PanelStyle PanelStyleOf(EdgePanelKind)`:唯一分类来源,`listRows` / `countTable` /
`markerGroups` + `buildButton` / `twoButtons` / `markerFooter`,`switch` 穷举不写 default
(新增面板漏登记 ⇒ `-Wswitch` 编译报警)。
其中专门区分 `footerButtons()`(底部有按钮行)与 `buildButton`(是"构建为新信号")——
混为一谈会让「选区截取」底部少预留一行高度(本轮实测 124 → 87 px,被像素基线当场抓住)。
* `PanelScrollMetricsFor(...)`:滚动条轨道/滑块几何 + 滚动量夹取 + 内容宽度扣减的唯一实现。
* `PlotView::PanelTitle(EdgePanelKind)`:标题唯一来源。
⚠️ 修掉一处**潜伏**错误:`EdgePanelWidth` 的标题链漏了 MARK/CLIP(两者按"边沿计数"量宽),
目前两种标题恰好等宽所以看不出来,但任何文案改动都会让宽度与标题不匹配。
二、收敛调用点
* `EdgePanelWidth` / `EdgePanelLayoutFor`(表格 / 列表 / 自由标记三分支)/ `PaintEdgeCursor`
全部改用 `PanelStyleOf` 与 `PanelTitle`;滚动几何改用 `PanelScrollMetricsFor`。
* `EdgePanelLayoutFor` 的 5 层滚动量三元式 → `LanePanelScrollOf(lane, kind)`(该派发点第 33 轮
就已存在,但布局这里一直没用它)。
* `PaintEdgeCursor` 的滚动条绘制抽成 `paintPanelScrollBar(layout)`,三处调用。
* 7 个 `SetLane*Scroll` 改为一行转发到新的 `SetLanePanelScroll(laneIndex, kind, scroll)`;
滚轮与"拖滚动条"两条 `switch`/`if` 链改为直接调它(读与写从此同一个入口)。
三、新增离屏检查 `panelscroll`(护栏)
* 对 7 种面板逐一:可滚动时滚到底 / 滚回顶,断言 `scrollOffset` 夹取到 `maxScroll`、
滑块到达/返回轨道端点、内容区几何不随滚动改变;不可滚动时断言不出现滚动条。
* 用 `MF4-Record.MF4`(566 信号合并到同一网格)实测 6/7 面板可滚(自由标记无固定点,正确)。
没有这条检查,本次收敛就等于"改完只能靠肉眼"。
验证(本机 GCC 15.2.0 UCRT / 125% DPI)
* `panels -mode compare`:7 个面板的**像素哈希与几何逐项不变**(零差异)。
这条检查当场抓到了上面 `footerButtons` 的回归,证明基线有效。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `UnitTests`:`checks=219 failed=0`。
* release 全量零 error / 零 warning;产物 788,480 → **787,968 B**(−512)。
This commit is contained in:
1 parent
5c48bf922e
commit
abcf3e6c44
7 files changed
+416
-250
No files matched your search
@@ -1197,6 +1197,20 @@ namespace jdi
|
||||
*/
|
||||
bool SetLaneFreeMarkerOpacity(int laneIndex, float opacity);
|
||||
|
||||
/**
|
||||
* @brief 设置某种面板的**内容滚动偏移**(客户区像素,0 = 顶部)—— 唯一实现。
|
||||
*
|
||||
* ⚠️ 下面七个 `SetLane*Scroll` 都只是本函数的一行转发(保留是为了不改调用点与原语义)。
|
||||
* 新增面板时**只需**在 `LanePanelScrollOf` / `PanelStyleOf` / `PanelTitle` 三处登记,
|
||||
* 不要再复制一份"取字段 → 夹取 → 比较 → 写回"的实现。
|
||||
*
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @param [IN] kind 面板种类。
|
||||
* @param [IN] scroll 目标滚动量(负值按 0 处理)。
|
||||
* @return 偏移真的发生变化返回 true。
|
||||
*/
|
||||
bool SetLanePanelScroll(int laneIndex, EdgePanelKind kind, float scroll);
|
||||
|
||||
/**
|
||||
* @brief 设置某网格「自由标记」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||||
* @return 偏移真的发生变化返回 true。
|
||||
@@ -2846,6 +2860,23 @@ namespace jdi
|
||||
double edgeValue, bool hasRange, double rangeMin, double rangeMax,
|
||||
std::size_t& rising, std::size_t& falling, bool& complete) const;
|
||||
|
||||
/**
|
||||
* @brief 浮动面板的**标题文本**(唯一来源:宽度计算与绘制共用)。
|
||||
*
|
||||
* ⚠️ 过去标题在两处各写一条多层三元式:
|
||||
* * `EdgePanelWidth` 里的链**漏了「自由标记」与「选区截取」**(两种面板都退化成
|
||||
* 「边沿计数」的文字来量宽度);
|
||||
* * `PaintEdgeCursor` 里的链是完整的。
|
||||
* 目前两种标题恰好与「边沿计数」等宽(或宽度被其它列主导),所以看不出来;
|
||||
* 但任何一次文案改动都会让"面板宽度与实际标题不匹配"(标题被省略 / 右侧留白)。
|
||||
*
|
||||
* 现在宽度与绘制都调本函数,且 `switch` 穷举、不写 `default:`。
|
||||
*
|
||||
* @param [IN] kind 面板种类。
|
||||
* @return 标题(「边沿计数」与「中心轴边沿计数」会追加双光标区间尾缀)。
|
||||
*/
|
||||
std::wstring PanelTitle(EdgePanelKind kind) const;
|
||||
|
||||
/**
|
||||
* @brief 计算实验性功能面板所需的宽度(排版与绘制共用,保证文字完整不被省略)。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
|
||||
@@ -1178,6 +1178,142 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- 面板的「排版类别」与「滚动几何」的唯一来源 --------------------------------
|
||||
//
|
||||
// ⚠️⚠️ 为什么要有这一层:面板的**分类布尔量**过去在三个地方各写一份
|
||||
// (`EdgePanelWidth` / `EdgePanelLayoutFor` / `PaintEdgeCursor`),
|
||||
// 每份都是 6 个 `kind == XXX` + 3 条组合判断。新增一种面板时若只改其中两处,
|
||||
// 就会出现"宽度按 A 类算、绘制按 B 类画"的错位 —— 这正是历史上
|
||||
// 「元素超出浮动面板」「内容区多出一段空白」「滚动条画在表头上」这类缺陷的共同形态。
|
||||
// 现在只在这里分派一次,三个消费者都读同一份结果。
|
||||
//
|
||||
// ⚠️ 同样**不写 `default:`**:新增 `EdgePanelKind` 而未登记时,
|
||||
// `-Wswitch`(属 `-Wall`)会直接编译报警。
|
||||
|
||||
/**
|
||||
* @brief 一种浮动面板的排版类别(互斥 + 若干可选部件)。
|
||||
*
|
||||
* 三类互斥的内容排版:
|
||||
* * `listRows` —— 可滚动**分组列表**:条目栏(三角 + 名称 + 复选框)+ 展开后的参数行;
|
||||
* * `countTable` —— **表格**:固定表头(信号名 / 上升沿 / 下降沿)+ 可滚动行;
|
||||
* * `markerGroups` —— **分组栏 + 条目**:每个信号一个可折起分组栏 + 固定点条目 + 吸顶。
|
||||
*/
|
||||
struct PanelStyle
|
||||
{
|
||||
bool listRows = false; /**< 可滚动分组列表(均值 / 中心轴 / 弱峰 / 重采样 / 选区截取)。 */
|
||||
bool countTable = false; /**< 固定表头的表格(边沿计数)。 */
|
||||
bool markerGroups = false; /**< 分组栏 + 条目(自由标记)。 */
|
||||
|
||||
/** @brief 内容区是否可滚动(三类都是,保留字段以便未来出现「不可滚动」的面板)。 */
|
||||
bool scrollableContent() const { return listRows || countTable || markerGroups; }
|
||||
|
||||
/** @brief 底部是否有「构建为新信号」样式的单一按钮。 */
|
||||
bool buildButton = false;
|
||||
/** @brief 底部是否有**两个并排**按钮(选区截取)。 */
|
||||
bool twoButtons = false;
|
||||
/** @brief 底部是否有「自由标记」那组固定控件(透明度滑块 + 3 个切换按钮)。 */
|
||||
bool markerFooter = false;
|
||||
|
||||
/**
|
||||
* @brief 底部是否存在任何内容(按钮 / 滑块 / 开关)。
|
||||
*
|
||||
* ⚠️ 与 `buildButton` **不是同一个概念**:「选区截取」有两个并排按钮,
|
||||
* 但没有「构建为新信号」;而面板高度算式里的「按钮行高」必须两者都为真。
|
||||
* 曾经把两者当作一回事 ⇒ 截取面板底部少预留一行高度(124 → 87 px),
|
||||
* 内容区却仍按旧高度计算 ⇒ 面板高度与内容区不一致。
|
||||
*/
|
||||
bool footerButtons() const { return buildButton || twoButtons; }
|
||||
};
|
||||
|
||||
/** @brief 取某种面板的排版类别(唯一分类来源)。 */
|
||||
inline PanelStyle PanelStyleOf(EdgePanelKind kind)
|
||||
{
|
||||
PanelStyle style;
|
||||
switch (kind)
|
||||
{
|
||||
case EdgePanelKind::COUNT:
|
||||
style.countTable = true;
|
||||
break;
|
||||
case EdgePanelKind::CENTER:
|
||||
style.listRows = true;
|
||||
break;
|
||||
case EdgePanelKind::MEAN:
|
||||
style.listRows = true;
|
||||
style.buildButton = true;
|
||||
break;
|
||||
case EdgePanelKind::PEAK:
|
||||
style.listRows = true;
|
||||
style.buildButton = true;
|
||||
break;
|
||||
case EdgePanelKind::MARK:
|
||||
style.markerGroups = true;
|
||||
style.markerFooter = true;
|
||||
break;
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
style.listRows = true;
|
||||
style.buildButton = true;
|
||||
break;
|
||||
case EdgePanelKind::CLIP:
|
||||
style.listRows = true;
|
||||
style.twoButtons = true;
|
||||
break;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 浮动面板**内容区滚动条**的几何与滚动量(三个排版分支共用同一套算式)。
|
||||
*
|
||||
* 旧实现把这段算式在 `EdgePanelLayoutFor` 里抄了三遍(表格 / 列表 / 自由标记),
|
||||
* 三份的差异只在于"总内容高"和"滚动量来自哪个字段"。任何一次微调(比如把
|
||||
* 最小滑块高度 18 改成别的)都必须记得改三处 —— 典型的"漏一处就错位"。
|
||||
*
|
||||
* @param [IN] panelLeft 面板左边界(客户区)。
|
||||
* @param [IN] contentTop 内容区上边界。
|
||||
* @param [IN] viewHeight 内容区可视高度。
|
||||
* @param [IN] contentTotal 内容总高度(可能大于可视高度)。
|
||||
* @param [IN] scrollValue 期望滚动量(会被夹到 `[0, maxScroll]`)。
|
||||
* @param [IN] widthWithoutScroll 未扣滚动条时的内宽。
|
||||
* @param [IN] leftPad 面板左内边距。
|
||||
* @param [IN] scrollWidth 滚动条宽度(已乘缩放)。
|
||||
* @param [IN] scale DPI 缩放。
|
||||
* @return 滚动指标(`scrollable == false` 时 `track/thumb` 为空矩形、`innerWidth` 不扣减)。
|
||||
*/
|
||||
struct PanelScrollMetrics
|
||||
{
|
||||
bool scrollable = false; /**< 内容是否超高(需要滚动条)。 */
|
||||
float maxScroll = 0.0f; /**< 最大滚动量。 */
|
||||
float scroll = 0.0f; /**< 实际滚动量(已夹取)。 */
|
||||
float innerWidth = 0.0f; /**< 内容可用宽度(需要滚动条时已扣掉它)。 */
|
||||
gd::RectF track; /**< 滚动条轨道。 */
|
||||
gd::RectF thumb; /**< 滚动条滑块。 */
|
||||
};
|
||||
|
||||
inline PanelScrollMetrics PanelScrollMetricsFor(float panelLeft, float contentTop, float viewHeight,
|
||||
float contentTotal, float scrollValue,
|
||||
float widthWithoutScroll, float leftPad,
|
||||
float scrollWidth, float scale)
|
||||
{
|
||||
PanelScrollMetrics metrics;
|
||||
metrics.scrollable = contentTotal > viewHeight + 0.5f;
|
||||
metrics.maxScroll = std::max(0.0f, contentTotal - viewHeight);
|
||||
metrics.scroll = std::max(0.0f, std::min(scrollValue, metrics.maxScroll));
|
||||
metrics.innerWidth = std::max(
|
||||
0.0f, widthWithoutScroll - (metrics.scrollable ? (scrollWidth + 4.0f * scale) : 0.0f));
|
||||
if (!metrics.scrollable)
|
||||
{
|
||||
return metrics;
|
||||
}
|
||||
metrics.track = gd::RectF(panelLeft + leftPad + metrics.innerWidth + 4.0f * scale, contentTop,
|
||||
scrollWidth, viewHeight);
|
||||
const float thumbHeight = std::max(18.0f * scale, viewHeight * (viewHeight / contentTotal));
|
||||
const float thumbTop =
|
||||
contentTop + (viewHeight - thumbHeight) *
|
||||
((metrics.maxScroll > 0.0f) ? (metrics.scroll / metrics.maxScroll) : 0.0f);
|
||||
metrics.thumb = gd::RectF(metrics.track.X, thumbTop, scrollWidth, thumbHeight);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 计算一条派生曲线在给定采样窗口内的取值(同步路径与后台线程共用)。
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user