Files
Just-DataInspector/Components/SignalPanel/Src/SignalPanelInternal.hpp
T
DeepSeek V4.1 Flash 82b5ebe4f9 「所属网格」菜单:标题固定 + 10 行上限 + 滚动条;网格滚动联动改为动态过渡
1) 菜单滚动时最上方的「所属网格」标题不再跟着滚走
   - 根因:上一轮只实现了“尾部固定区”(最后一个分隔线 + 「新建网格」钉在底部),
     而滚动区是从条目 0 开始的,于是标题也被一起滚掉。
   - 修法:BuildLayout 新增 m_scrollFirst(= 前导 HEADER / HEADER_ACTION 的个数),
     滚动区收敛为半开区间 [m_scrollFirst, m_pinnedIndex):
     · 前导标题从菜单顶向下固定排(不参与滚动);
     · 滚动区条目受 m_scrollOffset 影响;
     · 尾部固定区从菜单底向上对齐。
   - 配套:Paint 的裁剪与“跳过不可见条目”范围、ItemAtPoint 的“必须落在滚动视口内”判定、
     EnsureItemVisible 的下界全部改用该区间;新增 ScrollRegion(first, last) 供查询/测试。

2) 菜单长度上限改为「最多 10 条网格」并加滚动条
   - MenuLayoutConstraint 新增 maxScrollRows(滚动区最多显示的标准行数;与 maxHeight
     同时给出时取更严格者);OpenLaneMenu 传 10(kLaneMenuVisibleRows),
     另保留 maxHeight = max(180, 客户区高 × 0.8) 作为矮窗口的硬上限。
   - 新增滚动条:轨道贴滚动条目区右内侧(不盖住标题与底部固定项),滑块高度按视口/内容比
     (下限 22 逻辑像素);滚动区条目宽度让出 kScrollBarWidth + kScrollBarGap,
     文本与对勾不会压在滑块上。
   - 滚动条可拖拽:按下滑块 → 拖动(只改偏移、不更新悬停);点击轨道空白 → 滑块中心移过去;
     松开结束拖拽且不下发命令、菜单保持打开。新增 ScrollOffsetForThumbTop 反算偏移。
   - ⚠️ BuildLayout 绝不能复位 m_scrollBarDragging:拖拽时每帧都会 RebuildLayout(),
     在那里清零会让拖拽“拖一下就脱手”(该状态只在 Open / Close 里复位)。

3) 网格滚动联动改为动态过渡(旧实现直接把视界瞬移过去,过于僵硬)
   - PlotView::ScrollLaneIntoView 只负责启动平滑动画:与信号列表的“滚动到目标信号”同源
     (ease-out cubic;时长 clamp(距离×0.35ms, 130, 300)),新增
     m_laneScrollAnimActive/From/To/StartMs/DurationMs。
   - 宿主 16ms 定时器(kScrollTimerId)调用 AdvanceLaneScrollAnimation() 逐帧推进,
     与信号面板动画共用同一条定时器;Render 的“需要保持定时重绘”判定加入
     LaneScrollAnimationActive()。
   - 每帧 ClampLaneScroll() + BuildLaneVisuals()(网格滚动量参与场景/图层指纹与各网格 rect);
     结束时精确落到目标。已完整可见的网格仍然不启动动画。
   - ⚠️ 用户自己滚动网格(滚轮 / 拖滚动条)会 CancelLaneScrollAnimation(),避免与动画抢滚动位置。

验证(全部离屏执行:窗口 SW_HIDE + 只注入合成消息 + 进程与线程 BELOW_NORMAL,
不移动真实光标、不抢焦点、不截屏):
- .preflight/check 扩到 95 项,95/95 通过;新增断言:
  · 菜单顶部标题在滚动时不动(Y 坐标不变)、滚动视口高度恰为 10 × 行高;
  · 滚动条轨道/滑块存在、随滚动移动、按住滑块拖到底(菜单不关、不下发命令、末条目入视口)、
    滚轮回到顶部;
  · ScrollLaneIntoView 不瞬移(刚触发时几乎未动)、中间帧单调递进、收尾精确到位、
    已可见时不启动动画、滚轮/手动滚动会取消动画。
- A/B:回退三处(标题固定 / 10 行上限 / 滚动动画)⇒ 10 项如实失败;恢复后 95/95。
- make release / make debug 零警告。

文档:Documents/agent_mem.md 新增 §9.39、§11.2 第 104–107 条与变更日志一行。
2026-10-16 14:12:42 +08:00

325 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#ifndef __SIGNALPANEL_INTERNAL_HPP__
#define __SIGNALPANEL_INTERNAL_HPP__
// SignalPanel 组件内部共享的常量与工具函数(私有头,仅本组件的 .cpp 使用)。
//
// 本组件的实现按功能拆成多个协作文件,它们共用这一份内部工具集。
//
// ⚠️ 为让多个翻译单元都能使用,原匿名命名空间改为**具名**命名空间 `spdetail`,
// 并把其中每个函数与常量标为 `inline` —— 单一定义、单一副本,不会产生代码重复。
// 各实现文件在包含本头之后用 `using namespace spdetail;` 引入这些工具,
// 因此函数体里原有的非限定调用一行都不用改。
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <utility>
#include <vector>
#include "SignalPanel.hpp"
#include "UiControls.hpp"
#include "Utf8Util.hpp"
namespace jdi
{
namespace spdetail
{
/**
* @brief SignalPanel 内部工具集(常量与自由函数)。
*
* 全部符号都是 `inline` ⇒ 多个翻译单元共用同一个定义:既不重复符号,也不产生多份代码。
* 不要把它们移回匿名命名空间(那样每个 .cpp 都会各自复制一份)。
*/
inline const float kHeaderHeight = 38.0f; /**< 标题区高度(逻辑像素)。 */
inline const float kGroupRowHeight = 30.0f; /**< 分组行高。 */
inline const float kSignalRowHeight = 27.0f; /**< 信号行高。 */
inline const float kPadding = 12.0f; /**< 通用内边距。 */
inline const float kCheckSize = 15.0f; /**< 复选框尺寸。 */
inline const float kSwatchSize = 12.0f; /**< 信号行颜色小方块尺寸。 */
inline const float kBadgeWidth = 30.0f; /**< 网格徽标宽度。 */
inline const float kBadgeHeight = 17.0f; /**< 网格徽标高度。 */
inline const float kCorner = 4.0f; /**< 通用圆角半径。 */
inline const float kDisclosureWidth = 16.0f; /**< 折叠三角占位宽度。 */
inline const float kIndent = 14.0f; /**< 信号行相对分组行的缩进。 */
inline const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */
inline const float kCloseButtonSize = 18.0f; /**< 行尾关闭(✕)按钮尺寸(分组行与信号行共用)。 */
inline const float kGroupSeparatorGap = 7.0f; /**< 分组标签与分隔线/名称的间距。 */
inline const float kGroupSeparatorWidth = 1.0f; /**< 分组标签右侧分隔线宽度。 */
inline const float kFilterHeight = 26.0f; /**< 「筛选信号」输入框高度。 */
inline const float kFilterPadV = 5.0f; /**< 筛选框上下留白。 */
inline const float kFilterTextPad = 4.0f; /**< 筛选框内文字/输入框相对外框的内缩(四边一致)。 */
inline const float kRowTailGap = 6.0f; /**< 网格徽标与行尾 ✕ 之间的间距。 */
/**
* @brief 「所属网格」菜单里可滚动的网格条目**最多显示的行数**(用户 2026-10-16 要求)。
*
* 信号很多时(例如把 MF4 的上百条信号各自放进独立网格)该菜单会有上百个条目;
* 条目太多既超出屏幕、又让人难以选择,因此只显示 10 行,其余用滚动条 / 滚轮翻动。
*/
inline const int kLaneMenuVisibleRows = 10;
/**
* @brief 大小写无关的通配符匹配(`*` 匹配任意长度的任意字符序列)。
* @param [IN] text 待匹配文本。
* @param [IN] pattern 模式(可含多个 `*`)。
* @return 匹配返回 true。
*/
inline bool WildcardMatchNoCase(const std::wstring& text, const std::wstring& pattern)
{
const auto fold = [](wchar_t character) -> wchar_t
{
// 仅对基本拉丁字母做大小写折叠(中文等其它字符原样比较)。
if (character >= L'A' && character <= L'Z')
{
return static_cast<wchar_t>(character - L'A' + L'a');
}
return character;
};
// 经典双指针回溯:`star` 记录最近一个 `*` 的位置,`resume` 记录当时尝试的文本位置。
std::size_t t = 0;
std::size_t p = 0;
std::size_t star = std::wstring::npos;
std::size_t resume = 0;
while (t < text.size())
{
if (p < pattern.size() && pattern[p] == L'*')
{
star = p;
resume = t;
++p;
}
else if (p < pattern.size() && fold(pattern[p]) == fold(text[t]))
{
++p;
++t;
}
else if (star != std::wstring::npos)
{
p = star + 1;
++resume;
t = resume;
}
else
{
return false;
}
}
// 文本已耗尽:模式剩余部分必须全是 `*`。
while (p < pattern.size() && pattern[p] == L'*')
{
++p;
}
return p == pattern.size();
}
// 样式编辑区:固定五行网格(颜色 / 样式 / 调整 / 线宽 / 透明度),
// 行高、间距与内容列宽度统一,保证面板宽度变化时各行控件始终对齐,且不会被无限制拉伸。
inline const float kStylePadH = 12.0f; /**< 样式区左右内边距。 */
inline const float kStylePadV = 12.0f; /**< 样式区上下内边距。 */
inline const float kStyleRowHeight = 26.0f; /**< 样式区单行高度。 */
inline const float kStyleRowGap = 10.0f; /**< 样式区行间距。 */
inline const float kStyleLabelWidth = 44.0f; /**< 样式行标签列宽度。 */
inline const float kStyleLabelGap = 8.0f; /**< 标签列与内容列间距。 */
inline const float kStyleRowCount = 5.0f; /**< 样式区固定行数(不含顶部「目标」行)。 */
inline const float kStyleTargetRowHeight = 26.0f; /**< 样式区顶部「目标」行高度(与其它行一致)。 */
/**
* @brief GX 网格标签与信号名之间的间距(逻辑像素)。
*
* 用户要求:目标信号已绘制到网格时,在信号名**后方**显示它所在的 GX 标签。
*/
inline const float kTargetBadgeGap = 8.0f;
/**
* @brief 平滑滚动的动画时长(毫秒)。
*
* 距离越远时长越长(但不会无限增长),靠近时由 ease-out 自动变慢 —— 用户要求
* “越远时越快、靠近时变慢”且“整个动画过程不要太久”。
*/
inline const float kScrollAnimMinMs = 130.0f; /**< 最短时长(短距离)。 */
inline const float kScrollAnimMaxMs = 300.0f; /**< 最长时长(长距离)。 */
inline const float kScrollAnimMsPerPixel = 0.35f; /**< 每像素距离增加的时长。 */
// ---- 分组折起/展开动画(用户 2026-10-16 要求)----
// 整组信号作为一个**刚性整体**向上平移(收起)/ 向下平移(展开),
// 越过组名下边界的部分被裁掉 —— 即“抽拉卷尺”观感:元素相对位置不变。
// 总时长 = clamp(基础 + 行数×每行, 下限, 上限),保证“整个动画过程不会太久”。
inline const float kGroupAnimBaseMs = 115.0f; /**< 动画基础时长。 */
inline const float kGroupAnimPerRowMs = 6.0f; /**< 每多一行增加的时长。 */
inline const float kGroupAnimMinMs = 150.0f; /**< 最短时长。 */
inline const float kGroupAnimMaxMs = 320.0f; /**< 最长时长。 */
inline const float kStyleContentMax = 260.0f; /**< 内容列最大宽度(再拉宽面板也不再延伸)。 */
inline const float kSwatchEdge = 18.0f; /**< 颜色色块边长上限(含自定义颜色入口)。 */
inline const float kSwatchMin = 11.0f; /**< 颜色色块边长下限。 */
inline const float kSwatchGap = 4.0f; /**< 颜色色块最小间距。 */
inline const float kSwatchMaxGap = 10.0f; /**< 颜色色块最大间距(用于铺满内容列)。 */
inline const float kSliderValueWidth = 52.0f; /**< 滑块行数值文本宽度(透明度与线宽一致)。 */
inline const float kSliderValueGap = 8.0f; /**< 滑块轨道与数值文本间距。 */
inline const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */
inline const float kAdjustGap = 6.0f; /**< 「调整」行三个按钮之间的间距。 */
inline const float kAdjustMenuGap = 4.0f; /**< 「系数/偏移」「镜像」二级面板与按钮左上角的额外间距。 */
/** 「调整」行的按钮下标(与 `m_adjustRects` 同序)。 */
inline const int kAdjustGain = 0; /**< 系数(点击展开系数二级面板)。 */
inline const int kAdjustMirror = 1; /**< 镜像(点击展开镜像二级面板)。 */
inline const int kAdjustAbsolute = 2; /**< 绝对值。 */
inline const int kAdjustCount = 3; /**< 按钮数量。 */
// 线宽范围与「稀疏信号默认线宽」共用 `DataModel.hpp` 的常量,避免两处漂移
//(稀疏信号初始化时用的最大线宽必须与这里滑块的上限完全一致)。
inline const float kWidthMin = kSignalLineWidthMin; /**< 允许的最小线宽(px)。 */
inline const float kWidthMax = kSignalLineWidthMax; /**< 允许的最大线宽(px)。 */
/** @brief 系数调整允许的绝对值上限(防止输入极端值把波形完全挤出可视区)。 */
inline const double kGainLimit = 10000.0;
/** @brief 系数为默认值 1(不做任何缩放)时按钮显示为未启用样式。 */
inline bool GainIsDefault(float gain)
{
return std::fabs(gain - 1.0f) < 1.0e-6f;
}
/** @brief 可选绘制样式顺序(不含阶梯方式)。 */
inline const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER};
inline const int kModeOptionCount = static_cast<int>(sizeof(kModeOptions) / sizeof(kModeOptions[0]));
/**
* @brief 计算滑块的便捷命中矩形(比轨道略大,便于拖拽)。
* @param [IN] track 轨道矩形。
* @param [IN] scale 缩放比例。
* @return 命中矩形。
*/
inline gd::RectF SliderHitRect(const gd::RectF& track, float scale)
{
return gd::RectF(track.X - 6.0f * scale, track.Y - 9.0f * scale, track.Width + 12.0f * scale,
track.Height + 18.0f * scale);
}
/**
* @brief 判断颜色是否偏暗(用于在色块之上选择可读的图标颜色)。
* @param [IN] color 0xAARRGGBB 颜色值。
* @return 相对亮度低于阈值返回 true。
*/
inline bool IsDarkColor(std::uint32_t color)
{
const double r = static_cast<double>((color >> 16) & 0xFFu);
const double g = static_cast<double>((color >> 8) & 0xFFu);
const double b = static_cast<double>(color & 0xFFu);
return (0.299 * r + 0.587 * g + 0.114 * b) < 140.0;
}
/**
* @brief 将数值限制到合法区间。
* @param [IN] value 输入值。
* @param [IN] low 下限。
* @param [IN] high 上限。
* @return 限制后的数值。
*/
inline float ClampFloat(float value, float low, float high)
{
return (value < low) ? low : ((value > high) ? high : value);
}
/**
* @brief 数值文本:**至少两位小数**,精度更高时按实际精度显示。
*
* 例:`1` → `1.00`、`2.5` → `2.50`、`0.125` → `0.125`、`1.2345678` → `1.2345678`。
* 实现:从 2 位小数开始递增位数,直到文本能精确表示该数值
*(容差 1e-7 相对量,与 float 精度匹配)。
*
* @param [IN] value 数值。
* @return 显示文本。
*/
inline std::wstring FormatNumberText(double value)
{
const double tolerance = std::max(1.0e-12, std::fabs(value) * 1.0e-7);
for (int decimals = 2; decimals <= 9; ++decimals)
{
const std::wstring text = jdi::FormatNumber(value, decimals);
const double parsed = std::wcstod(text.c_str(), nullptr);
if (std::fabs(parsed - value) <= tolerance)
{
return text;
}
}
return jdi::FormatNumber(value, 9);
}
/**
* @brief 带正负符号的数值文本(非编辑态显示偏移量时使用)。
*
* 规则(用户明确要求):**正数带 `+`、负数带 `-`、0 不带任何符号**。
* (旧实现 0 也会显示 `+0.00`,用户报告“偏移调整的 Y 轴输入框默认值为 0 时也显示了正号,这不正确”。)
* @param [IN] value 数值。
* @return 形如 `+1.50` / `-0.25` / `0.00` 的文本。
*/
inline std::wstring FormatSignedNumber(double value)
{
const std::wstring text = FormatNumberText(std::fabs(value));
if (value > 0.0)
{
return L"+" + text;
}
if (value < 0.0)
{
return L"-" + text;
}
return text; // 0:不带符号
}
/**
* @brief 把系数格式化为界面文本(与偏移量同一套两位小数规则 + 显式正号)。
*
* 规则(用户明确要求):“系数调整”输入框**非编辑态**显示时,非 0 正值前面加 `+`,
* 负值按原样显示负号,0 不带符号。编辑态仍用不带符号的纯数值(见 `FormatNumberText`)。
* @param [IN] gain 系数。
* @return 显示文本。
*/
inline std::wstring FormatGainText(float gain)
{
const double value = static_cast<double>(gain);
if (value > 0.0)
{
return L"+" + FormatNumberText(value);
}
return FormatNumberText(value);
}
/**
* @brief 「时间轴」偏移的非编辑态显示文本(`Δt:` + 带正负号的复合单位间隔)。
*
* 规则(用户明确要求):在 `Δt:` 冒号后的时间显示值**前面加上正负号**,
* 只有值为 0 时不显示符号。示例:`Δt:+1h 1min 2s` / `Δt:-5s 60ms` / `Δt:0s`。
*(`FormatDurationCompound` 内部取绝对值,因此符号由这里补上。)
* @param [IN] seconds 时间偏移(秒,可为负)。
* @return 显示文本。
*/
inline std::wstring FormatOffsetTimeText(double seconds)
{
if (seconds > 0.0)
{
return L"Δt:+" + FormatDurationCompound(seconds);
}
if (seconds < 0.0)
{
return L"Δt:-" + FormatDurationCompound(seconds);
}
return L"Δt:" + FormatDurationCompound(0.0);
}
} // namespace spdetail
using namespace spdetail;
} // namespace jdi
#endif // __SIGNALPANEL_INTERNAL_HPP__