Files
Just-DataInspector/Components/SignalPanel/Inc/SignalPanelInternal.hpp
T
DeepSeek V4.1 Flash affb9cc340 目录规范化(Inc/Src)+「自由标记」第六轮交互调整
1) 目录规范化:所有 .hpp 只放 Inc,Src 只放 .cpp
   - 4 个 *Internal.hpp 从 Src 迁到 Inc(DataModel/Application/PlotView/SignalPanel)
   - Makefile 固化该约定(注释必须单独成行:写在同一行会把 CXX_INCLUDES_PATHS 整条注释掉)
   - include 全是扁平形式,搬家无需改任何 include

2) 常规模式 = 普通鼠标(未开启「自由标记点选取」)
   - ProbeFreeMarker 早退:不预览、不画纵向光标、单击不固定
   - 补 if (!hitCurve) continue:否则整列光标下 probe.ref 恒有效,任意位置都能固定
   - 固定点仍恒吸附到真实采样时刻;「已吸附」圆环只在稀疏判据成立时画

3) 两个显示开关:按钮改名「自由标记点选取」,其下新增「显示增量 | 显示枚举文本」
   - 默认关闭,只影响已固定点的胶囊文本;预览胶囊恒为「值 = 枚举」
   - 开关行计入面板高度与内容区高度

4) 面板六列:新增恒存在的「序号」列(组内 1 起)
   - 枚举列存在时,无枚举/对不上任何枚举项显示 N/A
   - 列与列之间画竖线;悬停某行时其它固定点封顶 50% 不透明度,移出恢复
   - 删除固定点 ✕ 的悬停浮动提示(PaintFreeMarkerTooltip 实现与声明全部移除)
   - 开启选取模式的网格上,信号悬停提示只显示信号名

5) 缓存键补齐
   - FreeMarkerContents 混入枚举表指纹(旧键会让加上枚举表后仍显示 N/A)
   - PanelSlotKey 混入两个新按钮矩形与开关状态

6) 注释/文档
   - 修正 FreeMarkerProbe、SetLaneFreeMarkerCursor、标记透明度等处的过期说明
   - 空态提示按是否开启选取模式区分文案

验证(全离屏,未占用鼠标/键盘/屏幕)
   - 新增 .preflight/mark5check 52/52
   - 回归:markcheck 67/67、mark4check 40/40、fixcheck 37/37、round3check 31/31、
     round4check 34/34、round5check 5/5、editcheck 20/20、convcheck 18/18、diag 19/19
   - check 136/137(唯一失败项为既有不稳定项,drift=15818 与改动前逐位相同)
   - make release / make debug 零警告;Release 产物 TargetCheck.exe 857,600 字节
2026-10-23 15:50:55 +08:00

336 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; /**< 最长时长。 */
/**
* @brief 分组**名称区域**进入改名编辑所需的「快速双击」时间上限(毫秒)。
*
* 用户 2026-10-17 要求:名称区域单击 = 展开/收起(恢复),**只有快速双击**才进入编辑框;
* “较慢但明显不是双击的点击频率”必须只当两次单击。
* Windows 的双击判定上限通常是 500ms(`GetDoubleClickTime()`),太宽松 ——
* 用户快速连点两次想“展开看一眼再收起”时会误入编辑态。这里收紧到 300ms:
* 真双击(一般 <250ms)照常进入编辑,稍慢的两次单击则各自触发展开/收起。
*/
inline const std::uint64_t kGroupRenameDoubleClickMs = 300;
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__