Files
Just-DataInspector/Components/SignalPanel/Inc/SignalPanel.hpp
T
DeepSeek V4.1 Flash 1d314ddc24 信号面板分组折起动画 + 筛选态折起(项数按筛选计数)+ 悬浮组名可交互与“先滚动再折起” + 适应视图与 Y 轴自适应同源
① 分组折起/展开改为动画过渡(用户要求:被折起的信号向上收起、下方组随之上升、
   离组名越远的成员收起越快、末段减速、总时长合理):
   - 新增 SignalPanel::m_groupAnims + GroupRowScale():每行速度倍率
     1 + 1.2×ordinal/(行数−1)(越靠下越快收尽),叠加 ease-out cubic;
     时长 = clamp(115 + 14×行数, 140, 300) ms。
   - BuildRows 按比例输出非整行高;低于 0.75 逻辑 px 的行不再产生(不绘制/不命中)。
   - 宿主 WM_TIMER(kScrollTimerId) 同时驱动滚动与折起动画;Render 的定时器判定改为
     ScrollAnimationActive() || GroupAnimationActive()。
   - 新增 SetRowClip/ClearRowClip:动画期间行内容裁到本行矩形(否则 DT_VCENTER
     垂直居中的文本会溢出到相邻行)。

② 筛选状态下的分组折起 + 项数按筛选结果计数:
   - 筛选模式改用独立的 m_filterCollapsed(默认展开),不再“筛选时忽略折叠状态”,
     也不再把筛选里的临时折起污染到普通列表的布局选择。
   - ListRow::displayCount 记录当前模式下列出的信号数;组名后方「N 项」只读它
     (筛选时 = 该组内符合筛选条件的信号个数)。
   - 筛选文本变化 / 清空筛选 / 数据变化 ⇒ 清空临时折起状态 + 进行中的动画 + 挂起请求。

③ 悬浮组名可正常交互 + 点击时“先滚动就位再折起”:
   - 根因:HitRow 只按滚动偏移把指针映射到内容位置,而悬浮组名是画在列表顶部的覆盖层
     (其自然位置已滚出列表)⇒ 点击穿透到它下面那一行(折叠三角 / 复选框 / ✕ /
     名称区域全都点不动)。
   - 修法:HitRow 先用 StickyGroupRowIndex() / StickyGroupRowTop() 判定是否落在悬浮行
     矩形内,命中则临时打开 m_rowYOverrideActive 并调用新的 RowHitPart() 算行内区域;
     同时跳过高度为 0 的行(折起动画中已收尽,否则空行会白白吸走一次点击)。
   - 新增 ToggleGroupFromRow():若该行正悬浮(滚动偏移大于该行的内容偏移),先
     StartScrollAnimationTo(该行内容偏移, groupIndex) 并把挂起请求记在
     m_pendingGroupToggle,滚动到位后才启动折起动画;分组行的单击与双击折叠三角都走
     这个入口。滚轮 / 拖滚动条 / 筛选变化 / 切标签页一律清掉挂起请求。

④ 「适应视图」与「Y 轴自适应」彻底同源(单数据点信号):
   - 根因:FitSinglePointCluster 给每个网格另算一套幅值区间(“散点半径留白 + 跨度 1”),
     而 AutoRangeLaneToView 走 PadValueRange ⇒ 同一视界下两者结果不同;且它显式写
     lane.fitYToSelection = false ⇒ 点「适应视图」会把「Y 轴自适应」悄悄关掉。
   - 修法:单点簇只负责时间轴(以该时刻居中 + 一大格 1s),逐网格幅值轴与常规路径 /
     Y 轴自适应完全同源(fitYToSelection → AutoRangeLaneToView,否则 autoRange = true,
     两者之后都接 ApplySinglePointValueMargins),并且绝不改写 fitYToSelection。
   - 有意的行为变化:单点信号「适应视图」的幅值跨度由旧的 1 变为 2×max(1,|值|×0.1)
     (即与 PadValueRange 一致);§9.33 用例 [A] 里“幅值跨度 1”的期望已作废,
     散点仍在网格正中且完整可见(ApplySinglePointValueMargins 兜底)。

验证(离屏 SW_HIDE,不占用鼠标 / 键盘 / 屏幕;进程与线程均 BELOW_NORMAL):
   - 新增 .preflight/perf/panelanim_main.cpp(panelanim.exe)94/94 通过(连跑 3 次稳定):
     折起/展开的分帧过渡与内容高度单调收敛、越靠下的行越矮、末态精确无残留、
     筛选计数与独立折起状态、悬浮组名四个可交互区域全部命中且下层行仍可点击、
     点击后“先滚动到位再折起”、真实 MF4-Record.MF4 单点信号下两种自适应逐位一致且往返稳定。
   - A/B 三组回退(回退绘图区修复 / 禁用悬浮命中优先 / 动画时长置 0)分别 4 / 13 / 8 项如实失败。
   - 既有 verify 49/49、datacheck 69/69、edgecheck 46/46;make clean && make all 零警告。
     (顺带修正本机 verify 探针 4 条与远端新规则不符的过期断言:幅值留白改为调用唯一的
     PadValueRange、派生信号 offsetTime 断言 0(§9.21 已烘入数据)、拖拽换位按“被拖网格
     自身边界”判据的新目标值。)

记忆:Documents/agent_mem.md 新增 §9.35 与 §11.2 第 76–81 条,并更新第 70 条与 §9.33 备注。
2026-10-16 01:25:54 +08:00

1080 lines
40 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_pendingGroupToggle = -1;
}
/**
* @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 向当前输入框送入一个字符。
* @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 推进一步滚动动画(由宿主定时器驱动)。
* @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` 表示样式区「目标」行)。 */
FILE_NAME = 4 /**< 分组显示名称(列表行内)。 */
};
/**
* @brief 列表行的类型。
*/
enum class RowKind : int
{
GROUP = 0, /**< 文件分组标题行。 */
SIGNAL = 1 /**< 信号行。 */
};
/**
* @brief 行的可交互区域。
*/
enum class RowPart : int
{
NONE = 0, /**< 未命中。 */
DISCLOSURE, /**< 折叠三角。 */
CHECK, /**< 复选框。 */
BODY, /**< 行主体。 */
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 要求:折起分组时不能下一帧立刻切换布局,而要有一段过渡动画 ——
* 被折起的信号向上收起、下方的分组随之上升;**离组名越远的成员收起越快**,
* 随后整体减速直到收起完成;总时长控制在一个合理范围(百余毫秒到 300ms)。
*
* 动画期间 `BuildRows` 会按 `GroupRowScale()` 给出每行的收缩比例,
* 因此列表内容高度连续变化、下方分组平滑上升。
*/
struct GroupAnim
{
int groupIndex = -1; /**< 目标分组下标。 */
bool collapsing = false; /**< true = 正在折起,false = 正在展开。 */
int rowCount = 0; /**< 该分组参与动画的信号行数。 */
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 目标滚动偏移(内部会夹取到合法范围)。
* @param [IN] pendingGroupToggle 滚动到位后要折起/展开的分组下标(-1 = 无)。
*/
void StartScrollAnimationTo(float desired, int pendingGroupToggle = -1);
/**
* @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 启动某分组的折起/展开动画。
* @param [IN] groupIndex 分组下标。
* @param [IN] collapsing 是否折起。
* @param [IN] rowCount 该分组参与动画的信号行数。
*/
void StartGroupAnimation(int groupIndex, bool collapsing, int rowCount);
/**
* @brief 取某分组动画中第 ordinal 行的收缩比例。
*
* 规则(用户 2026-10-16 要求):**离组名越远的成员收起速度越快**,整体减速(ease-out)
* 直到完成;因此越靠下的行越早消失,紧接着下方分组平滑上升。
*
* @param [IN] groupIndex 分组下标。
* @param [IN] ordinal 该行在分组内「参与列出」的序号(0 = 紧邻组名的第一行)。
* @param [IN] rowCount 该分组参与动画的行数。
* @return 1 = 完整高度,0 = 完全收起。
*/
float GroupRowScale(int groupIndex, int ordinal, int rowCount) const;
/**
* @brief 切换分组折起状态并播放动画(不处理“悬浮先滚动”的前置动作)。
* @param [IN] groupIndex 分组下标。
*/
void ToggleGroupCollapsedAnimated(int groupIndex);
/**
* @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 重新计算「目标」行内信号名与 GX 标签的矩形。
*
* 名称宽度依赖文本实际宽度、GX 标签是否出现依赖该信号是否已绘制到网格 —— 两者都可能在
* 不重新布局的情况下变化(切换选中、勾选/取消勾选),因此用一个小缓存键避免重算。
*/
void UpdateTargetRowGeometry();
/**
* @brief 绘制「目标」行(样式区最上方:名称 + GX 标签,可悬停 / 双击改名)。
* @param [IN,OUT] g 绘图上下文。
*/
void PaintTargetRow(gd::Graphics& g);
/** @brief 取消正在进行的滚动动画(用户主动滚动时调用)。 */
void CancelScrollAnimation()
{
m_scrollAnimActive = false;
// 挂起的“滚动到位后折起”也必须取消:用户已经自己滚动了,
// 再补一次折起会让人莫名其妙。
m_pendingGroupToggle = -1;
}
/**
* @brief 重新构建列表行集合。
*/
void BuildRows();
/**
* @brief 在需要时重建列表行集合。
*/
void EnsureRows();
/**
* @brief 依据内容高度重算滚动条可见性与列表可用宽度。
*/
void RefreshScrollBar();
/**
* @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 FileCloseRect(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 结束行内裁剪(恢复为整个列表矩形)。 */
void ClearRowClip() { m_rowClipActive = false; }
/**
* @brief 绘制文件分组标题行。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] row 行下标。
*/
void PaintFileRow(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`)。
*
* @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 OpenFileMenu(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 输入框矩形,未处于编辑状态时返回空矩形。
*/
gd::RectF EditFieldRect() 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 FileGroupLabelWidth(int row) const;
/**
* @brief 调整当前信号不透明度。
* @param [IN] x 客户区横坐标。
* @return 取值发生变化返回 true。
*/
bool ApplyOpacityFromX(float x);
/**
* @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 滚动到位后要折起/展开的分组下标(-1 = 无)。
*
* 悬浮组名上点击折叠时,必须先滚动到该组名完整可见,再播放折起动画
*(用户 2026-10-16 要求)。
*/
int m_pendingGroupToggle = -1;
/** @brief 正在进行的分组折起/展开动画(可同时有多个分组)。 */
std::vector<GroupAnim> m_groupAnims;
/**
* @brief 筛选模式下的分组折起状态(按分组下标)。
*
* 用户 2026-10-16 要求筛选结果也能以组为单位折起,但筛选只是一个临时视图,
* 因此使用与普通列表**分开**的一套状态,避免污染用户在普通列表里的布局选择。
* 表长不足 / 越界 = 展开(筛选结果默认展开,便于直接看到匹配项)。
*/
std::vector<bool> m_filterCollapsed;
/**
* @brief 当前正在绘制的行的裁剪矩形(用于折起动画中的部分高度行)。
*
* 行高在折起动画期间连续缩小,若不把行内容裁剪到行矩形内,
* 文本会溢出到相邻行上(`UiText` 是 `DT_VCENTER` 垂直居中,
* 在变矮的矩形里会被裁掉上下两半,这正是想要的“挤压”观感)。
*/
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; /**< 左键抬起时是否弹出「镜像」的二级面板。 */
/**
* @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_menuTargetFile = -1; /**< 菜单操作的目标文件。 */
};
} // namespace jdi
#endif // __SIGNAL_PANEL_HPP__