① 主题自适应(`Theme.cpp`):新增 `SrgbLuminance` / `ContrastRatio` / `EnsureContrast` /
`MixToLuminance` / `ThemeLaneBackground`;四套主题的结构色(gridWeak 1.30 / gridStrong 1.65 /
axisLine 2.10 / borderStrong 1.70)按底色自动校正,Solarised Light 逐位不变;网格叠底改用
`ThemeLaneBackground()`(4 处),深色主题下网格层与绘图区底色可辨。
② 派生预览曲线(`HighContrastDerived`)重写为**连续**方案:方向沿信号所在侧远离底色,
目标亮度 = max(与底色对比度 3.2, 信号亮度 ± 0.18),再按亮度平滑混色;调用点传入真实网格底色
(旧实现默认写死 Solarised Light 底色 ⇒ 深色主题永远选到更暗的那一侧、几乎看不见)。
③ 信号样式区「线宽 / 透明度」两行右侧新增**复位按钮**(`UiPaintResetIcon` 成为唯一实现,
菜单/弱峰面板/样式区共用同一套笔画);默认线宽统一到 `DefaultSignalLineWidth(finiteCount)`
(单点信号取 10.0)。按钮右边界与内容列对齐,内容列过窄时整列不显示。
④ 「均值滤波」面板重做:信号色叠底标题栏 + 复选框 + 展开三角 + 「均值点数 / 迭代次数」两行滑块
(点击数值可改滑块上限、右侧复位)+ 内容区滚动;新增 `MeanFilterItem::{maxPoints,maxIterations,
collapsed}` 与 `Lane::meanFilterScroll`,缓存键同步补全。
⑤ 浮动面板命中像素缓存时只贴**面板圆角形状之内**的像素(`CreateRoundRectRgn` 裁剪)并按本帧重画投影
⇒ 中键移视界后面板周围不再出现一圈旧波形。
⑥ 显示布局边界:每个浮动面板**独占一个绘制层**(按 `m_panelOrder` 递增),绘制侧
`UiSetTextLayer` 在同一循环内同步推进 ⇒ 下层面板的边框/文字不再画到上层面板上。
⑦ 自由标记:采样候选距离改为**二维欧氏距离**(单点信号不再在整幅宽度内吸住光标),
纵向光标画在鼠标位置、圆点落在真实采样点(新增 `FreeMarkerProbe::pointX`);
`MinorCellSparse` 只对胜出信号算一次;折线缓存容量改为 `max(64, 信号数×2)` 且查找改为
单键哈希 ⇒ MF4 的 285 信号合并网格中 `ProbeFreeMarker` 平均 0.11ms。
验证:`.preflight` 探针重建(`panelcheck` 71/71、`markercheck` 13/13、`ringcheck` 6/6,合计 90 条,
LTO 目标文件上复跑同样全绿);`make release` / `make debug` 零警告;产物 815,104 B。
文档:`agent_mem.md` 第十六轮(含补丁脚本与编码/行尾踩坑)。
1483 lines
60 KiB
C++
1483 lines
60 KiB
C++
#ifndef __SIGNAL_PANEL_HPP__
|
||
#define __SIGNAL_PANEL_HPP__
|
||
|
||
#include <windows.h>
|
||
#include <objidl.h>
|
||
#include <gdiplus.h>
|
||
|
||
#include <array>
|
||
#include <cstdint>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include "ColorPickerPopup.hpp"
|
||
#include "DataModel.hpp"
|
||
#include "PaintUtil.hpp"
|
||
#include "PopupMenu.hpp"
|
||
#include "Theme.hpp"
|
||
#include "UiComponent.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
class UiBoxTable;
|
||
|
||
/**
|
||
* @brief 信号面板弹出菜单的命令标识。
|
||
*/
|
||
enum MenuCommand : int
|
||
{
|
||
CMD_SIGNAL_SOLO = 2, /**< 仅显示该信号。 */
|
||
CMD_SIGNAL_CLONE = 5, /**< 克隆该信号(应用「调整」并烘进采样值,时间轴偏移保留)。 */
|
||
CMD_GROUP_SHOW_ALL = 10, /**< 选中该分组全部信号。 */
|
||
CMD_GROUP_HIDE_ALL = 11, /**< 取消该分组全部信号。 */
|
||
CMD_GROUP_REMOVE = 12, /**< 从工作区移除该分组。 */
|
||
CMD_LANE_BASE = 300, /**< 网格归属基准(配合下标)。 */
|
||
CMD_LANE_NEW = 400, /**< 新建网格。 */
|
||
CMD_GAIN_EDIT = 500, /**< 「系数」二级面板里的系数输入框(幂等提交)。 */
|
||
CMD_GAIN_RESET = 501, /**< 「系数调整」标题栏右侧的还原按钮:把系数重置为 1。 */
|
||
CMD_OFFSET_RESET = 502, /**< 「偏移调整」标题栏右侧的还原按钮:两个偏移都恢复为默认值。 */
|
||
CMD_OFFSET_Y = 503, /**< 「偏移调整」:Y 轴偏移输入框。 */
|
||
CMD_OFFSET_TIME = 504, /**< 「偏移调整」:时间轴偏移输入框。 */
|
||
CMD_MIRROR_HORIZONTAL = 510, /**< 「镜像」二级面板:水平镜像(数据反序)。 */
|
||
CMD_MIRROR_VERTICAL = 511 /**< 「镜像」二级面板:垂直镜像(按 Y=0 上下翻转)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 左侧信号面板:按分组的信号树、可见性勾选与样式编辑。
|
||
*
|
||
* 面板只读取数据层(DataSet),全部显示配置读写均落在当前标签页(TabConfig)上,
|
||
* 因此不同标签页的信号勾选与样式互不影响。所有弹出菜单与拾色器均使用主题化自绘
|
||
* 浮动面板,不出现系统默认菜单样式。
|
||
*/
|
||
class SignalPanel : public UiComponent
|
||
{
|
||
public:
|
||
SignalPanel();
|
||
|
||
/**
|
||
* @brief 绑定宿主窗口、字体、主题与共用浮动控件。
|
||
* @param [IN] host 宿主窗口句柄。
|
||
* @param [IN] fonts 字体集合,需在面板生命周期内保持有效。
|
||
* @param [IN] theme 主题配色,需在面板生命周期内保持有效。
|
||
* @param [IN] menu 共用的主题化弹出菜单,需在面板生命周期内保持有效。
|
||
* @param [IN] colorPicker 共用的浮动拾色器,需在面板生命周期内保持有效。
|
||
*/
|
||
void Setup(HWND host, const UiFonts* fonts, const Theme* theme, PopupMenu* menu,
|
||
ColorPickerPopup* colorPicker);
|
||
|
||
/**
|
||
* @brief 绑定数据层与当前标签页配置。
|
||
* @param [IN] dataSet 数据集合(可写,用于重命名信号与分组)。
|
||
* @param [IN] tab 当前标签页配置,允许为空。
|
||
*/
|
||
void SetContext(DataSet* dataSet, TabConfig* tab);
|
||
|
||
/**
|
||
* @brief 设置宿主客户区矩形(用于约束浮动面板位置)。
|
||
* @param [IN] bounds 客户区矩形。
|
||
*/
|
||
void SetClientBounds(const gd::RectF& bounds) { m_clientBounds = bounds; }
|
||
|
||
/**
|
||
* @brief 标记列表结构失效(数据或折叠状态变化后调用)。
|
||
*/
|
||
void InvalidateRows()
|
||
{ m_rowsDirty = true; // 列表结构变化后行下标会失效 ⇒ 悬停联动高亮也必须一并作废,
|
||
// 否则可能对一条已经不存在(或已被替换)的信号继续高亮它的网格。
|
||
m_hoveredSignal = SignalRef();
|
||
m_hoverRow = -1;
|
||
m_hoverPart = RowPart::NONE;
|
||
// 分组下标可能整体位移 ⇒ 折起动画(含与滚动的同步)也不能继续沿用。
|
||
m_groupAnims.clear();
|
||
m_rowShift.clear();
|
||
// ⚠️ **行下标也要一并作废**:`m_editRow` / `m_pendingCloseRow` / `m_groupNameClickRow`
|
||
// 长期保存着行下标,而行集合在编辑期间可能变化(加载文件 → `SyncTabsToDataSet()` →
|
||
// 本函数)。不清理的话,随后回车会把新名字写到**另一条**信号上(插入型变更会让
|
||
// 下标整体位移),✕ 也可能删错分组。
|
||
if (m_editField != StyleEditField::NONE)
|
||
{
|
||
m_edit.End();
|
||
m_editField = StyleEditField::NONE;
|
||
}
|
||
m_editRow = -1;
|
||
m_pendingCloseRow = -1;
|
||
m_pendingClosePart = RowPart::NONE;
|
||
m_pendingFilterClear = false;
|
||
m_groupNameClickRow = -1;
|
||
}
|
||
|
||
/**
|
||
* @brief 设置 DPI 缩放比例。
|
||
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
|
||
*/
|
||
void SetScale(float scale) override;
|
||
|
||
/**
|
||
* @brief 按给定区域重新排布面板内容。
|
||
* @param [IN] area 面板可用区域。
|
||
*/
|
||
void Layout(const gd::RectF& area) override;
|
||
|
||
/** @brief 面板占用的整块区域(与 `Layout` 同源)。 */
|
||
gd::RectF Area() const override { return m_area; }
|
||
|
||
/**
|
||
* @brief 绘制面板。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void Paint(gd::Graphics& g) override;
|
||
|
||
/**
|
||
* @brief 主题配色变化后作废全部**像素级**缓存。
|
||
*
|
||
* 行位图缓存里烘着主题色(底色 / 文字 / 描边),换主题后必须整体丢弃,
|
||
* 否则列表会停留在旧配色上(直到那些行因为别的原因被重画)。
|
||
*/
|
||
void OnThemeChanged()
|
||
{
|
||
InvalidateRowCache();
|
||
m_rowsDirty = true;
|
||
}
|
||
|
||
/**
|
||
* @brief 处理鼠标移动。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 悬停状态发生变化返回 true。
|
||
*/
|
||
bool OnMouseMove(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理鼠标左键按下。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnLButtonDown(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理鼠标左键抬起(网格徽标与自定义颜色入口在此刻触发展开)。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 视觉状态发生变化返回 true。
|
||
*/
|
||
bool OnLButtonUp(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理鼠标左键双击。
|
||
*
|
||
* 语义按落点区分:**线宽 / 透明度数值**进入输入状态;**分组名称 / 信号名称**进入重命名;
|
||
* 复选框、网格徽标、两处 ✕ 没有双击语义(由宿主按普通按下处理)。
|
||
*
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnDoubleClick(int x, int y);
|
||
|
||
/**
|
||
* @brief 双击落点是否应**当作普通「按下」**处理(该位置没有双击语义)。
|
||
*
|
||
* ⚠️ 窗口类带 `CS_DBLCLKS`:快速连点同一个控件时,第二次(及之后)按下会被 Windows
|
||
* 合成为 `WM_LBUTTONDBLCLK`。若宿主把它交给 `OnDoubleClick`,而该位置本来只有
|
||
* 「按下 / 单击」语义(复选框、全部显示隐藏、绝对值、折叠三角、「组X」、✕、GX 徽标…),
|
||
* 那一次按下就**完全丢失** —— 用户看到的正是“连点好几下、状态只切换了几次”(不跟手)。
|
||
*
|
||
* ✓ 本函数为这些位置返回 true,宿主改走 `OnLButtonDown`(与单击完全相同的路径)。
|
||
* 只有真正有双击语义的位置(线宽/不透明度数值框、样式区「目标」行名称与 GX 标签、
|
||
* 分组 / 信号行的**名称区域**改名)返回 false。
|
||
*
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 应当作普通按下返回 true。
|
||
*/
|
||
bool DoubleClickIsPlainPress(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理鼠标右键按下(弹出上下文菜单)。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnRButtonDown(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理滚轮滚动(列表纵向滚动)。
|
||
* @param [IN] delta 滚轮增量。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnMouseWheel(int delta);
|
||
|
||
/**
|
||
* @brief 处理鼠标离开面板。
|
||
* @return 视觉状态发生变化返回 true。
|
||
*/
|
||
bool OnMouseLeave() override;
|
||
|
||
/**
|
||
* @brief 处理弹出菜单的选择结果。
|
||
* @param [IN] result 菜单选择结果。
|
||
* @return 需要重新布局返回 true。
|
||
*/
|
||
bool OnMenuCommand(const MenuResult& result);
|
||
|
||
/**
|
||
* @brief 判断面板是否处于内联文本输入状态。
|
||
* @return 正在输入返回 true。
|
||
*/
|
||
bool IsEditing() const override { return m_editField != StyleEditField::NONE || m_filterEditing; }
|
||
|
||
/**
|
||
* @brief 判断坐标是否落在列表上方的「筛选信号」输入框内。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 落在输入框内返回 true。
|
||
*/
|
||
bool IsOnFilterBox(int x, int y) const;
|
||
|
||
/**
|
||
* @brief 进入筛选输入框的编辑状态(单击输入框时调用)。
|
||
*/
|
||
void BeginFilterEdit();
|
||
|
||
/**
|
||
* @brief 结束筛选输入框的编辑状态(点击别处或回车时调用)。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool EndFilterEdit();
|
||
|
||
/**
|
||
* @brief 取出并清除「请把绘图区滚动到该网格」的联动请求。
|
||
*
|
||
* 用户 2026-10-16 要求:点击信号选择区域的**信号名**(或样式区「目标」行的信号名)时,
|
||
* 若该信号所在网格不在绘图区可视范围内,则自动调节绘图区的滚动条让该网格完整出现。
|
||
* 悬停时**不做**这个滚动(只保留既有的网格高亮联动)。
|
||
*
|
||
* @param [OUT] laneIndex 需要滚动到可视区内的网格下标。
|
||
* @return 存在待处理的请求返回 true。
|
||
*/
|
||
bool TakePlotLaneScrollRequest(int& laneIndex)
|
||
{
|
||
if (!m_plotLaneRequestFlag)
|
||
{
|
||
return false;
|
||
}
|
||
m_plotLaneRequestFlag = false;
|
||
laneIndex = m_pendingPlotLane;
|
||
m_pendingPlotLane = -1;
|
||
return laneIndex >= 0;
|
||
}
|
||
|
||
/**
|
||
* @brief 向当前输入框送入一个字符。
|
||
* @param [IN] character 输入字符('\r' 提交、'\b' 退格、0x03 复制)。
|
||
* @return 界面状态发生变化返回 true。
|
||
*/
|
||
bool OnEditChar(wchar_t character);
|
||
|
||
/**
|
||
* @brief 向当前输入框送入一个按键。
|
||
* @param [IN] key 虚拟键码。
|
||
* @return 界面状态发生变化返回 true。
|
||
*/
|
||
bool OnEditKey(WPARAM key);
|
||
|
||
/**
|
||
* @brief 编辑态下按下鼠标右键:取消选区并把插入符移到文本末尾。
|
||
* @return 发生改变返回 true;不在编辑态返回 false。
|
||
*/
|
||
bool CollapseEditSelectionToEnd();
|
||
|
||
/**
|
||
* @brief 编辑态下在输入框内按下鼠标左键:定位插入符并开始拖拽框选。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 命中输入框返回 true。
|
||
*/
|
||
bool OnEditMouseDown(int x, int y);
|
||
|
||
/**
|
||
* @brief 编辑态下拖拽鼠标:扩展选区。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @return 选区发生变化返回 true。
|
||
*/
|
||
bool OnEditMouseMove(int x);
|
||
|
||
/**
|
||
* @brief 编辑态下松开鼠标左键:结束拖拽框选。
|
||
* @return 之前处于拖拽中返回 true。
|
||
*/
|
||
bool OnEditMouseUp(int x, int y);
|
||
|
||
/**
|
||
* @brief 获取指定信号的显示外观(只读,全局共享)。
|
||
* @param [IN] ref 信号引用。
|
||
* @return 外观指针,引用无效时返回 nullptr。
|
||
*/
|
||
const SignalStyle* StyleOf(const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 获取指定信号的显示外观(可写,全局共享)。
|
||
* @param [IN] ref 信号引用。
|
||
* @return 外观指针,引用无效时返回 nullptr。
|
||
*/
|
||
SignalStyle* StyleOf(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 获取当前选中信号(跨标签页共享,因此切换视图后选中项保持一致)。
|
||
* @return 信号引用。
|
||
*/
|
||
const SignalRef& SelectedRef() const;
|
||
|
||
/**
|
||
* @brief 设置当前选中信号(跨标签页共享)。
|
||
* @param [IN] ref 信号引用。
|
||
*/
|
||
void SelectSignal(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 将浮动拾色器的取色结果应用到当前选中信号。
|
||
* @param [IN] color 0xAARRGGBB 颜色值。
|
||
* @return 应用成功返回 true。
|
||
*/
|
||
bool ApplyColor(std::uint32_t color);
|
||
|
||
/**
|
||
* @brief 关闭自定义颜色面板与线宽输入状态。
|
||
* @return 界面状态发生变化返回 true。
|
||
*/
|
||
bool CloseTransientUi();
|
||
|
||
/**
|
||
* @brief 取出并清除“需要重新布局”标记。
|
||
* @return 需要重新布局返回 true。
|
||
*/
|
||
bool TakeLayoutDirty();
|
||
|
||
/**
|
||
* @brief 判断坐标是否落在面板区域内。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 落在区域内返回 true。
|
||
*/
|
||
bool HitTest(int x, int y) const override;
|
||
|
||
/**
|
||
* @brief 查询该位置应使用的鼠标光标(✕ 类按钮使用手型)。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 需要特殊光标时返回光标句柄,否则返回 nullptr。
|
||
* @note 需要按需重建列表行缓存(查 ✕ 的命中),因此不是 const。
|
||
*/
|
||
HCURSOR QueryCursor(int x, int y) override;
|
||
|
||
/**
|
||
* @brief 上报面板区域与其内部结构(表头 / 筛选框 / 列表 / 样式区 / 可见行 / 行内元素)
|
||
* 的几何 box。
|
||
*
|
||
* 行内元素(复选框、色块、名称、GX 徽标、✕、折起三角、组计数)的几何与
|
||
* `RowHitPart` / `PaintRow` **同源**:都基于同一个 `RowRect` 与同一批常量,
|
||
* 因此"画出来的框"就是"能点到的区域"。
|
||
*
|
||
* 面板的行位图缓存与本函数**无关**:box 只交给宿主在最后一步统一处理(目前是描边),
|
||
* 不进任何缓存 ⇒ 关掉「显示布局边界」后不会留下残影。
|
||
*
|
||
* @param [IN,OUT] boxes 收集器。
|
||
*/
|
||
void BuildBoxes(UiBoxTable& boxes) override;
|
||
|
||
/**
|
||
* @brief 将列表滚动到指定信号所在位置(无动画,立即到位)。
|
||
* @param [IN] ref 目标信号引用。
|
||
*/
|
||
void ScrollToSignal(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 平滑滚动到指定信号(带动画)。
|
||
*
|
||
* 用户 2026-10-15 要求:触发联动机制(点击图例 / 信号曲线,或在样式自定义区与「目标」行
|
||
* 做任何交互)时,要把信号列表**滚动**到目标信号完整可见:
|
||
* * 目标在当前视界上方 ⇒ 向上滚动,直到它**完整露出且不被悬浮组名遮挡**;
|
||
* * 目标在当前视界下方 ⇒ 向下滚动直到它完整露出;
|
||
* * 滚动速度随距离变化(越远越快、靠近时变慢),但总时长有上限(不要拖得太久)。
|
||
*
|
||
* @param [IN] ref 目标信号引用。
|
||
*/
|
||
void ScrollToSignalAnimated(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 若目标信号所在分组处于**当前模式的折起状态**,自动把它展开。
|
||
*
|
||
* 折起的分组不会列出信号行 ⇒ 行集合里找不到目标信号,任何“滚动到该信号”都会落空
|
||
*(用户 2026-10-16 报告:点网格图例联动时,被折起的组不会自动展开、列表不动)。
|
||
* 展开是**立刻生效**的(不播放展开动画),因为随后的滚动目标依赖稳定的行位置。
|
||
*
|
||
* @param [IN] ref 目标信号。
|
||
* @return 发生了展开返回 true。
|
||
*/
|
||
bool ExpandGroupForSignal(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 推进一步滚动动画(由宿主定时器驱动)。
|
||
* @return 动画仍在进行返回 true(宿主应保持定时重绘)。
|
||
*/
|
||
bool AdvanceScrollAnimation();
|
||
|
||
/** @brief 滚动动画是否正在进行。 */
|
||
bool ScrollAnimationActive() const { return m_scrollAnimActive; }
|
||
|
||
/**
|
||
* @brief 推进一步分组折起/展开动画(由宿主定时器驱动)。
|
||
* @return 仍有分组动画在进行返回 true(宿主应保持定时重绘)。
|
||
*/
|
||
bool AdvanceGroupAnimations();
|
||
|
||
/** @brief 分组折起/展开动画是否正在进行。 */
|
||
bool GroupAnimationActive() const { return !m_groupAnims.empty(); }
|
||
|
||
/**
|
||
* @brief 供绘图区联动高亮的「悬停信号」。
|
||
*
|
||
* 来源:① 列表里鼠标悬停的信号行;② 样式区「目标」行的信号名 / GX 标签。
|
||
* 鼠标移开即返回无效引用。
|
||
* @return 悬停的信号引用。
|
||
*/
|
||
const SignalRef& HoveredSignal() const { return m_hoveredSignal; }
|
||
|
||
/**
|
||
* @brief 「目标」(正在编辑样式的)信号所在的网格下标。
|
||
* @return 网格下标;未勾选绘制到任何网格时返回 -1。
|
||
*/
|
||
int TargetLaneIndex() const;
|
||
|
||
private:
|
||
/**
|
||
* @brief 样式区与列表行中可进行内联文本编辑的字段。
|
||
*/
|
||
enum class StyleEditField : int
|
||
{
|
||
NONE = 0, /**< 未处于编辑状态。 */
|
||
WIDTH = 1, /**< 线宽数值。 */
|
||
OPACITY = 2, /**< 不透明度数值(百分比)。 */
|
||
SIGNAL_NAME = 3, /**< 信号显示名称(列表行内;`row < 0` 表示样式区「目标」行)。 */
|
||
GROUP_TITLE = 4 /**< 分组显示名称(列表行内)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 列表行的类型。
|
||
*/
|
||
enum class RowKind : int
|
||
{
|
||
GROUP = 0, /**< 分组标题行。 */
|
||
SIGNAL = 1 /**< 信号行。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 行的可交互区域。
|
||
*/
|
||
enum class RowPart : int
|
||
{
|
||
NONE = 0, /**< 未命中。 */
|
||
DISCLOSURE, /**< 折叠三角。 */
|
||
CHECK, /**< 复选框。 */
|
||
BODY, /**< 行主体(分组行的名称区域 / 信号行的名称区域)。 */
|
||
GROUP_TITLE, /**< 分组行的「组X」标签(与折叠三角一样可点击折起)。 */
|
||
GROUP_COUNT, /**< 分组行右侧的「N 项」计数(同样只折叠展开,**绝不**进入改名)。 */
|
||
CLOSE, /**< 分组行的关闭按钮。 */
|
||
LANE_BADGE, /**< 信号行的网格徽标。 */
|
||
REMOVE_SIGNAL /**< 信号行**行尾**的「移除该信号」✕ 按钮(与分组 ✕ 同列同尺寸)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 样式编辑区中的可交互条目类型。
|
||
*/
|
||
enum class StyleHit : int
|
||
{
|
||
NONE = 0, /**< 未命中。 */
|
||
COLOR, /**< 预设颜色色块。 */
|
||
CUSTOM_CHIP, /**< 自定义颜色入口。 */
|
||
MODE, /**< 绘制样式按钮。 */
|
||
OPACITY_SLIDER, /**< 不透明度滑块。 */
|
||
OPACITY_VALUE, /**< 不透明度数值框(可点击输入)。 */
|
||
WIDTH_SLIDER, /**< 线宽滑块。 */
|
||
WIDTH_VALUE, /**< 线宽数值框(可点击输入)。 */
|
||
/**
|
||
* @brief 线宽行右侧的**复位按钮**(把线宽恢复为该信号的默认值)。
|
||
*
|
||
* 默认值:普通信号 = `kDefaultSignalLineWidth`(1.0 逻辑像素),
|
||
* 单数据点信号 = `kSinglePointSignalLineWidth`(最大线宽:散点半径随线宽增长)。
|
||
* 图标与「系数/偏移」二级菜单标题栏的还原图标**完全同款**(`UiPaintResetIcon`)。
|
||
*/
|
||
WIDTH_RESET,
|
||
/**
|
||
* @brief 透明度行右侧的**复位按钮**(把不透明度恢复为 100%)。
|
||
*
|
||
* 与 `WIDTH_RESET` 同一套图标与悬停观感,两者右边界与内容列右缘对齐。
|
||
*/
|
||
OPACITY_RESET,
|
||
GAIN_ADJUST, /**< 「系数」按钮(点击展开系数二级面板)。 */
|
||
MIRROR_ADJUST, /**< 「镜像」按钮(点击展开镜像二级面板)。 */
|
||
ABSOLUTE_TOGGLE, /**< 「绝对值」切换按钮。 */
|
||
TARGET_NAME, /**< 样式区「目标」行的信号名。 */
|
||
TARGET_BADGE, /**< 样式区「目标」行的 GX 网格标签。 */
|
||
TOGGLE_ALL, /**< 全部显示/隐藏按钮。 */
|
||
FILTER_CLEAR /**< 「筛选信号」输入框右侧的清空 ✕。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 列表中的一行(分组或信号)。
|
||
*/
|
||
struct ListRow
|
||
{
|
||
RowKind kind = RowKind::SIGNAL; /**< 行类型。 */
|
||
int groupIndex = -1; /**< 所属分组下标。 */
|
||
int signalIndex = -1; /**< 信号下标(仅信号行有效)。 */
|
||
float height = 0.0f; /**< 行高(像素)。 */
|
||
/**
|
||
* @brief 分组行显示的项数(仅分组行有效)。
|
||
*
|
||
* 普通模式 = 该分组的信号总数;筛选模式 = 该分组内**符合筛选条件**的信号个数
|
||
*(用户 2026-10-16 要求:筛选时组名后方的项数按筛选结果计数)。
|
||
*/
|
||
int displayCount = 0;
|
||
/**
|
||
* @brief 该行所属分组内「参与列出」的序号(仅信号行有效)。
|
||
*
|
||
* 用于折起动画按「离组名越远越快」计算每行的收缩比例。
|
||
*/
|
||
int ordinal = 0;
|
||
};
|
||
|
||
/**
|
||
* @brief 一个分组的折起/展开动画状态。
|
||
*
|
||
* 用户 2026-10-16 第二轮要求(**推翻**第一轮的按行缩放):“整个展开和收起的过程类似于
|
||
* 抽拉卷尺,卷尺上的元素相对位置是不会变的” —— 即**整组信号作为一个刚性整体平移**:
|
||
* 收起时所有信号一起向上平移、逐条消失在组名栏(越过组名下边界的部分被裁掉),
|
||
* 展开时逐条从组名栏下方冒出来。因为相对位置不变,所以**行高不发生变化**
|
||
* (无需每帧重建行集合),仅一个 `m_rowShift` 平移量在变 —— 这是动画流畅的关键。
|
||
*
|
||
* 另外:**悬浮(sticky)组名折起时不再“先滚动到位再折起”**,而是把滚动偏移与
|
||
* 平移**同步推进**(`scrollSynced`),保证悬浮组名始终留在列表最上方,
|
||
* 动画结束时滚动偏移恰好落在“组名正好处于最上方”的位置。
|
||
*/
|
||
struct GroupAnim
|
||
{
|
||
int groupIndex = -1; /**< 目标分组下标。 */
|
||
bool collapsing = false; /**< true = 正在折起,false = 正在展开。 */
|
||
int rowCount = 0; /**< 该分组参与动画的信号行数。 */
|
||
float blockHeight = 0.0f; /**< 该分组全部信号行的总高(展开态)。 */
|
||
/**
|
||
* @brief 动画起始时的平移量(0 = 全部露出,`blockHeight` = 全部藏进组名栏)。
|
||
*
|
||
* 一般是“干净的一端”(折起从 0、展开从 `blockHeight`);但若动画中途被反向
|
||
* 点击(折起没播完就点展开),就从**当前位移**续接,因此不会出现跳变。
|
||
*/
|
||
float fromShift = 0.0f;
|
||
bool scrollSynced = false; /**< 是否让滚动偏移随动画同步推进。 */
|
||
float scrollFrom = 0.0f; /**< 动画起始滚动偏移。 */
|
||
float scrollTo = 0.0f; /**< 动画结束滚动偏移(仅 `scrollSynced` 时生效)。 */
|
||
std::uint64_t startMs = 0; /**< 动画起始时刻(`GetTickCount64()`)。 */
|
||
std::uint32_t durationMs = 0; /**< 动画总时长(毫秒)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 计算「把某信号完整滚到可见」所需的滚动偏移。
|
||
*
|
||
* 顶部避让:当本组组名正在列表顶部悬浮时,列表顶部一段被组名占据,
|
||
* 上行方向必须让目标行落在组名的下方才算“完整可见且不被遮挡”。
|
||
*
|
||
* @param [IN] ref 目标信号。
|
||
* @param [OUT] desired 目标滚动偏移(未经 clamp)。
|
||
* @return 找到了该行返回 true。
|
||
*/
|
||
bool SignalScrollTarget(const SignalRef& ref, float& desired) const;
|
||
|
||
/**
|
||
* @brief 列表内容中某一行顶部相对内容原点的**自然**纵向偏移。
|
||
*
|
||
* 不含滚动偏移,也不含折起动画的刚性平移(动画只平移“动画分组之后”的行,
|
||
* 而分组行之前的行不受影响)⇒ 该值就是滚动到“本行正好处于列表最上方”所需的偏移。
|
||
* @param [IN] row 行下标。
|
||
* @return 偏移量(像素)。
|
||
*/
|
||
float ContentOffsetBeforeRow(int row) const;
|
||
|
||
/**
|
||
* @brief 启动一段滚动动画。
|
||
* @param [IN] desired 目标滚动偏移(内部会夹取到合法范围)。
|
||
*/
|
||
void StartScrollAnimationTo(float desired);
|
||
|
||
/**
|
||
* @brief 判断某分组在**当前模式**(筛选 / 普通)下是否处于折起状态。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @return 折起返回 true。
|
||
*/
|
||
bool IsGroupCollapsed(int groupIndex) const;
|
||
|
||
/**
|
||
* @brief 设置某分组在**当前模式**下的折起状态。
|
||
*
|
||
* ⚠️ 筛选模式与普通模式各有一套折起状态(`m_filterCollapsed` / `TabConfig`):
|
||
* 筛选只是临时的一个子集视图,不应污染用户在普通列表里的布局选择。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] collapsed 是否折起。
|
||
*/
|
||
void SetGroupCollapsed(int groupIndex, bool collapsed);
|
||
|
||
/**
|
||
* @brief 某分组当前会列出的信号数量(筛选模式下为符合筛选条件的个数)。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @return 数量。
|
||
*/
|
||
int ShownSignalCount(int groupIndex) const;
|
||
|
||
/**
|
||
* @brief 查找某分组的折起动画条目下标。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @return 条目下标;不存在返回 -1。
|
||
*/
|
||
int FindGroupAnimation(int groupIndex) const;
|
||
|
||
/**
|
||
* @brief 取动画的**缓动后进度**(0 → 1,ease-out cubic:起步快、末尾减速)。
|
||
* @param [IN] anim 动画条目。
|
||
* @return 缓动后的进度。
|
||
*/
|
||
float AnimEasedProgress(const GroupAnim& anim) const;
|
||
|
||
/**
|
||
* @brief 某个分组行在列表中的下标。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @return 行下标;不存在返回 -1。
|
||
*/
|
||
int GroupRowIndex(int groupIndex) const;
|
||
|
||
/**
|
||
* @brief 全部行的自然总高度(不含折起动画的折减)。
|
||
* @return 高度(像素)。
|
||
*/
|
||
float NaturalContentHeight() const;
|
||
|
||
/**
|
||
* @brief 某分组当前模式下会列出的信号行总高(展开态)。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @return 高度(像素)。
|
||
*/
|
||
float ShownBlockHeight(int groupIndex) const;
|
||
|
||
/**
|
||
* @brief 重新计算动画造成的“逐行刚性位移”与内容高度。
|
||
*
|
||
* 每帧(或行集合重建后)调用一次:把每个进行中的动画的位移量累加到它之后的每一行上,
|
||
* 并据此得出 `m_contentHeight`;与滚动同步的动画同时推进 `m_scrollOffset`。
|
||
* 行高本身不变,因此**不需要重建行集合**。
|
||
*/
|
||
void UpdateAnimGeometry();
|
||
|
||
/**
|
||
* @brief 开始一个分组的折起/展开动画。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] collapse true = 折起,false = 展开。
|
||
* @param [IN] scrollFrom 动画起始滚动偏移。
|
||
* @param [IN] scrollTo 动画结束滚动偏移(仅 `syncScroll` 为 true 时生效)。
|
||
* @param [IN] syncScroll 是否让滚动偏移随动画同步推进(悬浮组名折起时用)。
|
||
* @return 已开始动画返回 true(行数为 0 时无需动画,返回 false)。
|
||
*/
|
||
bool StartGroupToggle(int groupIndex, bool collapse, float scrollFrom, float scrollTo, bool syncScroll);
|
||
|
||
/**
|
||
* @brief 点击分组行:折起/展开该分组(悬浮时与滚动同步推进)。
|
||
*
|
||
* 用户 2026-10-16 第二轮要求:悬浮组名折起时**不再**“先滚动到位再折起”,
|
||
* 而是**一边向上滚动一边收起**,保证悬浮组名始终在最上方;动画完成时滚动偏移
|
||
* 恰好落在“组名正好在最上方”的位置,且信号已经收起。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] row 分组行下标。
|
||
*/
|
||
void ToggleGroupFromRow(int groupIndex, int row);
|
||
|
||
/**
|
||
* @brief 计算某行的可交互区域(使用 `RowRect` 的当前几何,含悬浮位置覆盖)。
|
||
* @param [IN] row 行下标。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 行内区域类型。
|
||
*/
|
||
RowPart RowHitPart(int row, float x, float y) const;
|
||
|
||
/**
|
||
* @brief 折起/展开动画中,某信号行“被藏进组名栏”的上边界(组名的**绘制**下边界)。
|
||
*
|
||
* 仅对本行所属分组正在播放动画的信号行有效;其余情况返回一个很小的值(等价于不裁剪)。
|
||
* @param [IN] row 行下标。
|
||
* @param [IN] stickyRow 当前悬浮的分组行下标(`StickyGroupRowIndex()` 的结果)。
|
||
* @return 裁剪上边界(屏幕 Y 坐标)。
|
||
*/
|
||
float AnimationHideTopForRow(int row, int stickyRow) const;
|
||
|
||
/**
|
||
* @brief 重新计算「目标」行内信号名与 GX 标签的矩形。
|
||
*
|
||
* 名称宽度依赖文本实际宽度、GX 标签是否出现依赖该信号是否已绘制到网格 —— 两者都可能在
|
||
* 不重新布局的情况下变化(切换选中、勾选/取消勾选),因此用一个小缓存键避免重算。
|
||
*/
|
||
void UpdateTargetRowGeometry();
|
||
|
||
/**
|
||
* @brief 绘制「目标」行(样式区最上方:名称 + GX 标签,可悬停 / 双击改名)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintTargetRow(gd::Graphics& g);
|
||
|
||
/** @brief 取消正在进行的滚动动画(用户主动滚动时调用)。 */
|
||
void CancelScrollAnimation()
|
||
{
|
||
m_scrollAnimActive = false;
|
||
// 与滚动同步的折起动画也不能再接管滚动偏移:用户已经自己滚动了,
|
||
// 继续同步会“抢”滚动条。折起/展开本身继续播放(只是不驱动滚动)。
|
||
for (GroupAnim& anim : m_groupAnims)
|
||
{
|
||
anim.scrollSynced = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 重新构建列表行集合。
|
||
*/
|
||
void BuildRows();
|
||
|
||
/**
|
||
* @brief 在需要时重建列表行集合。
|
||
*/
|
||
void EnsureRows();
|
||
|
||
/**
|
||
* @brief 依据内容高度重算滚动条可见性与列表可用宽度。
|
||
* @param [IN] clampScroll 是否顺带把滚动偏移夹到合法范围。
|
||
* ⚠️ 分组折起/展开动画期间必须传 `false`:与滚动同步的折起动画正按自己的进度
|
||
* 推进 `m_scrollOffset`,此时夹取会打断它(折起后内容比列表还短时尤其明显)。
|
||
*/
|
||
void RefreshScrollBar(bool clampScroll = true);
|
||
|
||
/**
|
||
* @brief 限制滚动偏移量到合法范围。
|
||
*/
|
||
void ClampScroll();
|
||
|
||
/**
|
||
* @brief 查询坐标命中的行下标与行内区域。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @param [OUT] part 行内区域类型。
|
||
* @return 行下标,未命中返回 -1。
|
||
*/
|
||
int HitRow(int x, int y, RowPart& part);
|
||
|
||
/**
|
||
* @brief 计算行的纵向矩形。
|
||
* @param [IN] row 行下标。
|
||
* @return 行矩形。
|
||
*/
|
||
gd::RectF RowRect(int row) const;
|
||
|
||
/**
|
||
* @brief 计算分组行关闭按钮矩形。
|
||
* @param [IN] row 行下标。
|
||
* @return 关闭按钮矩形。
|
||
*/
|
||
gd::RectF GroupCloseRect(int row) const;
|
||
|
||
/**
|
||
* @brief 计算分组行右侧「N 项」计数文本的矩形(**绘制与命中的唯一几何来源**)。
|
||
*
|
||
* ⚠️ 计数区与分组名相邻,但它**只有折叠/展开语义**(用户 2026-10-17 要求):
|
||
* 快速点击计数不得进入改名。命中判定必须用本矩形把 `RowPart::GROUP_COUNT`
|
||
* 从名称区域(`RowPart::BODY`)中切出来,否则两者会混在一起。
|
||
* @param [IN] row 行下标。
|
||
* @return 计数文本矩形;该行不是分组行时返回空矩形。
|
||
*/
|
||
gd::RectF GroupStatsRect(int row) const;
|
||
|
||
/**
|
||
* @brief 计算信号行网格徽标矩形。
|
||
* @param [IN] row 行下标。
|
||
* @return 徽标矩形。
|
||
*/
|
||
gd::RectF SignalBadgeRect(int row) const;
|
||
|
||
/**
|
||
* @brief 该信号是否**绘制**了网格徽标(唯一判据)。
|
||
*
|
||
* ⚠️ 绘制、命中测试、名称可用右界三处**必须共用本函数**:旧实现里命中只看
|
||
* "信号是否可见",而绘制还要求"已分配到网格" ⇒「可见但未分配网格」的信号在空白
|
||
* 徽标位置仍可点出「所属网格」菜单(点到的是不存在的徽标)。
|
||
*
|
||
* @param [IN] ref 信号引用。
|
||
* @return 已分配网格且在当前标签页可见时返回 true。
|
||
*/
|
||
bool SignalBadgeVisible(const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 计算信号行右侧「移除该信号」✕ 按钮的矩形。
|
||
*
|
||
* 位于信号条目的**最右端**,尺寸、样式与右边距与分组行的关闭 ✕ 完全一致,
|
||
* 并与它们在同一行内垂直居中(因此视觉上垂直对齐);网格徽标会让位到它的左侧。
|
||
*
|
||
* @param [IN] row 行下标。
|
||
* @return 按钮矩形;非信号行时返回空矩形。
|
||
*/
|
||
gd::RectF SignalRemoveRect(int row) const;
|
||
|
||
/**
|
||
* @brief 把后续行内绘制裁剪到指定行矩形(与列表矩形相交)。
|
||
*
|
||
* 行内容必须裁到行矩形内:折起动画中信号行会滑到组名栏之下,
|
||
* 不裁剪的话行内容会溢出到相邻行(或组名栏)上。
|
||
* @param [IN] rowRect 行矩形。
|
||
*/
|
||
void SetRowClip(const gd::RectF& rowRect)
|
||
{
|
||
m_rowClipActive = true;
|
||
m_rowClipRect = rowRect;
|
||
m_rowClipRect.Intersect(m_listRect);
|
||
}
|
||
|
||
/**
|
||
* @brief 把当前行裁剪矩形的**上边界**抬到给定值(折起动画中信号行“藏进组名栏”用)。
|
||
* @param [IN] top 新的裁剪上边界(屏幕 Y 坐标)。
|
||
*/
|
||
void ClampRowClipTop(float top)
|
||
{
|
||
if (!m_rowClipActive || top <= m_rowClipRect.Y)
|
||
{
|
||
return;
|
||
}
|
||
const float bottom = m_rowClipRect.Y + m_rowClipRect.Height;
|
||
m_rowClipRect.Y = top;
|
||
m_rowClipRect.Height = std::max(0.0f, bottom - top);
|
||
}
|
||
|
||
/** @brief 结束行内裁剪(恢复为整个列表矩形)。 */
|
||
void ClearRowClip() { m_rowClipActive = false; }
|
||
|
||
/**
|
||
* @brief 绘制分组标题行。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] row 行下标。
|
||
*/
|
||
void PaintGroupRow(gd::Graphics& g, int row);
|
||
|
||
/**
|
||
* @brief 绘制信号行。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] row 行下标。
|
||
*/
|
||
void PaintSignalRow(gd::Graphics& g, int row);
|
||
|
||
/**
|
||
* @brief 把一行(分组行 / 信号行)**绘制进离屏位图缓存**,再整块贴到列表上。
|
||
*
|
||
* ⚠️ 一行的绘制要调用十来个 GDI+ 原语(圆角托底 / 复选框 / 色块 / 两段文字…),
|
||
* 实测 **0.35ms/行**;列表一屏能放 35~40 行 ⇒ 单帧 **12~15ms** 全花在重复绘制
|
||
* **内容完全没变**的行上(分组折起/展开动画只是在整行平移)。
|
||
* 因此按"行的可见内容"做键缓存位图:动画帧只需把命中的位图按新位置贴出来。
|
||
*
|
||
* ⚠️ 缓存期间行的绘制**不能**裁到列表矩形(`m_rowClipRender`):位图是整行内容,
|
||
* 裁剪由贴图时的 GDI+ 裁剪区负责 —— 否则部分滚出上边界的行会永久丢掉顶部像素。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文(调用方已把裁剪区设为列表矩形)。
|
||
* @param [IN] row 行下标。
|
||
*/
|
||
void PaintRowCached(gd::Graphics& g, int row);
|
||
|
||
/**
|
||
* @brief 计算一行的缓存键(涵盖影响该行像素的全部状态)。
|
||
* @param [IN] row 行下标。
|
||
* @return 键值。
|
||
*/
|
||
std::uint64_t RowCacheKey(int row) const;
|
||
|
||
/** @brief 作废全部行位图缓存(行集合 / 字体 / 主题变化时调用)。 */
|
||
void InvalidateRowCache();
|
||
|
||
/** @brief 一行的离屏位图缓存项。 */
|
||
struct RowBitmapCache
|
||
{
|
||
/**
|
||
* @brief 该行的像素(DIB 内存 DC,已铺面板底色)。
|
||
*
|
||
* ⚠️ 必须是 **DIB + 内存 DC**,不能用 `gd::Bitmap`:GDI 在 GDI+ 位图的临时 HDC 上
|
||
* 会**关闭 ClearType**(退化为灰度抗锯齿),列表文字因此变细、观感与正文不一致
|
||
*(实测:整列文字丢失亚像素渲染)。DIB 内存 DC 上 ClearType 与屏幕一致。
|
||
*/
|
||
HDC dc = nullptr;
|
||
HBITMAP dib = nullptr;
|
||
std::uint64_t key = 0; /**< 内容键(不一致即重建)。 */
|
||
int width = 0; /**< 位图宽度(= 列表宽度)。 */
|
||
int height = 0; /**< 位图高度(= 行高)。 */
|
||
/**
|
||
* @brief 本行文本的**墨迹矩形**(位图坐标;调试用)。
|
||
*
|
||
* ⚠️ 必须缓存:命中缓存的帧根本不会调用 `UiText`,绘制期的文本收集器自然收不到
|
||
* 任何东西 —— 不缓存就会出现"第一帧有文本框、之后全部消失"。
|
||
*/
|
||
std::vector<gd::RectF> textRects;
|
||
};
|
||
|
||
std::vector<RowBitmapCache> m_rowBitmapCache; /**< 与 `m_rows` 同序的行位图缓存。 */
|
||
|
||
/**
|
||
* @brief 行位图缓存的**数量上限**(超过即整体作废重建)。
|
||
*
|
||
* 一行位图 ≈ 列表宽 × 行高 × 4B(430×30×4 ≈ 51KB)。用户滚动浏览上千行时,
|
||
* 缓存会按"画过的行下标"逐渐长大,必须有上限(这里 192 行 ≈ 10MB)。
|
||
*/
|
||
static constexpr std::size_t kRowBitmapCacheLimit = 192;
|
||
std::size_t m_rowBitmapLiveCount = 0; /**< 当前持有的行位图数量。 */
|
||
|
||
/** @brief 行绘制正被渲染进离屏位图(此时禁止按列表矩形裁剪)。 */
|
||
bool m_rowClipRender = false;
|
||
|
||
/**
|
||
* @brief 行位图缓存渲染期间的**几何平移量**(把整行搬到"位图坐标系")。
|
||
*
|
||
* ⚠️ 不能用 `gd::Graphics::TranslateTransform`:行内**文本走 GDI `DrawTextW`**,
|
||
* 而 GDI 文本**不遵循 GDI+ 的世界变换**(`Graphics::GetHDC` 拿到的 DC 只有裁剪区),
|
||
* 于是文字会被画到绝对坐标处、落到小位图之外(实测量产现象:行里只剩复选框/色块,
|
||
* 文字全部消失)。因此在**几何层**平移:`RowRect` 是行内一切矩形的唯一来源,
|
||
* 它统一减去该原点即可(`SignalBadgeRect` / ✕ / 名称框等全部自动跟随)。
|
||
*/
|
||
bool m_rowRenderActive = false;
|
||
float m_rowRenderOriginX = 0.0f; /**< 位图左上角(屏幕 x)。 */
|
||
float m_rowRenderOriginY = 0.0f; /**< 位图左上角(屏幕 y)。 */
|
||
|
||
/**
|
||
* @brief 判断哪个分组行需要「悬浮」在列表顶部(返回行下标,无需悬浮时返回 -1)。
|
||
*
|
||
* 判据:分组行已被滚出列表上边界(`RowRect(row).Y < m_listRect.Y`),
|
||
* 但该组内**仍有行留在列表里**(本组最后一行的下边界尚未越过列表上边界)。
|
||
* 两者同时成立时组名必须钉在最上方;整组都滚过去后组名跟随一起消失。
|
||
*
|
||
* @param [IN] rowCount 行总数。
|
||
* @return 需要悬浮的分组行下标;无需悬浮返回 -1。
|
||
*/
|
||
int StickyGroupRowIndex(int rowCount) const;
|
||
|
||
/**
|
||
* @brief 取某分组行的**最后一行**下标(列表行按分组连续排列)。
|
||
* @param [IN] row 分组行下标。
|
||
* @param [IN] rowCount 行总数。
|
||
* @return 本组最后一行下标(即下一个分组行的上一行)。
|
||
*/
|
||
int GroupLastRowIndex(int row, int rowCount) const;
|
||
|
||
/**
|
||
* @brief 计算分组行悬浮时应绘制到的**顶部坐标**(含平滑滚出)。
|
||
*
|
||
* 用户要求(2026-10-15):组名不能突然消失,也不能盖住下一个组名。公式:
|
||
* `min(列表上边界, 本组最后一行下边界 − 组名高度)` ⇒
|
||
* * 本组还有行在列表里时钉在列表上边界;
|
||
* * 本组最后一行开始滑出上边界时,组名跟着一起上移、随滚动条自然离界;
|
||
* * 悬浮组名的下边界永远不超过下一个分组行的自然位置 ⇒ 不会遮挡下一组。
|
||
*
|
||
* @param [IN] row 分组行下标。
|
||
* @param [IN] rowCount 行总数。
|
||
* @return 悬浮绘制的顶部 Y 坐标。
|
||
*/
|
||
float StickyGroupRowTop(int row, int rowCount) const;
|
||
|
||
/**
|
||
* @brief 把分组行「按悬浮位置」绘制(组名跟随滚动,直到整组滚过去)。
|
||
*
|
||
* ⚠️ 必须在常规行循环**之后**调用(它要盖在信号行之上),并且常规循环要**跳过**
|
||
* `row` 行,否则该行会被画两次。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] row 需要悬浮的分组行下标(< 0 表示什么都不做)。
|
||
*/
|
||
void PaintStickyGroupRow(gd::Graphics& g, int row);
|
||
|
||
/**
|
||
* @brief 在列表区内绘制行内文本(**显式裁剪到列表矩形 / 当前行矩形**)。
|
||
*
|
||
* ⚠️ `UiText` 走 GDI `DrawTextW`,**不遵循 GDI+ 的 `SetClip`**:列表需要滚动时,
|
||
* 被滚出视口的行(顶部部分可见的两三行)会把组名/信号名画到上方筛选框上。
|
||
* 因此**列表行**内文本一律经本函数绘制(见 `UiTextClipped`)。
|
||
* ⚠️ **样式区(筛选框上方之外的区域,如「目标」行)不得使用本函数** ——
|
||
* 它会把文本裁到 `m_listRect`(列表矩形之外的内容全都画不出来)。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] text 文本内容。
|
||
* @param [IN] font 字体句柄。
|
||
* @param [IN] rect 文本布局矩形。
|
||
* @param [IN] argb 文本颜色。
|
||
* @param [IN] align 水平对齐方式。
|
||
*/
|
||
void PaintRowText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect,
|
||
std::uint32_t argb, TextAlign align);
|
||
|
||
/**
|
||
* @brief 在列表区内填充行内矩形(**夹取到列表矩形**)。
|
||
*
|
||
* ⚠️ `UiFillRect` 对大面积的**不透明**矩形会改走 GDI `FillRect`(同样不受 GDI+ 裁剪约束),
|
||
* 高 DPI 下行的面积足以超过该阈值 ⇒ 行背景/选中底色必须先把矩形夹到列表矩形。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] rect 目标矩形。
|
||
* @param [IN] argb 填充颜色。
|
||
*/
|
||
void FillRowRect(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb);
|
||
|
||
/**
|
||
* @brief 绘制样式编辑区。
|
||
*
|
||
* 行序(下标从 0 起):「目标」/ 颜色 / 样式 / 调整 / 线宽 / 透明度,共 6 行;
|
||
* 各行控件共用同一内容列宽度与左边界。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintStyleSection(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 绘制列表滚动条。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintScrollBar(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 计算滚动条滑块矩形。
|
||
* @return 滑块矩形,无需滚动时返回空矩形。
|
||
*/
|
||
gd::RectF ScrollThumbRect() const;
|
||
|
||
/**
|
||
* @brief 面板的**客户区矩形**(宿主已告知客户区时用它,否则退化为自身区域)。
|
||
*
|
||
* 弹出菜单 / 拾色器需要"限制在窗口内"的边界;此前这段三元表达式在 6 处重复,
|
||
* 一旦判据变化就要全改,故收敛成一个函数。
|
||
*
|
||
* @return 客户区矩形(客户区坐标)。
|
||
*/
|
||
gd::RectF PanelBounds() const
|
||
{
|
||
return (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
|
||
}
|
||
|
||
/**
|
||
* @brief 布置样式编辑区内各控件的矩形。
|
||
*/
|
||
void LayoutStyleControls();
|
||
|
||
/**
|
||
* @brief 取得样式编辑区固定所需高度。
|
||
* @return 逻辑高度(未乘缩放比例)。
|
||
*/
|
||
float DesiredStyleHeight() const;
|
||
|
||
/**
|
||
* @brief 弹出信号右键菜单。
|
||
* @param [IN] ref 目标信号引用。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
*/
|
||
void OpenSignalMenu(const SignalRef& ref, int x, int y);
|
||
|
||
/**
|
||
* @brief 弹出分组右键菜单。
|
||
* @param [IN] groupIndex 目标分组下标。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
*/
|
||
void OpenGroupMenu(int groupIndex, int x, int y);
|
||
|
||
/**
|
||
* @brief 弹出网格归属菜单(由信号行的网格徽标触发)。
|
||
* @param [IN] ref 目标信号引用。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
*/
|
||
void OpenLaneMenu(const SignalRef& ref, int x, int y);
|
||
|
||
/**
|
||
* @brief 打开浮动拾色器(由自定义颜色入口触发)。
|
||
* @param [IN] style 当前信号样式。
|
||
* @return 打开成功返回 true。
|
||
*/
|
||
bool OpenColorPickerPopup(const SignalStyle& style);
|
||
|
||
/**
|
||
* @brief 打开「系数」的**二级面板**(一个整行输入框 + 标题栏右侧的还原按钮)。
|
||
*
|
||
* 与自定义颜色入口同一套交互:点击按钮 → 抬起时展开浮动面板 → 面板内的输入框可编辑
|
||
* 绘制前乘上的系数(默认 1,至少显示两位小数),回车 / 点击其它位置提交,面板保持打开
|
||
*(与网格 Y 轴上下限的菜单内联输入框一致)。“还原”图标按钮把系数重置为 1。
|
||
*
|
||
* @return 面板已打开返回 true。
|
||
*/
|
||
bool OpenGainMenu();
|
||
|
||
/**
|
||
* @brief 打开「镜像」的**二级面板**(两行带复选框的选项:水平镜像 / 垂直镜像)。
|
||
*
|
||
* 两项都是“切换项”,点击后勾选状态立即生效且**菜单保持打开**,方便连续勾选;
|
||
* 只要任一项被勾选,「镜像」按钮即显示为激活态。
|
||
*
|
||
* @return 面板已打开返回 true。
|
||
*/
|
||
bool OpenMirrorMenu();
|
||
|
||
/**
|
||
* @brief 清除样式区的悬停高亮。
|
||
*
|
||
* 二级面板(弹出菜单)一打开就会接管鼠标事件,样式区不再收到 `WM_MOUSEMOVE`,
|
||
* 因此必须在打开面板时主动清掉悬停态,否则该按钮会一直保持"鼠标按住"般的高亮。
|
||
*/
|
||
void ClearStyleHover();
|
||
|
||
/**
|
||
* @brief 判断当前标签页中全部信号是否均为可见状态。
|
||
* @return 全部可见且至少有一条信号返回 true。
|
||
*/
|
||
bool AreAllSignalsVisible() const;
|
||
|
||
/**
|
||
* @brief 统计全部信号数量。
|
||
* @return 信号总数。
|
||
*/
|
||
int TotalSignalCount() const;
|
||
|
||
/**
|
||
* @brief 进入指定字段的内联输入状态。
|
||
* @param [IN] field 目标字段。
|
||
* @param [IN] row 列表行下标(仅行内字段有效)。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool BeginEdit(StyleEditField field, int row = -1);
|
||
|
||
/**
|
||
* @brief 结束内联输入状态。
|
||
* @param [IN] commit 是否提交输入值。
|
||
* @return 状态或取值发生变化返回 true。
|
||
*/
|
||
bool EndEdit(bool commit);
|
||
|
||
/**
|
||
* @brief 计算当前输入框的矩形。
|
||
* @return 输入框矩形,未处于编辑状态时返回空矩形。
|
||
* @note 「目标」行编辑时需要按当前目标信号的名称 / GX 标签重算几何(名称宽度),
|
||
* 因此不是 const。
|
||
*/
|
||
gd::RectF EditFieldRect();
|
||
|
||
/**
|
||
* @brief 当前输入框绘制与命中/插入符换算所使用的字体。
|
||
* @return 字体句柄(分组名用 `uiSmall`,其余用 `ui`)。
|
||
* @note ⚠️ 绘制(`PaintGroupRow` / `PaintSignalRow`)与鼠标定位(`UiEditOffsetAt`)
|
||
* **必须使用同一个字体**,否则点哪儿插入符就落到哪儿这个不变量会被破坏。
|
||
*/
|
||
HFONT EditFieldFont() const;
|
||
|
||
/**
|
||
* @brief 内联名称编辑时的「吸收区」:输入框横向范围 × 整行高度。
|
||
* @param [IN] field 输入框矩形。
|
||
* @return 吸收区矩形(未在编辑行内时返回 `field`)。
|
||
* @note ⚠️ 行高大于输入框高度(上下各留 3 逻辑像素)。若不吸收这块区域,
|
||
* 鼠标落在内边距上会穿透成行点击 ⇒ 分组被展开/收起(用户 2026-10-19 报告)。
|
||
* 横向仍限定在名称区域内,同一行的复选框 / 「N 项」/ 徽标 / ✕ 继续可点。
|
||
*/
|
||
gd::RectF NameEditAbsorbRect(const gd::RectF& field) const;
|
||
|
||
/**
|
||
* @brief 计算「筛选信号」输入框的内容矩形(编辑与显示共用,保证不跳动)。
|
||
* @return 内容矩形。
|
||
*/
|
||
gd::RectF FilterFieldRect() const;
|
||
|
||
/**
|
||
* @brief 计算「筛选信号」输入框右侧清空按钮 ✕ 的矩形(无输入时为空矩形)。
|
||
* @return 清空按钮矩形。
|
||
*/
|
||
gd::RectF FilterClearRect() const;
|
||
|
||
/**
|
||
* @brief 清空筛选内容(点击 ✕ 时调用),并刷新列表。
|
||
*/
|
||
void ClearFilter();
|
||
|
||
/**
|
||
* @brief 计算指定行中名称区域的矩形(列表行内联编辑使用)。
|
||
* @param [IN] row 行下标。
|
||
* @return 名称区域矩形。
|
||
*/
|
||
gd::RectF RowNameRect(int row) const;
|
||
|
||
/**
|
||
* @brief 计算分组行中“组X”标签的像素宽度。
|
||
* @param [IN] row 行下标。
|
||
* @return 标签宽度。
|
||
*/
|
||
float GroupLabelWidth(int row) const;
|
||
|
||
/**
|
||
* @brief 分组行左侧「组X」标签的文本(绘制、命中、宽度共用同一来源)。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @return 形如 `组1` 的文本。
|
||
*/
|
||
static std::wstring GroupLabelText(int groupIndex);
|
||
|
||
/**
|
||
* @brief 分组行右侧项数文本(绘制、命中、宽度共用同一来源)。
|
||
* @param [IN] row 行下标。
|
||
* @return 形如 `3 项` 的文本;行无效时返回空串。
|
||
*/
|
||
std::wstring GroupStatsText(int row) const;
|
||
|
||
/**
|
||
* @brief 调整当前信号不透明度。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @return 取值发生变化返回 true。
|
||
*/
|
||
bool ApplyOpacityFromX(float x);
|
||
|
||
/**
|
||
* @brief 把当前目标信号的**线宽恢复为默认值**(「线宽」行右侧复位按钮)。
|
||
*
|
||
* 默认值与新建信号时同一来源(`DefaultSignalLineWidth(finiteCount)`):
|
||
* 普通信号 1.0 逻辑像素、**单数据点信号**取最大线宽(散点半径随线宽增长)。
|
||
* @return 取值发生变化返回 true。
|
||
*/
|
||
bool ResetStyleLineWidth();
|
||
|
||
/**
|
||
* @brief 把当前目标信号的**不透明度恢复为 100%**(「透明度」行右侧复位按钮)。
|
||
* @return 取值发生变化返回 true。
|
||
*/
|
||
bool ResetStyleOpacity();
|
||
|
||
/**
|
||
* @brief 断言某「样式」选项对当前目标信号是否可用(不可用时置灰且不可点)。
|
||
*
|
||
* ⚠️ **只有一个有效采样点**的信号固定使用「散点」:单个数据点在「线段」下根本画不出来,
|
||
* 而「点线」与「放大自动点线」机制语义重叠,因此这两个选项对单点信号置灰
|
||
*(用户 2026-10-16 要求)。
|
||
* @param [IN] optionIndex 选项下标(与 `kModeOptions` 同序)。
|
||
* @return 可用返回 true。
|
||
*/
|
||
bool ModeOptionEnabled(int optionIndex) const;
|
||
|
||
/**
|
||
* @brief 登记「点击信号 ⇒ 请宿主把绘图区滚动到该网格」的联动请求。
|
||
* @param [IN] ref 被点击的信号。
|
||
*/
|
||
void RequestPlotLaneScroll(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 依据线宽滑块横坐标调整当前信号线宽。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @return 取值发生变化返回 true。
|
||
*/
|
||
bool ApplyWidthFromX(float x);
|
||
|
||
HWND m_host = nullptr; /**< 宿主窗口句柄。 */
|
||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||
PopupMenu* m_menu = nullptr; /**< 共用弹出菜单。 */
|
||
ColorPickerPopup* m_colorPicker = nullptr; /**< 共用浮动拾色器。 */
|
||
const DataSet* m_dataSet = nullptr; /**< 数据集合(只读访问)。 */
|
||
DataSet* m_dataSetWritable = nullptr; /**< 数据集合(重命名等写入操作)。 */
|
||
TabConfig* m_tab = nullptr; /**< 当前标签页配置。 */
|
||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||
gd::RectF m_clientBounds; /**< 宿主客户区矩形。 */
|
||
|
||
gd::RectF m_area; /**< 面板整体区域。 */
|
||
gd::RectF m_headerRect; /**< 标题区域。 */
|
||
gd::RectF m_filterRect; /**< 列表上方的「筛选信号」输入框区域。 */
|
||
gd::RectF m_listRect; /**< 列表区域。 */
|
||
gd::RectF m_styleRect; /**< 样式编辑区域。 */
|
||
gd::RectF m_toggleAllRect; /**< 全部显示/隐藏按钮。 */
|
||
gd::RectF m_scrollBarRect; /**< 滚动条区域。 */
|
||
|
||
std::array<gd::RectF, 12> m_colorRects; /**< 预设颜色色块矩形。 */
|
||
int m_colorCount = 0; /**< 实际可用的预设颜色数量。 */
|
||
gd::RectF m_customChipRect; /**< 自定义颜色入口矩形(与预设色块同尺寸)。 */
|
||
|
||
std::array<gd::RectF, 3> m_modeRects; /**< 绘制样式按钮矩形。 */
|
||
|
||
// 样式区固定五行(不含顶部的「目标」行):颜色 / 样式 / 调整 / 线宽 / 透明度。
|
||
// 各行控件共用同一内容列宽度与左边界。
|
||
std::array<gd::RectF, 5> m_styleRowRects; /**< 五行整体矩形(含标签列)。 */
|
||
|
||
gd::RectF m_opacityTrackRect; /**< 不透明度滑块轨道矩形。 */
|
||
gd::RectF m_opacityValueRect; /**< 不透明度数值文本矩形。 */
|
||
gd::RectF m_widthTrackRect; /**< 线宽滑块轨道矩形。 */
|
||
gd::RectF m_widthValueRect; /**< 线宽数值文本矩形(双击可输入)。 */
|
||
/**
|
||
* @brief 线宽行右侧的**复位按钮**矩形(正方形,右边界与内容列右缘对齐)。
|
||
*
|
||
* 点击把该信号的线宽恢复为默认值(单数据点信号与普通信号的默认线宽不同)。
|
||
* 宽高为 0 表示当前不显示(没有目标信号)。
|
||
*/
|
||
gd::RectF m_widthResetRect;
|
||
/** @brief 透明度行右侧的**复位按钮**矩形(与 `m_widthResetRect` 同一列、同一尺寸)。 */
|
||
gd::RectF m_opacityResetRect;
|
||
/** 「调整」行的三个等宽按钮(下标 = 内部常量 `kAdjust*`):0 = 系数/偏移,1 = 镜像,2 = 绝对值。 */
|
||
std::array<gd::RectF, 3> m_adjustRects;
|
||
|
||
std::vector<ListRow> m_rows; /**< 列表行集合(惰性缓存)。 */
|
||
/**
|
||
* @brief 行顶前缀和:`m_rowTop[i]` = 第 0..i-1 行的高度之和(长度 = `m_rows.size() + 1`)。
|
||
*
|
||
* ⚠️ **性能硬约束**:`RowRect(row)` 原来每次调用都从头累加行高(O(row)),
|
||
* 而画一行要调它 5 次(✕ / 徽标 / 名称 / 组计数 / 折起三角各一次)。
|
||
* 200 行列表只看 30 行时每帧就是上万次加法,命中测试(每次鼠标移动)同样走这条路。
|
||
* 改成前缀和之后一次查表即可(O(1)),统一由 `UpdateAnimGeometry()` 重算。
|
||
*/
|
||
std::vector<float> m_rowTop;
|
||
bool m_rowsDirty = true; /**< 列表行集合是否需要重建。 */
|
||
float m_contentHeight = 0.0f; /**< 列表内容总高度。 */
|
||
float m_listFullWidth = 0.0f; /**< 列表区不含滚动条的完整宽度。 */
|
||
bool m_scrollBarVisible = false; /**< 是否需要显示滚动条。 */
|
||
|
||
float m_scrollOffset = 0.0f; /**< 列表滚动偏移量。 */
|
||
/**
|
||
* @brief 「组名悬浮」绘制用的行顶部坐标覆盖。
|
||
*
|
||
* 置位时 `RowRect` 忽略滚动偏移、直接返回 `m_rowYOverrideY` 作为行顶部,
|
||
* 从而使分组行(以及它派生的名称框 / ✕ / 徽标)整体画在钉住的位置。
|
||
* 只在使用期间短暂置位,绘制完立即复位(见 `PaintStickyGroupRow`)。
|
||
*/
|
||
bool m_rowYOverrideActive = false;
|
||
float m_rowYOverrideY = 0.0f;
|
||
|
||
bool m_draggingScrollBar = false; /**< 是否正在拖拽滚动条。 */
|
||
int m_scrollGrabOffset = 0; /**< 拖拽时鼠标相对滑块顶部的偏移。 */
|
||
bool m_draggingOpacity = false; /**< 是否正在拖拽不透明度滑块。 */
|
||
bool m_draggingWidth = false; /**< 是否正在拖拽线宽滑块。 */
|
||
int m_hoverRow = -1; /**< 当前悬停行。 */
|
||
RowPart m_hoverPart = RowPart::NONE; /**< 当前悬停的行内区域。 */
|
||
StyleHit m_hoverStyle = StyleHit::NONE; /**< 当前悬停的样式条目类型。 */
|
||
int m_hoverStyleIndex = -1; /**< 当前悬停的样式条目下标。 */
|
||
/**
|
||
* @brief 供绘图区联动高亮的悬停信号(列表信号行 / 样式区「目标」行)。
|
||
*
|
||
* 用户 2026-10-15 要求:鼠标悬停在信号列表里的某条信号(或样式区「目标」行的
|
||
* 信号名 / GX 标签)上时,若该信号已勾选绘制到某个网格,则该网格变色高亮、
|
||
* 且该信号曲线本身也高亮;鼠标移开即恢复。
|
||
*
|
||
* ⚠️ 不能用 `m_hoverRow` 现算:鼠标移出列表后它被清掉,而悬停可能仍在「目标」行上;
|
||
* 且列表行会在数据变化时重建,行下标会失效。
|
||
*/
|
||
SignalRef m_hoveredSignal;
|
||
/** 「目标」行(样式区最上方)的几何。 */
|
||
gd::RectF m_targetRowRect;
|
||
gd::RectF m_targetNameRect;
|
||
gd::RectF m_targetBadgeRect;
|
||
/** 「目标」行几何的缓存键(目标信号 + 所在网格 + 名称),避免每次鼠标移动都重算。 */
|
||
SignalRef m_targetGeomRef;
|
||
int m_targetGeomLane = -2; /**< -2 = 尚未计算过。 */
|
||
std::wstring m_targetGeomTitle;
|
||
bool m_targetGeomValid = false;
|
||
/** 滚动动画状态(见 `ScrollToSignalAnimated` / `AdvanceScrollAnimation`)。 */
|
||
bool m_scrollAnimActive = false;
|
||
float m_scrollAnimFrom = 0.0f;
|
||
float m_scrollAnimTo = 0.0f;
|
||
std::uint64_t m_scrollAnimStartMs = 0;
|
||
std::uint32_t m_scrollAnimDurationMs = 0;
|
||
|
||
/** @brief 正在进行的分组折起/展开动画(可同时有多个分组)。 */
|
||
std::vector<GroupAnim> m_groupAnims;
|
||
|
||
/**
|
||
* @brief 动画造成的逐行刚性位移(与 `m_rows` 同长;内容坐标,绘制时从行顶部减去)。
|
||
*
|
||
* 只有“动画分组的组名行之后”的行会被平移,因此组名本身始终留在原处,
|
||
* 信号行从它下面滑过去并被裁掉 —— 这就是“抽拉卷尺”观感的来源。
|
||
* 行高不变 ⇒ 动画期间无需重建行集合(性能关键)。
|
||
*/
|
||
std::vector<float> m_rowShift;
|
||
|
||
/**
|
||
* @brief 筛选模式下的分组折起状态(按分组下标)。
|
||
*
|
||
* 用户 2026-10-16 要求筛选结果也能以组为单位折起,但筛选只是一个临时视图,
|
||
* 因此使用与普通列表**分开**的一套状态,避免污染用户在普通列表里的布局选择。
|
||
* 表长不足 / 越界 = 展开(筛选结果默认展开,便于直接看到匹配项)。
|
||
*/
|
||
std::vector<bool> m_filterCollapsed;
|
||
|
||
/**
|
||
* @brief 当前正在绘制的行的裁剪矩形(用于折起动画中“藏进组名栏”的信号行)。
|
||
*
|
||
* `SetRowClip` 先把行内容裁到本行矩形(∩ 列表矩形),折起/展开动画中再用
|
||
* `ClampRowClipTop` 把上边界抬到组名栏的下边界 ⇒ 越过的部分被裁掉,
|
||
* 形成“滑进组名栏”的观感。
|
||
*/
|
||
bool m_rowClipActive = false;
|
||
gd::RectF m_rowClipRect;
|
||
|
||
StyleEditField m_editField = StyleEditField::NONE; /**< 当前处于输入状态的字段。 */
|
||
int m_editRow = -1; /**< 行内输入所作用的列表行下标。 */
|
||
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
|
||
|
||
bool m_filterEditing = false; /**< 「筛选信号」输入框是否处于编辑状态。 */
|
||
std::wstring m_filterText; /**< 当前筛选文本(空串表示不过滤)。 */
|
||
TextEdit m_filterEdit; /**< 筛选输入框的编辑模型。 */
|
||
|
||
/**
|
||
* @brief 设置筛选文本并立即生效(重建行集合、回到顶部)。
|
||
* @param [IN] text 新的筛选文本(空串表示不过滤)。
|
||
*/
|
||
void ApplyFilterText(const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 判断某信号的显示名是否匹配当前筛选表达式。
|
||
* @param [IN] signal 信号数据。支持 `*` 通配符,忽略大小写。
|
||
* @return 匹配(或未设置筛选)返回 true。
|
||
*/
|
||
bool FilterMatchesSignal(const SignalData& signal) const;
|
||
|
||
bool m_pendingLaneMenu = false; /**< 左键抬起时是否弹出网格归属菜单。 */
|
||
SignalRef m_pendingLaneRef; /**< 待弹出菜单的目标信号。 */
|
||
bool m_pendingColorPicker = false; /**< 左键抬起时是否弹出拾色器。 */
|
||
bool m_pendingGainMenu = false; /**< 左键抬起时是否弹出「系数」的二级面板。 */
|
||
bool m_pendingMirrorMenu = false; /**< 左键抬起时是否弹出「镜像」的二级面板。 */
|
||
bool m_pendingTargetLaneMenu = false; /**< 左键抬起时是否弹出「目标」行 GX 标签的网格菜单。 */
|
||
|
||
/** 待宿主处理的「把绘图区滚动到该网格」请求(见 `TakePlotLaneScrollRequest`)。 */
|
||
bool m_plotLaneRequestFlag = false;
|
||
int m_pendingPlotLane = -1;
|
||
|
||
/**
|
||
* @brief 待确认的「关闭类」按钮(✕)。
|
||
*
|
||
* 关闭类按钮统一改为**左键抬起**时生效:按下只记录目标,抬起时若指针仍在同一个
|
||
* ✕ 上才真正执行;按住后移开再松开视为放弃(与常见"可取消按钮"交互一致)。
|
||
*/
|
||
RowPart m_pendingClosePart = RowPart::NONE; /**< 待确认的 ✕ 所在行内区域(CLOSE / REMOVE_SIGNAL)。 */
|
||
|
||
/**
|
||
* @brief 最近一次「点击分组行名称区域」的时刻 / 行 / 点击前的折叠状态。
|
||
*
|
||
* 用户 2026-10-17 要求:分组**名称区域**单击也展开/收起(恢复),但**只有快速双击**
|
||
* 才进入改名编辑(降低灵敏度:较慢但明显不是双击的点击频率只当两次单击)。
|
||
* 因此需要记住上一次单击的信息,以便在双击时**撤销那一次折叠切换**再进入编辑。
|
||
*/
|
||
std::uint64_t m_groupNameClickMs = 0;
|
||
int m_groupNameClickRow = -1;
|
||
bool m_groupNameClickWasCollapsed = false;
|
||
int m_pendingCloseRow = -1; /**< 待确认的 ✕ 所在行下标。 */
|
||
bool m_pendingFilterClear = false; /**< 待确认的筛选框清空 ✕。 */
|
||
|
||
bool m_layoutDirty = false; /**< 是否需要重新布局。 */
|
||
|
||
SignalRef m_menuTargetSignal; /**< 菜单操作的目标信号。 */
|
||
int m_menuTargetGroup = -1; /**< 菜单操作的目标分组。 */
|
||
};
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __SIGNAL_PANEL_HPP__
|