按组件目录结构把四个巨型实现文件拆分为多个协作文件(PlotView / SignalPanel / Application / DataModel)
承接上一条提交(0871eb0)—— 该提交只完成了 Components/Shared 与注释/命名清理,并**主动未做**
大文件拆分;本提交把大文件拆分补齐,至此“按功能拆分逻辑块到各组件自己的 Inc/Src”已落地。
## 一、拆分结果
| 原文件 | 拆分后 |
| --- | --- |
| Components/PlotView/Src/PlotView.cpp(13 695 行 / 544 KB) | PlotView.cpp(787) + PlotViewPaint.cpp(6 367) + PlotViewInteraction.cpp(2 715) + PlotViewFeatures.cpp(3 005) + PlotViewInternal.hpp(936) |
| Components/SignalPanel/Src/SignalPanel.cpp(4 184 行) | SignalPanel.cpp(1 141) + SignalPanelPaint.cpp(697) + SignalPanelInteraction.cpp(2 095) + SignalPanelInternal.hpp |
| Components/Application/Src/Application.cpp(3 135 行) | Application.cpp(1 427) + ApplicationCommands.cpp(1 184) + ApplicationLoading.cpp(270) + ApplicationInternal.hpp |
| Utilities/Src/DataModel.cpp(2 615 行) | DataModel.cpp(1 583,含自由函数区) + DataModelTab.cpp(732) + DataModelInternal.hpp |
分片职责(以 PlotView 为例):
core 视图核心:构造/析构、上下文、布局、坐标映射、视界与平移、网格结构与归属
paint 渲染与像素缓存:全部绘制、场景/装饰缓存、每网格图层、宽基底与滚动补条
interaction 交互与编辑:命中测试、鼠标/键盘、光标形状、内联输入框、浮动面板层序
features 视图特性与派生数据:光标读数/提示/缩放、自适应、吸附、单点、派生曲线线程池、
边沿统计、实验性功能参数
(SignalPanel 为 core/paint/interaction;Application 为 core/commands/loading;
DataModel 为 core/tab。)
## 二、关键手法(这是能安全拆开的前提)
原文件的辅助区都是**匿名命名空间**(PlotView 约 870 行常量与工具函数),被所有成员函数广泛引用。
直接拆文件会让各 .cpp 拿不到这些工具,因此:
1. 把该块整块搬进 `<组件>Internal.hpp`,从 `namespace { }` 改为**具名**命名空间
(pvdetail / spdetail / appdetail / dmdetail);
2. 块内**每个函数与常量都标 inline** ⇒ 多个翻译单元共用同一个定义:既不重复符号,
也不产生 N 份函数定义;
3. 头部末尾 `using namespace <ns>;` ⇒ 各 .cpp 函数体里原有的非限定调用(IntersectRect /
NiceStep / ClampDouble …)**一行都不用改**;
4. 成员定义按「方法名 → 分片」归类后**整块搬移** —— 全程**纯代码搬移**,没有改动任何一行函数体。
## 三、拆分过程中踩到并修正的四个坑(已固化到 .preflight/perf/split_component.ps1)
1. `[[maybe_unused]]` 等属性**必须排在 inline 之前**(`inline [[x]] T` 非法);
单独的 `{`/`}` 行、注释行、`struct`/`enum`/`using`/`template` 行不能加 inline。
2. 匿名命名空间的**结束行可能带注释**(`} // namespace`),只认 `^\t}$` 会一路匹配到后面
某个成员函数的结束括号,把大段成员定义误当成“辅助区”搬走。
3. **首个成员定义之前的所有行**必须跟着进核心文件:DataModel.cpp 在匿名命名空间之后、
首个成员定义之前还有约 390 行命名空间外的自由函数(SignalStats / ComputeStats /
CacheSparseSamples / ApplySinglePointDefaultStyle …),漏掉会被静默丢弃。
4. 内部头必须带上原文件真正用到的全部头:DataModel 需要 Theme.hpp / <cstring> / <cstdio> /
<deque> / <atomic> / <cwctype> / <new>(原文件靠这些头才编译得过)。
## 四、附带完成的命名审计
PlotView 中残留的 File 语义命名一并修正:LaneSpansMultipleFiles → LaneSpansMultipleGroups、
firstFile → firstGroup、multiFile → multiGroup、SceneCacheKey::haloFile → haloGroup。
## 五、验证(全部离屏:SW_HIDE + 不注入真实输入 + 进程/线程 BELOW_NORMAL)
- .preflight/perf/verify.exe(PlotView 渲染/交互,含逐像素断言)49/49
- .preflight/perf/panelanim.exe(信号面板:卷尺式折起/展开、筛选态、悬浮组名、目标行)156/156
- .preflight/perf/datacheck.exe(数据层:单轴/多时间轴 CSV、MF4、重命名语义)69/69
- .preflight/perf/edgecheck.exe(边沿统计与面板排版)46/46
- make release / make debug 零警告。
即:拆分后**行为与拆分前完全一致**(纯代码搬移,四套探针全绿)。
## 六、体积(诚实记录)
Target.exe: 754 176 → 764 416 B(+10 240 B,+1.4%)
因为 inline 工具函数会在每个引用它的翻译单元里各生成一份(PE/COFF 链接器没有 ICF 合并)。
这再次印证:拆分解决的是**可维护性**,不是体积(本轮的 −24.6% 全部来自 ICO 256×256 条目改 PNG)。
## 七、文档
Documents/agent_mem.md:§9.37 新增“七、多文件拆分(已完成)”与“八、仍未完成”(只剩
Lane → LanePanelState、模态提示解耦、MF4 压缩块三项);§11.2 新增第 91、92 条(多文件化的
唯一安全手法 + 体积代价)。Documents/refactor_report_2026-10-16.md 同步更新 §五/§七/§八。
This commit is contained in:
1 parent
0871eb0cba
commit
e6b7bf66ec
19 files changed
+19130
-18771
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,316 @@
|
||||
#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 大小写无关的通配符匹配(`*` 匹配任意长度的任意字符序列)。
|
||||
* @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__
|
||||
|
||||
@@ -0,0 +1,697 @@
|
||||
// SignalPanel 实现分片 —— 本文件只包含 SignalPanel 的**成员函数定义**(纯代码搬移,行为与拆分前完全一致)。
|
||||
//
|
||||
// 共用的常量与自由函数在 SignalPanelInternal.hpp(命名空间 `spdetail`,全部 inline)。
|
||||
// 分片按“职责”划分:核心(状态/布局/几何)、渲染、交互与编辑,见各文件名。
|
||||
#include "SignalPanelInternal.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
void SignalPanel::PaintStickyGroupRow(gd::Graphics& g, int row)
|
||||
{
|
||||
if (row < 0 || row >= static_cast<int>(m_rows.size()) ||
|
||||
m_rows[static_cast<std::size_t>(row)].kind != RowKind::GROUP)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int rowCount = static_cast<int>(m_rows.size());
|
||||
const float height = m_rows[static_cast<std::size_t>(row)].height;
|
||||
const float naturalY = RowRect(row).Y;
|
||||
const float stickyY = StickyGroupRowTop(row, rowCount);
|
||||
if (stickyY + height <= m_listRect.Y + 0.5f)
|
||||
{
|
||||
return; // 已经整块滑出列表上边界
|
||||
}
|
||||
|
||||
m_rowYOverrideActive = true;
|
||||
m_rowYOverrideY = stickyY;
|
||||
// 悬浮行必须是不透明的,否则下面的信号行会与组名叠在一起看不清;
|
||||
// `FillRowRect` 会把矩形夹到列表矩形(GDI 填充不受 GDI+ 裁剪约束)。
|
||||
SetRowClip(gd::RectF(m_listRect.X, stickyY, m_listRect.Width, height));
|
||||
FillRowRect(g, gd::RectF(m_listRect.X, stickyY, m_listRect.Width, height), m_theme->panelBg);
|
||||
PaintGroupRow(g, row);
|
||||
ClearRowClip();
|
||||
// 只有真正“浮”在其它行之上时才补一条底部边界线(否则会与正常绘制有差异)。
|
||||
if (stickyY > naturalY + 0.5f)
|
||||
{
|
||||
UiDrawLine(g, m_listRect.X, stickyY + height - 0.5f, m_listRect.X + m_listRect.Width,
|
||||
stickyY + height - 0.5f, m_theme->border, 1.0f);
|
||||
}
|
||||
m_rowYOverrideActive = false;
|
||||
}
|
||||
|
||||
void SignalPanel::Paint(gd::Graphics& g)
|
||||
{
|
||||
if (m_theme == nullptr || m_fonts == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureRows();
|
||||
UiFillRect(g, m_area, m_theme->panelBg);
|
||||
|
||||
// 标题区。
|
||||
{
|
||||
std::wstring title = L"信号";
|
||||
if (m_dataSet != nullptr && m_tab != nullptr)
|
||||
{
|
||||
title += L" · " + std::to_wstring(m_tab->VisibleCount()) + L"/" +
|
||||
std::to_wstring(TotalSignalCount()) + L" 已选";
|
||||
}
|
||||
|
||||
gd::RectF titleRect(m_headerRect.X + kPadding * m_scale, m_headerRect.Y,
|
||||
std::max(20.0f, m_headerRect.Width - kPadding * 2.0f * m_scale -
|
||||
m_toggleAllRect.Width - 8.0f * m_scale),
|
||||
m_headerRect.Height);
|
||||
UiText(g, UiEllipsize(title, m_fonts->uiBold, titleRect.Width), m_fonts->uiBold, titleRect,
|
||||
m_theme->textStrong, TextAlign::LEFT);
|
||||
|
||||
const bool allVisible = AreAllSignalsVisible();
|
||||
const std::wstring toggleLabel = allVisible ? L"全部隐藏" : L"全部显示";
|
||||
const bool toggleHover = (m_hoverStyle == StyleHit::TOGGLE_ALL);
|
||||
if (toggleHover)
|
||||
{
|
||||
UiFillRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->hover);
|
||||
}
|
||||
UiDrawRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->border, 1.0f);
|
||||
const bool enabled = (m_dataSet != nullptr && !m_dataSet->Empty());
|
||||
UiText(g, toggleLabel, m_fonts->uiSmall, m_toggleAllRect,
|
||||
enabled ? m_theme->text : m_theme->textMuted, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
// 「筛选信号」输入框:未输入内容时显示占位提示,单击进入编辑,输入实时过滤下方列表。
|
||||
{
|
||||
UiFillRoundRect(g, m_filterRect, kCorner * m_scale, m_theme->cardBg);
|
||||
UiDrawRoundRect(g, m_filterRect, kCorner * m_scale,
|
||||
m_filterEditing ? m_theme->accent : m_theme->border,
|
||||
m_filterEditing ? 1.6f : 1.0f);
|
||||
|
||||
const float pad = kFilterTextPad * m_scale;
|
||||
const gd::RectF clearRect = FilterClearRect();
|
||||
// 有输入内容时在右侧留出 ✕ 的位置,文本不会压到按钮上。
|
||||
const float textRight = (clearRect.Width > 0.0f) ? clearRect.X : (m_filterRect.X + m_filterRect.Width);
|
||||
const float textWidth = std::max(8.0f, textRight - pad - (m_filterRect.X + pad));
|
||||
const gd::RectF textRect(m_filterRect.X + pad, m_filterRect.Y, textWidth,
|
||||
m_filterRect.Height);
|
||||
|
||||
if (m_filterEditing)
|
||||
{
|
||||
// 外框(m_filterRect)已经画好了底板与强调色边框,这里只叠加文本内容。
|
||||
// 若改用 UiDrawEditField,会在外框内部再套一个带边框的圆角框,
|
||||
// 空内容时只剩一圈内框十分突兀(用户明确要求去掉这层"内圈")。
|
||||
gd::RectF fieldRect = FilterFieldRect();
|
||||
fieldRect.Width = std::max(8.0f, textWidth + pad);
|
||||
UiDrawEditFieldContent(g, fieldRect, m_filterEdit, m_fonts->uiSmall, m_scale);
|
||||
}
|
||||
else if (m_filterText.empty())
|
||||
{
|
||||
UiText(g, L"筛选信号", m_fonts->uiSmall, textRect, m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiText(g, UiEllipsize(m_filterText, m_fonts->uiSmall, textWidth), m_fonts->uiSmall,
|
||||
textRect, m_theme->text, TextAlign::LEFT);
|
||||
}
|
||||
|
||||
// 清空按钮 ✕:仅在筛选框有内容时出现,悬停高亮(鼠标光标保持箭头)。
|
||||
if (clearRect.Width > 0.0f)
|
||||
{
|
||||
const bool clearHot = (m_hoverStyle == StyleHit::FILTER_CLEAR);
|
||||
if (clearHot)
|
||||
{
|
||||
UiFillRoundRect(g, clearRect, 3.0f * m_scale, WithAlpha(m_theme->danger, 0x33));
|
||||
}
|
||||
const std::uint32_t crossColor = clearHot ? m_theme->danger : m_theme->textMuted;
|
||||
const float crossPad = clearRect.Width * 0.28f;
|
||||
UiDrawLine(g, clearRect.X + crossPad, clearRect.Y + crossPad,
|
||||
clearRect.X + clearRect.Width - crossPad,
|
||||
clearRect.Y + clearRect.Height - crossPad, crossColor, 1.5f * m_scale);
|
||||
UiDrawLine(g, clearRect.X + clearRect.Width - crossPad, clearRect.Y + crossPad,
|
||||
clearRect.X + crossPad, clearRect.Y + clearRect.Height - crossPad, crossColor,
|
||||
1.5f * m_scale);
|
||||
}
|
||||
}
|
||||
|
||||
// 列表区。
|
||||
{
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(gd::RectF(m_listRect.X, m_listRect.Y, m_listRect.Width, m_listRect.Height));
|
||||
|
||||
if (m_dataSet == nullptr || m_dataSet->Empty())
|
||||
{
|
||||
UiText(g, L"尚未加载数据,请打开数据文件", m_fonts->uiSmall, m_listRect, m_theme->textMuted,
|
||||
TextAlign::CENTER);
|
||||
}
|
||||
else if (m_rows.empty() && !m_filterText.empty())
|
||||
{
|
||||
// 筛选结果为空:明确提示"无匹配",而不是让人误以为数据丢失。
|
||||
UiText(g, L"无匹配的筛选信号", m_fonts->uiSmall, m_listRect, m_theme->textMuted,
|
||||
TextAlign::CENTER);
|
||||
}
|
||||
else
|
||||
{
|
||||
const int rowCount = static_cast<int>(m_rows.size());
|
||||
// 先定出要「悬浮」的分组行(若有):常规循环跳过它,最后统一钉在列表顶部绘制,
|
||||
// 否则它会先被画一次在滚动位置上、又被画一次在顶部(视觉上重叠两份)。
|
||||
const int stickyRow = StickyGroupRowIndex(rowCount);
|
||||
for (int row = 0; row < rowCount; ++row)
|
||||
{
|
||||
if (row == stickyRow)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const gd::RectF rect = RowRect(row);
|
||||
if (rect.Y + rect.Height < m_listRect.Y || rect.Y > m_listRect.Y + m_listRect.Height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 折起/展开动画:信号行整组向上平移,越过组名下边界的部分要“消失(藏进组名栏)”。
|
||||
// 已经完全越过的行整行跳过(省一次绘制),部分越过的行把裁剪上边界抬到组名下边界;
|
||||
// ⚠️ 此时还必须把 **GDI+ 裁剪**也收紧到同一条线 —— 行内的色块 / 复选框 / 网格徽标
|
||||
// 走的是 GDI+ 绘制(只受 `SetClip` 约束),不收紧就会画到组名栏上面去。
|
||||
const int rowSave = g.Save();
|
||||
SetRowClip(rect);
|
||||
const float hideTop = AnimationHideTopForRow(row, stickyRow);
|
||||
if (hideTop > rect.Y + 0.5f)
|
||||
{
|
||||
if (rect.Y + rect.Height <= hideTop + 0.5f)
|
||||
{
|
||||
g.Restore(rowSave);
|
||||
ClearRowClip();
|
||||
continue;
|
||||
}
|
||||
ClampRowClipTop(hideTop);
|
||||
g.SetClip(m_rowClipRect, gd::CombineModeIntersect);
|
||||
}
|
||||
|
||||
if (m_rows[static_cast<std::size_t>(row)].kind == RowKind::GROUP)
|
||||
{
|
||||
PaintGroupRow(g, row);
|
||||
}
|
||||
else
|
||||
{
|
||||
PaintSignalRow(g, row);
|
||||
}
|
||||
g.Restore(rowSave);
|
||||
ClearRowClip();
|
||||
}
|
||||
|
||||
PaintStickyGroupRow(g, stickyRow);
|
||||
}
|
||||
|
||||
g.Restore(saveState);
|
||||
}
|
||||
|
||||
PaintScrollBar(g);
|
||||
PaintStyleSection(g);
|
||||
|
||||
// 分隔线。
|
||||
UiDrawLine(g, m_area.X + m_area.Width - 1.0f, m_area.Y, m_area.X + m_area.Width - 1.0f,
|
||||
m_area.Y + m_area.Height, m_theme->border, 1.0f);
|
||||
UiDrawLine(g, m_headerRect.X, m_headerRect.Y + m_headerRect.Height - 1.0f,
|
||||
m_headerRect.X + m_headerRect.Width, m_headerRect.Y + m_headerRect.Height - 1.0f,
|
||||
m_theme->border, 1.0f);
|
||||
if (m_styleRect.Height > 0.0f)
|
||||
{
|
||||
UiDrawLine(g, m_styleRect.X, m_styleRect.Y, m_styleRect.X + m_styleRect.Width, m_styleRect.Y,
|
||||
m_theme->border, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalPanel::PaintRowText(gd::Graphics& g, const std::wstring& text, HFONT font,
|
||||
const gd::RectF& rect, std::uint32_t argb, TextAlign align)
|
||||
{
|
||||
// 行内文本必须显式裁剪:GDI 文本不遵循 GDI+ 的 `SetClip`,
|
||||
// 否则列表滚动时顶部那些“部分可见”的行会把组名/信号名画到筛选框上。
|
||||
// 另外,分组折起动画期间行高会连续缩小 —— 此时必须裁到**本行矩形**内,
|
||||
// 否则垂直居中的文字会溢出到相邻行上。
|
||||
UiTextClipped(g, text, font, rect, argb, align, m_rowClipActive ? m_rowClipRect : m_listRect);
|
||||
}
|
||||
|
||||
void SignalPanel::FillRowRect(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb)
|
||||
{
|
||||
// 夹取而不是裁剪:纯色填充下两者等价,而夹取对 `UiFillRect` 的 GDI 快路径同样有效
|
||||
//(大面积不透明矩形会改走 GDI `FillRect`,它同样不受 GDI+ 裁剪约束)。
|
||||
gd::RectF clipped = rect;
|
||||
clipped.Intersect(m_rowClipActive ? m_rowClipRect : m_listRect);
|
||||
if (clipped.Width <= 0.0f || clipped.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UiFillRect(g, clipped, argb);
|
||||
}
|
||||
|
||||
void SignalPanel::PaintGroupRow(gd::Graphics& g, int row)
|
||||
{
|
||||
if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
const SignalGroup* group = m_dataSet->GroupAt(item.groupIndex);
|
||||
if (group == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const gd::RectF rect = RowRect(row);
|
||||
const bool hovered = (m_hoverRow == row);
|
||||
if (hovered)
|
||||
{
|
||||
FillRowRect(g, rect, m_theme->hover);
|
||||
}
|
||||
FillRowRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height),
|
||||
WithAlpha(m_theme->borderStrong, 0x90));
|
||||
|
||||
const float baseX = rect.X + kPadding * m_scale;
|
||||
const float centerY = rect.Y + rect.Height * 0.5f;
|
||||
const bool collapsed = IsGroupCollapsed(item.groupIndex);
|
||||
|
||||
// 折叠三角。
|
||||
{
|
||||
const float cx = baseX + kDisclosureWidth * m_scale * 0.5f;
|
||||
const std::uint32_t color =
|
||||
(hovered && m_hoverPart == RowPart::DISCLOSURE) ? m_theme->accent : m_theme->text;
|
||||
UiDrawTriangleIcon(g, gd::RectF(cx - 6.0f * m_scale, centerY - 6.0f * m_scale, 12.0f * m_scale,
|
||||
12.0f * m_scale),
|
||||
color, collapsed ? IconDirection::RIGHT : IconDirection::DOWN);
|
||||
}
|
||||
|
||||
// 分组复选框(三态:全选 / 部分 / 未选)。
|
||||
const gd::RectF checkRect(baseX + kDisclosureWidth * m_scale, centerY - kCheckSize * m_scale * 0.5f,
|
||||
kCheckSize * m_scale, kCheckSize * m_scale);
|
||||
const bool allVisible = m_tab->GroupAllVisible(item.groupIndex);
|
||||
const bool anyVisible = m_tab->GroupHasVisible(item.groupIndex);
|
||||
UiDrawCheckBox(g, checkRect, allVisible ? 1 : (anyVisible ? 2 : 0), 3.0f * m_scale);
|
||||
|
||||
const gd::RectF closeRect = GroupCloseRect(row);
|
||||
|
||||
// 分组标题:左侧为“组X”标签,右侧以绘制的竖线分隔分组名(不使用字符),再显示可重命名的名称。
|
||||
const std::wstring groupLabel = GroupLabelText(item.groupIndex);
|
||||
// 项数文本与 `RowNameRect`(组名编辑框)共用同一来源,保证名称宽度与绘制不漂移。
|
||||
const std::wstring stats = GroupStatsText(row);
|
||||
const float groupWidth = GroupLabelWidth(row);
|
||||
const float statsWidth = UiMeasureTextWidth(m_fonts->uiSmall, stats);
|
||||
const float statsRight = closeRect.X - 6.0f * m_scale;
|
||||
const float nameLeft = checkRect.X + checkRect.Width + 8.0f * m_scale;
|
||||
const float nameAreaLeft =
|
||||
nameLeft + groupWidth + kGroupSeparatorGap * 2.0f * m_scale + kGroupSeparatorWidth * m_scale;
|
||||
const float nameWidth =
|
||||
std::max(20.0f, statsRight - statsWidth - 6.0f * m_scale - nameAreaLeft);
|
||||
|
||||
// 序号以强调色突出,便于在多组之间快速定位。
|
||||
PaintRowText(g, groupLabel, m_fonts->uiBold, gd::RectF(nameLeft, rect.Y, groupWidth, rect.Height),
|
||||
m_theme->accent, TextAlign::LEFT);
|
||||
|
||||
// 竖线分隔符:以绘制方式实现,与文字基线无关,排版更稳。
|
||||
FillRowRect(g, gd::RectF(nameLeft + groupWidth + kGroupSeparatorGap * m_scale,
|
||||
rect.Y + 7.0f * m_scale, kGroupSeparatorWidth * m_scale,
|
||||
rect.Height - 14.0f * m_scale),
|
||||
WithAlpha(m_theme->borderStrong, 0xB0));
|
||||
|
||||
if (m_editField == StyleEditField::GROUP_TITLE && m_editRow == row)
|
||||
{
|
||||
UiDrawEditField(g, RowNameRect(row), m_edit, m_fonts->uiSmall, m_scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
PaintRowText(g, UiEllipsize(group->name, m_fonts->uiBold, nameWidth), m_fonts->uiBold,
|
||||
gd::RectF(nameAreaLeft, rect.Y, nameWidth, rect.Height), m_theme->textStrong,
|
||||
TextAlign::LEFT);
|
||||
}
|
||||
|
||||
PaintRowText(g, stats, m_fonts->uiSmall, gd::RectF(statsRight - statsWidth, rect.Y, statsWidth, rect.Height),
|
||||
m_theme->textMuted, TextAlign::RIGHT);
|
||||
|
||||
// 关闭按钮(从工作区移除该分组)。
|
||||
const bool closeHot = hovered && (m_hoverPart == RowPart::CLOSE);
|
||||
if (closeHot)
|
||||
{
|
||||
UiFillRoundRect(g, closeRect, 3.0f * m_scale, m_theme->pressed);
|
||||
}
|
||||
UiDrawCrossIcon(g, closeRect, closeHot ? m_theme->danger : m_theme->textMuted, 1.5f * m_scale);
|
||||
}
|
||||
|
||||
void SignalPanel::PaintSignalRow(gd::Graphics& g, int row)
|
||||
{
|
||||
if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||||
const SignalData* signal = m_dataSet->Resolve(ref);
|
||||
const SignalStyle* style = StyleOf(ref);
|
||||
if (signal == nullptr || style == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const gd::RectF rect = RowRect(row);
|
||||
const bool selected = (SelectedRef() == ref);
|
||||
const bool hovered = (m_hoverRow == row);
|
||||
|
||||
if (selected)
|
||||
{
|
||||
FillRowRect(g, rect, m_theme->selection);
|
||||
FillRowRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height), m_theme->accent);
|
||||
}
|
||||
else if (hovered)
|
||||
{
|
||||
FillRowRect(g, rect, m_theme->hover);
|
||||
}
|
||||
|
||||
const float baseX = rect.X + kPadding * m_scale + kIndent * m_scale;
|
||||
const float centerY = rect.Y + rect.Height * 0.5f;
|
||||
|
||||
// 复选框。
|
||||
const gd::RectF checkRect(baseX + kDisclosureWidth * m_scale, centerY - kCheckSize * m_scale * 0.5f,
|
||||
kCheckSize * m_scale, kCheckSize * m_scale);
|
||||
// 可见性属于当前标签页,与全局共享的显示外观分开读取。
|
||||
const bool visible = m_tab->IsVisible(ref);
|
||||
UiDrawCheckBox(g, checkRect, visible ? 1 : 0, 3.0f * m_scale);
|
||||
|
||||
// 颜色块:叠加不透明度,透明度过低时自动补描边(与样式区色块规则一致)。
|
||||
const gd::RectF swatchRect(checkRect.X + checkRect.Width + 6.0f * m_scale,
|
||||
centerY - kSwatchSize * m_scale * 0.5f, kSwatchSize * m_scale,
|
||||
kSwatchSize * m_scale);
|
||||
UiDrawSwatch(g, swatchRect, style->color, visible ? style->opacity : 0.3f, 2.5f * m_scale,
|
||||
m_scale);
|
||||
|
||||
// 网格徽标(可点击,用于调整所属网格)。行尾 ✕ 恒在最右侧,徽标整体前移让位。
|
||||
const int laneIndex = m_tab->LaneOfSignal(ref);
|
||||
const bool badgeVisible = (laneIndex >= 0) && visible;
|
||||
const gd::RectF badgeRect = SignalBadgeRect(row);
|
||||
const gd::RectF removeRect = SignalRemoveRect(row);
|
||||
|
||||
float nameRightLimit = removeRect.X - 6.0f * m_scale;
|
||||
if (badgeVisible)
|
||||
{
|
||||
nameRightLimit = badgeRect.X - 6.0f * m_scale;
|
||||
}
|
||||
|
||||
// 名称:完全保留文件内的列名(含 "[n]" 序号与括号描述),双击可重命名。
|
||||
const float nameLeft = swatchRect.X + swatchRect.Width + 6.0f * m_scale;
|
||||
if (m_editField == StyleEditField::SIGNAL_NAME && m_editRow == row)
|
||||
{
|
||||
UiDrawEditField(g, RowNameRect(row), m_edit, m_fonts->ui, m_scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
const float nameWidth = std::max(16.0f, nameRightLimit - nameLeft);
|
||||
const std::uint32_t nameColor = visible ? m_theme->text : m_theme->textMuted;
|
||||
PaintRowText(g, UiEllipsize(signal->name, m_fonts->ui, nameWidth), m_fonts->ui,
|
||||
gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), nameColor, TextAlign::LEFT);
|
||||
}
|
||||
|
||||
if (badgeVisible)
|
||||
{
|
||||
const bool badgeHot = hovered && (m_hoverPart == RowPart::LANE_BADGE);
|
||||
UiFillRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
|
||||
badgeHot ? m_theme->accentSoft : m_theme->cardBg);
|
||||
UiDrawRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
|
||||
badgeHot ? m_theme->accent : m_theme->border, 1.0f);
|
||||
PaintRowText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
|
||||
badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
// 「移除该信号」✕:位于信号条目最右端,尺寸/样式/右边距与分组行的关闭 ✕ 完全一致。
|
||||
// 最后绘制,确保它永远压在最上层(与分组行 ✕ 的绘制顺序保持一致)。
|
||||
{
|
||||
const bool removeHot = hovered && (m_hoverPart == RowPart::REMOVE_SIGNAL);
|
||||
if (removeHot)
|
||||
{
|
||||
UiFillRoundRect(g, removeRect, 3.0f * m_scale, m_theme->pressed);
|
||||
}
|
||||
UiDrawCrossIcon(g, removeRect, removeHot ? m_theme->danger : m_theme->textMuted,
|
||||
1.5f * m_scale);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalPanel::PaintScrollBar(gd::Graphics& g)
|
||||
{
|
||||
if (!m_scrollBarVisible || m_contentHeight <= m_listRect.Height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UiFillRoundRect(g, m_scrollBarRect, kScrollBarThickness * m_scale * 0.5f, m_theme->gridWeak);
|
||||
UiFillRoundRect(g, ScrollThumbRect(), kScrollBarThickness * m_scale * 0.5f, m_theme->borderStrong);
|
||||
}
|
||||
|
||||
void SignalPanel::PaintStyleSection(gd::Graphics& g)
|
||||
{
|
||||
if (m_styleRect.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UiFillRect(g, m_styleRect, m_theme->panelBg);
|
||||
|
||||
// ---- 顶部「目标」行(无论是否已选中信号都要绘制:未选中时显示占位文本)----
|
||||
PaintTargetRow(g);
|
||||
|
||||
const SignalStyle* style = (m_tab == nullptr) ? nullptr : StyleOf(SelectedRef());
|
||||
if (style == nullptr || SelectedRef().IsValid() == false)
|
||||
{
|
||||
// 提示文本只占「目标」行以下的区域,避免与目标行重叠。
|
||||
const float top = m_targetRowRect.Y + m_targetRowRect.Height;
|
||||
UiText(g, L"请选择信号以编辑样式", m_fonts->uiSmall,
|
||||
gd::RectF(m_styleRect.X, top, m_styleRect.Width,
|
||||
std::max(0.0f, m_styleRect.Y + m_styleRect.Height - top)),
|
||||
m_theme->textMuted, TextAlign::CENTER);
|
||||
return;
|
||||
}
|
||||
|
||||
const float labelWidth = kStyleLabelWidth * m_scale;
|
||||
|
||||
// 行标签统一以整行为基准垂直居中,确保与同行控件纵向对齐。
|
||||
auto paintRowLabel = [&](int rowIndex, const wchar_t* text)
|
||||
{
|
||||
const gd::RectF& row = m_styleRowRects[static_cast<std::size_t>(rowIndex)];
|
||||
UiText(g, text, m_fonts->uiSmall, gd::RectF(row.X, row.Y, labelWidth, row.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
};
|
||||
|
||||
// ---- 第一行:颜色(预设色块 + 自定义颜色入口)----
|
||||
{
|
||||
paintRowLabel(0, L"颜色");
|
||||
|
||||
int paletteCount = 0;
|
||||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||||
|
||||
// 判定“当前是否在用自定义色”:必须同时满足
|
||||
// 1) 显示颜色等于记忆的自定义色;
|
||||
// 2) 该颜色不是任何一个可见的预设色。
|
||||
// 否则一旦自定义色与某个预设色相同时,点击那个预设色块会把高亮错误地
|
||||
// 挪到自定义色块上(用户报告的“点预设色直接跳到自定义色块”)。
|
||||
bool presetMatches = false;
|
||||
for (int index = 0; index < m_colorCount; ++index)
|
||||
{
|
||||
if (style->color == palette[index % paletteCount])
|
||||
{
|
||||
presetMatches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const bool customActive = (style->color == style->customColor) && !presetMatches;
|
||||
for (int index = 0; index < m_colorCount; ++index)
|
||||
{
|
||||
const gd::RectF& rect = m_colorRects[static_cast<std::size_t>(index)];
|
||||
const std::uint32_t color = palette[index % paletteCount];
|
||||
const bool hovered = (m_hoverStyle == StyleHit::COLOR && m_hoverStyleIndex == index);
|
||||
const bool active = !customActive && (style->color == color);
|
||||
|
||||
// 预设色块同样叠加不透明度;透明度过低时自动补描边以保证可辨识(与自定义色块一致)。
|
||||
UiDrawSwatch(g, rect, color, style->opacity, kCorner * m_scale, m_scale);
|
||||
if (active || hovered)
|
||||
{
|
||||
const gd::RectF ring(rect.X - 2.0f * m_scale, rect.Y - 2.0f * m_scale,
|
||||
rect.Width + 4.0f * m_scale, rect.Height + 4.0f * m_scale);
|
||||
UiDrawRoundRect(g, ring, 5.0f * m_scale,
|
||||
active ? m_theme->textStrong : m_theme->borderStrong,
|
||||
active ? 1.6f : 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义颜色入口:与预设色块完全同尺寸、同外框规则的小方格,仅多一个加号图标。
|
||||
// 方格始终显示记忆的自定义颜色,因此从任意预设色再次点击它都能立即回到上次的自定义色。
|
||||
const bool chipHot = (m_hoverStyle == StyleHit::CUSTOM_CHIP);
|
||||
const std::uint32_t chipColor =
|
||||
UiDrawSwatch(g, m_customChipRect, style->customColor, style->opacity, kCorner * m_scale, m_scale);
|
||||
const std::uint32_t iconColor = IsDarkColor(chipColor) ? 0xFFFDF6E3u : 0xFF073642u;
|
||||
UiDrawPlusIcon(g, m_customChipRect, iconColor, 1.6f * m_scale);
|
||||
|
||||
// 当前正在使用自定义颜色、或鼠标悬停时,显示与预设色块一致的外框。
|
||||
if (customActive || chipHot)
|
||||
{
|
||||
const gd::RectF& chip = m_customChipRect;
|
||||
const gd::RectF ring(chip.X - 2.0f * m_scale, chip.Y - 2.0f * m_scale,
|
||||
chip.Width + 4.0f * m_scale, chip.Height + 4.0f * m_scale);
|
||||
UiDrawRoundRect(g, ring, 5.0f * m_scale,
|
||||
customActive ? m_theme->textStrong : m_theme->borderStrong,
|
||||
customActive ? 1.6f : 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 第二行:样式(线段 / 散点 / 点线)----
|
||||
{
|
||||
paintRowLabel(1, L"样式");
|
||||
|
||||
for (int index = 0; index < kModeOptionCount; ++index)
|
||||
{
|
||||
const gd::RectF& rect = m_modeRects[static_cast<std::size_t>(index)];
|
||||
const bool active = (style->mode == kModeOptions[index]);
|
||||
const bool hovered = (m_hoverStyle == StyleHit::MODE && m_hoverStyleIndex == index);
|
||||
|
||||
UiFillRoundRect(g, rect, kCorner * m_scale,
|
||||
active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg));
|
||||
UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f);
|
||||
UiText(g, DrawModeName(kModeOptions[index]), m_fonts->uiSmall, rect,
|
||||
active ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 第三行:调整(系数 / 镜像 / 绝对值,三个切换类按钮)----
|
||||
// 结算顺序固定为「系数 → 绝对值 → 镜像」;「系数」「镜像」点击后展开二级面板。
|
||||
{
|
||||
paintRowLabel(2, L"调整");
|
||||
|
||||
const auto paintToggle = [&](int index, const wchar_t* label, bool checked, StyleHit target)
|
||||
{
|
||||
const gd::RectF& rect = m_adjustRects[static_cast<std::size_t>(index)];
|
||||
const bool hot = (m_hoverStyle == target);
|
||||
UiFillRoundRect(g, rect, kCorner * m_scale,
|
||||
checked ? m_theme->accentSoft : (hot ? m_theme->hover : m_theme->cardBg));
|
||||
UiDrawRoundRect(g, rect, kCorner * m_scale,
|
||||
checked ? m_theme->accent : m_theme->border, 1.0f);
|
||||
UiText(g, label, m_fonts->uiSmall, rect,
|
||||
checked ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||
};
|
||||
// 系数/偏移:系数不等于默认值 1、或任一偏移不为 0 时显示为"已启用"。
|
||||
paintToggle(kAdjustGain, L"系数/偏移",
|
||||
!GainIsDefault(style->gain) || style->Offset(), StyleHit::GAIN_ADJUST);
|
||||
// 镜像:水平或垂直任一被勾选即显示为激活态。
|
||||
paintToggle(kAdjustMirror, L"镜像", style->Mirrored(), StyleHit::MIRROR_ADJUST);
|
||||
paintToggle(kAdjustAbsolute, L"绝对值", style->absolute, StyleHit::ABSOLUTE_TOGGLE);
|
||||
}
|
||||
|
||||
// ---- 第四行:线宽(数值可点击输入)----
|
||||
{
|
||||
paintRowLabel(3, L"线宽");
|
||||
|
||||
const double ratio =
|
||||
(static_cast<double>(style->lineWidth) - kWidthMin) / (kWidthMax - kWidthMin);
|
||||
const bool active = m_draggingWidth || (m_hoverStyle == StyleHit::WIDTH_SLIDER);
|
||||
UiDrawSlider(g, m_widthTrackRect, ratio, m_theme->accent, active, m_scale);
|
||||
|
||||
if (m_editField == StyleEditField::WIDTH)
|
||||
{
|
||||
UiDrawEditField(g, m_widthValueRect, m_edit, m_fonts->monoSmall, m_scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::wstring numberText = FormatNumber(style->lineWidth, 1);
|
||||
const std::wstring fullText = numberText + L" px";
|
||||
// 空间不足时省略单位,保证数值本身完整可读。
|
||||
const std::wstring shown =
|
||||
(UiMeasureTextWidth(m_fonts->monoSmall, fullText) >
|
||||
m_widthValueRect.Width - 6.0f * m_scale)
|
||||
? numberText
|
||||
: fullText;
|
||||
UiText(g, shown, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, TextAlign::RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 第五行:透明度(数值可点击输入)----
|
||||
{
|
||||
paintRowLabel(4, L"透明度");
|
||||
|
||||
const bool active = m_draggingOpacity || (m_hoverStyle == StyleHit::OPACITY_SLIDER);
|
||||
UiDrawSlider(g, m_opacityTrackRect, static_cast<double>(style->opacity), m_theme->accent, active,
|
||||
m_scale);
|
||||
|
||||
if (m_editField == StyleEditField::OPACITY)
|
||||
{
|
||||
UiDrawEditField(g, m_opacityValueRect, m_edit, m_fonts->monoSmall, m_scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
const int percent = static_cast<int>(style->opacity * 100.0f + 0.5f);
|
||||
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect,
|
||||
m_theme->textStrong, TextAlign::RIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalPanel::PaintTargetRow(gd::Graphics& g)
|
||||
{
|
||||
if (m_fonts == nullptr || m_theme == nullptr || m_targetRowRect.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UpdateTargetRowGeometry();
|
||||
|
||||
const float labelWidth = kStyleLabelWidth * m_scale;
|
||||
// 行标签与前五行一致(同一标签列宽度、同一纵向居中基准)。
|
||||
UiText(g, L"目标", m_fonts->uiSmall,
|
||||
gd::RectF(m_targetRowRect.X, m_targetRowRect.Y, labelWidth, m_targetRowRect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
|
||||
const SignalRef& target = SelectedRef();
|
||||
const SignalData* signal =
|
||||
(m_dataSet != nullptr && target.IsValid()) ? m_dataSet->Resolve(target) : nullptr;
|
||||
|
||||
// 没有可编辑的目标信号:只显示占位提示(与样式区其余行的"请选择信号"提示一致)。
|
||||
if (signal == nullptr)
|
||||
{
|
||||
const float contentLeft = m_targetRowRect.X + labelWidth + kStyleLabelGap * m_scale;
|
||||
UiText(g, L"未选择信号", m_fonts->uiSmall,
|
||||
gd::RectF(contentLeft, m_targetRowRect.Y,
|
||||
std::max(0.0f, m_targetRowRect.X + m_targetRowRect.Width - contentLeft),
|
||||
m_targetRowRect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
return;
|
||||
}
|
||||
|
||||
// 名称:悬停时给一层与信号列表行一致的背景高亮(用户要求"显示与交互效果相同")。
|
||||
if (m_hoverStyle == StyleHit::TARGET_NAME && m_targetNameRect.Width > 0.0f)
|
||||
{
|
||||
UiFillRoundRect(g, m_targetNameRect, kCorner * m_scale, m_theme->hover);
|
||||
}
|
||||
|
||||
if (m_editField == StyleEditField::SIGNAL_NAME && m_editRow < 0)
|
||||
{
|
||||
UiDrawEditField(g, EditFieldRect(), m_edit, m_fonts->ui, m_scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 颜色与信号列表行一致:已绘制到网格用常态色,未绘制用弱色。
|
||||
const std::uint32_t nameColor = (m_targetGeomLane >= 0) ? m_theme->text : m_theme->textMuted;
|
||||
// ⚠️ 这里**不能**用 `PaintRowText`(它把文本裁到 `m_listRect`,而「目标」行在样式区、
|
||||
// 列表矩形之外 ⇒ 文本会被完全裁掉,用户报告“信号名与 GX 标签都没正确绘制”)。
|
||||
UiText(g, UiEllipsize(signal->name, m_fonts->ui, m_targetNameRect.Width), m_fonts->ui,
|
||||
m_targetNameRect, nameColor, TextAlign::LEFT);
|
||||
}
|
||||
|
||||
// GX 网格标签:信号已绘制到某个网格时显示在名称**后方**(用户 2026-10-15 要求),
|
||||
// 并与其它控件一样**右对齐到内容列右边缘**。
|
||||
if (m_targetBadgeRect.Width > 0.0f)
|
||||
{
|
||||
const bool badgeHot = (m_hoverStyle == StyleHit::TARGET_BADGE);
|
||||
UiFillRoundRect(g, m_targetBadgeRect, kBadgeHeight * m_scale * 0.5f,
|
||||
badgeHot ? m_theme->accentSoft : m_theme->cardBg);
|
||||
UiDrawRoundRect(g, m_targetBadgeRect, kBadgeHeight * m_scale * 0.5f,
|
||||
badgeHot ? m_theme->accent : m_theme->border, 1.0f);
|
||||
UiText(g, L"G" + std::to_wstring(m_targetGeomLane + 1), m_fonts->monoSmall,
|
||||
m_targetBadgeRect, badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
Reference in new issue
Block a user