Files
Just-DataInspector/Components/SignalPanel/Inc/SignalPanel.hpp
T
DeepSeek V4.1 Flash 9f6de3bba4 全局 Y 轴自适应跟随网格结构 + 组名悬浮 + 稀疏信号默认点线 + 单点光标吸附 + 放大自动点线
一、全局「Y 轴自适应」跟随网格结构
 * 新勾选信号产生的新网格现在会**继承**该开关:RebuildLanes 重建前先算
   previousAllAutoFit(非空且每个网格都开着 = 全局开启的正式定义),LaneCarry 加
   matched 标记,新出现的信号用它作为 fitYToSelection 的默认值。
 * 新增 PlotView::ReapplyGlobalAutoFitIfEnabled(),在 Layout()(网格数量变化 →
   Relayout)、RefreshLanes()(信号数量变化)、SplitLane() 之后立即重算一次。
   ⇒ 新勾选(按当前视界自适应)、多信号网格内取消显示某信号(剩余信号重新收敛)、
     拆分网格(每份各自自适应)三项都覆盖。

二、信号列表「组名悬浮」
 * 分组行被滚出列表上边界、而该组仍有信号留在列表里时,把分组行钉在列表顶部
   (不透明底 + 底部边界线);整组翻过去后本组组名消失,改由下一组悬浮。
 * 新增 SignalPanel::StickyGroupRowIndex() / PaintStickyGroupRow();RowRect 增加
   m_rowYOverrideActive/m_rowYOverrideY 覆盖行顶部坐标(RowRect 是行内名称框 / ✕ /
   徽标的唯一几何来源)。常规行循环跳过被钉住的那一行,避免同一行被画两次。

三、「少数数据点」信号的默认样式
 * 有效采样点 1 个 → 「点线」+ 最大线宽(10px);2 … 16 个 → 「点线」+ 2px;
   其余保持「线段」+ 1px。只在新建信号时生效(BuildSignalGroup 与
   InsertDerivedSignal),不覆盖用户设置。
 * 线宽范围常量统一到 DataModel.hpp(kSignalLineWidthMin/Max),SignalPanel 的线宽
   滑块改为引用它们(原本两处各自硬编码,容易漂移)。

四、单点信号的光标吸附(吸附而非卡住)
 * 旧逻辑要求时间轴至少 2 个采样点才能算「采样间距」,单点信号因此永远不吸附。
 * 新增 SignalData::singlePointTime(加载时与 ComputeStats 同趟扫描缓存该点时刻),
   SnapTimeToSample 新增半径分支(kCursorSnapPointRadiusPixels = 8 逻辑像素):
   半径内吸附到该点,半径外自由移动(移开即自由,不是把光标吸住)。

五、放大到「采样点稀疏」时自动启用点线
 * SignalStyle 新增运行态三字段:autoScatterActive / autoScatterSavedMode /
   autoScatterUserOverride。
 * AxisSparseInView()(采样点间距 > 半个绘图区宽度,与光标吸附上门限同源;单点时间轴
   不算稀疏)+ UpdateAutoScatterStyles():进入阈值保存原绘制方式并切「点线」,
   退出阈值(如适应视图)恢复;平移不改变缩放等级 ⇒ 结果稳定,不会样式抖动。
 * 自动启用期间用户手动改了绘制方式则退出时**不恢复**(尊重用户选择);该判定必须在
   恢复判断之前完成 —— 用户改完样式可能紧接着就退出阈值,否则会被覆盖。
 * 钩子:SetTimeView(视界唯一写入口)与 Layout / RefreshLanes / SplitLane。

六、验证(离屏 .preflight/check:SW_HIDE 窗口、不注入真实鼠标键盘、进程/线程 BELOW_NORMAL)
 65/65 通过(连跑 3 次稳定):[A1]/[A2]/[A3] 全局自适应、[B] 组名悬浮(含钉住行与自然
 位置逐像素一致、整组翻过后组名消失)、[C] 稀疏默认样式边界(1/2/5/16/17/1000 点)、
 [D] 单点吸附(半径内吸附、半径外自由、无残留粘滞)、[E] 自动点线(进入/退出/平移稳定/
 用户手动改过后不覆盖/再次进入再启用)。
 A/B:临时禁用六个机制后重跑 ⇒ 21 项如实失败(覆盖全部六项),证明断言有判别力。
 make release 零警告(用户正运行 Target.exe,链接用临时 -o 名字验证通过)。

七、规则同步
 Documents/agent_mem.md 新增 §9.29、§11.2 第 55–58 条硬约束与变更日志。
2026-10-15 14:10:13 +08:00

770 lines
27 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; }
/**
* @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);
private:
/**
* @brief 样式区与列表行中可进行内联文本编辑的字段。
*/
enum class StyleEditField : int
{
NONE = 0, /**< 未处于编辑状态。 */
WIDTH = 1, /**< 线宽数值。 */
OPACITY = 2, /**< 不透明度数值(百分比)。 */
SIGNAL_NAME = 3, /**< 信号显示名称(列表行内)。 */
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, /**< 「绝对值」切换按钮。 */
TOGGLE_ALL, /**< 全部显示/隐藏按钮。 */
FILTER_CLEAR /**< 「筛选信号」输入框右侧的清空 ✕。 */
};
/**
* @brief 列表中的一行(文件分组或信号)。
*/
struct ListRow
{
RowKind kind = RowKind::SIGNAL; /**< 行类型。 */
int groupIndex = -1; /**< 所属文件下标。 */
int signalIndex = -1; /**< 信号下标(仅信号行有效)。 */
float height = 0.0f; /**< 行高(像素)。 */
};
/**
* @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,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 把分组行「钉在列表顶部」绘制(组名跟随滚动,直到整组滚过去)。
*
* 用户要求长列表(例如上百个信号的 MF4)滚动时组名一直可见。
* ⚠️ 必须在常规行循环**之后**调用(它要盖在信号行之上),并且常规循环要**跳过**
* `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; /**< 当前悬停的样式条目下标。 */
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__