Files
Just-DataInspector/Components/SignalPanel/Inc/SignalPanel.hpp
T
DeepSeek V4.1 Flash f845616938 回退 5b52eef 并修复「屏幕不刷新」根因:拖动滚动条卡顿 / 悬停高亮卡住 / 信号拖拽无效果
根因(代码级确认)
- Render() 把界面画进后备缓冲区后用 GetDC + BitBlt 回送窗口,5b52eef 把回送写成
  `if (fullFrame) { BitBlt(整窗) } else if (panelOnly) { BitBlt(本帧重绘区域并集) }`,没有兜底 else。
- 「鼠标只在绘图区内移动」的帧(WM_MOUSEMOVE 的 plotOnlyMove 路径只 InvalidatePlotRect)恰好
  fullFrame == false && panelOnly == false ⇒ 后备缓冲区被正确重绘,窗口 DC 一个像素都不回送。
- 这一个根因同时造成用户报告的三个症状:悬停光晕卡住(移开不消失)、拖动滚动条不跟手/非常卡顿、
  拖拽信号换网格看不到任何结果。

修法
- git reset --hard HEAD~1 撤销 5b52eef(保留 backup-bad-commit 标签供 A/B)。
- 只挑回该提交里与更新区域无关的功能改动(用户明确要求过、本身正确):
  * SignalPanel::ExpandGroupForSignal():联动滚动前立刻展开折起的分组(不播展开动画);
  * FitAll() / FitSinglePointCluster() 不再复位 Tab().LaneScroll()(适应视图不影响滚动条);
  * 右下角「回到最上方」浮动按钮 + 网格滚动到可视区的平滑动画。
- Application.cpp 只保留 3 处最小改动:Render 里把 panelAnimating 计入整帧脏、WM_TIMER 推进
  AdvanceBackToTopAnimation() 且保持整窗失效、定时器存活判定带 BackToTopAnimationActive()。
  Render 的绘制与 BitBlt 路径逐字节等同于 82b5ebe。
- 彻底删除 m_paintPlot / m_panelDirty / panelOnly / InvalidatePanelRect / 并集回送。

验证(全部离屏执行,不移动真实光标、不抢焦点、不截屏)
- .preflight/diag(新增离屏探针,19/19):窗口移到屏幕外 + WS_EX_TOOLWINDOW + SW_SHOWNOACTIVATE,
  保证有真实更新区域与 WM_PAINT 但不影响用户。断言滚动条拖动(滑块与绘图区都在后备缓冲区被重绘)、
  悬停光晕出现与消退(像素完全回到基线)、在曲线上按下进入 MOVE_SIGNAL 且拖到另一网格后归属正确。
- .preflight/check 137/137。
- .preflight/diag/verify_blit.ps1(新增源码级护栏,10/10):断言 Render() 的回送块必须有无条件 else、
  不得出现 else if、m_paintPlot/m_panelDirty/panelOnly 必须不存在、失效只允许整窗或仅绘图区。
  A/B:换回 backup-bad-commit ⇒ 4 项如实失败;换回修复版 ⇒ 10/10。
- make release 零错误零警告。

文档:Documents/agent_mem.md 新增 §9.41、§11.2 第 108–116 条(含 §9.40 与旧 108–114 条的作废标注)
与变更日志一行。

教训(写进记忆):① 离屏探针无法验证「窗口回送 / 合成」(屏幕外 DWM 不合成、窗口 DC 恒为空白)⇒
回送类约束必须有源码级护栏;② 离屏窗口 + TrackMouseEvent 会立刻收到 WM_MOUSELEAVE,探针须丢弃该消息;
③ 网格回收会让下标左移,断言要用锚信号。
2026-10-16 17:33:08 +08:00

1226 lines
47 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 __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"
namespace jdi
{
/**
* @brief 信号面板弹出菜单的命令标识。
*/
enum MenuCommand : int
{
CMD_SIGNAL_TOGGLE = 1, /**< 切换该信号可见性。 */
CMD_SIGNAL_SOLO = 2, /**< 仅显示该信号。 */
CMD_SIGNAL_SHOW_ALL = 3, /**< 显示全部信号。 */
CMD_SIGNAL_HIDE_ALL = 4, /**< 隐藏全部信号。 */
CMD_GROUP_SHOW_ALL = 10, /**< 选中该分组全部信号。 */
CMD_GROUP_HIDE_ALL = 11, /**< 取消该分组全部信号。 */
CMD_GROUP_REMOVE = 12, /**< 从工作区移除该分组。 */
CMD_COLOR_BASE = 100, /**< 预设颜色基准(配合色块下标)。 */
CMD_MODE_BASE = 200, /**< 绘制样式基准(配合下标)。 */
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:
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();
}
/**
* @brief 设置 DPI 缩放比例。
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
*/
void SetScale(float scale);
/**
* @brief 按给定区域重新排布面板内容。
* @param [IN] area 面板可用区域。
*/
void Layout(const gd::RectF& area);
/**
* @brief 绘制面板。
* @param [IN,OUT] g 绘图上下文。
*/
void Paint(gd::Graphics& g);
/**
* @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 处理鼠标右键按下(弹出上下文菜单)。
* @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();
/**
* @brief 处理弹出菜单的选择结果。
* @param [IN] result 菜单选择结果。
* @return 需要重新布局返回 true。
*/
bool OnMenuCommand(const MenuResult& result);
/**
* @brief 判断面板是否处于内联文本输入状态。
* @return 正在输入返回 true。
*/
bool IsEditing() const { 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;
/**
* @brief 查询该位置应使用的鼠标光标(✕ 类按钮使用手型)。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 需要特殊光标时返回光标句柄,否则返回 nullptr。
* @note 需要按需重建列表行缓存(查 ✕ 的命中),因此不是 const。
*/
HCURSOR QueryCursor(int x, int y);
/**
* @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」标签(与折叠三角一样可点击折起)。 */
CLOSE, /**< 分组行的关闭按钮。 */
LANE_BADGE, /**< 信号行的网格徽标。 */
REMOVE_SIGNAL /**< 信号行左侧的「移除该信号」✕ 按钮。 */
};
/**
* @brief 样式编辑区中的可交互条目类型。
*/
enum class StyleHit : int
{
NONE = 0, /**< 未命中。 */
COLOR, /**< 预设颜色色块。 */
CUSTOM_CHIP, /**< 自定义颜色入口。 */
MODE, /**< 绘制样式按钮。 */
OPACITY_SLIDER, /**< 不透明度滑块。 */
OPACITY_VALUE, /**< 不透明度数值框(可点击输入)。 */
WIDTH_SLIDER, /**< 线宽滑块。 */
WIDTH_VALUE, /**< 线宽数值框(可点击输入)。 */
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 计算信号行网格徽标矩形。
* @param [IN] row 行下标。
* @return 徽标矩形。
*/
gd::RectF SignalBadgeRect(int row) 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 判断哪个分组行需要「悬浮」在列表顶部(返回行下标,无需悬浮时返回 -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 绘制样式编辑区(固定四行:颜色 / 线宽 / 样式 / 透明度)。
* @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 布置样式编辑区内各控件的矩形。
*/
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 内容矩形。
*/
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 断言某「样式」选项对当前目标信号是否可用(不可用时置灰且不可点)。
*
* ⚠️ **只有一个有效采样点**的信号固定使用「散点」:单个数据点在「线段」下根本画不出来,
* 而「点线」与「放大自动点线」机制语义重叠,因此这两个选项对单点信号置灰
*(用户 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; /**< 线宽数值文本矩形(双击可输入)。 */
/** 「调整」行的三个等宽按钮:0 = 系数调整,1 = 取绝对值,2 = 中心反转。 */
std::array<gd::RectF, 3> m_adjustRects;
std::vector<ListRow> m_rows; /**< 列表行集合(惰性缓存)。 */
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)。 */
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__