一、采样间隔/频率显示不一致(用户报告 1ms vs 1000us、100us 0ns) * FormatDurationCompound 改为「先量化到整数皮秒、再整数分解」, 彻底消除同一数值因浮点噪声落到不同单位/留下零尾数的问题; 秒级分支与亚秒分支共用 totalPs 方案,次级进位/单位升级集中在一处。 * FormatFrequency 的单位选择改为按「舍入后的显示值」判定(999.9999999Hz 与 1000Hz 均为 1.000kHz),并做向下校正避免 0.001kHz。 二、选区截取面板 * 提示文本改为用户指定原文「启用双光标模式以进行选区截取」。 * EdgePanelWidth 计入该提示的实际文本宽度;绘制改用 UiTextClipped 不再省略。 * viewHeight 补回漏减的 bottomPad,修掉条目区与按钮之间的空白。 三、未展开的信号条目栏悬停不再变成手型光标。 四、开启「显示布局边界」后 GX 标签可正常拖拽排序 * 抽出 PaintLaneReorderOverlay(吸附槽位 + 被拖轮廓),贴图与整幅重绘共用。 * 整幅重绘在 LANE_REORDER 时按 LaneDragOffset 逐格平移重画(被拖网格最后画), 修掉调试模式下不跟随鼠标、无吸附提示、只能盲拖的问题。 五、信号右键新增「移动到…」二级菜单(位于「仅显示该信号」之上) * 条目为「分组 N(组名)」,当前组打勾;无标题行;底部「新建分组」。 * MenuItem::submenuMaxRows + PopupMenu 独立子菜单滚动(滚动条、滚轮、视口命中过滤), IsScrollable 计入子菜单可滚状态。 * 数据层 DataSet::MoveSignalToGroup/MoveSignal 让时间轴随信号搬运 (独占则移动、共享则复制);TabConfig::RemapAfterSignalMove 改写引用而非先删后插。 * Application::MoveSignalCore 统一入口,保证派生计算暂停、引用修正、选中迁移与同步一致。 六、信号列表支持按住拖拽排序(组内重排 / 跨组移动) * 4px 阈值进入拖拽;条目脱离原位跟随鼠标(阴影 + 强调色描边 + 插入指示线)。 * 其它信号行/分组行按插入位置做避让位移动画(RowRect 统一叠加插值位移, 宿主 kScrollTimerId 驱动 AdvanceSignalDragAnimation)。 * 数据层 DataSet::MoveSignal 支持任意位置;TabConfig::RemapAfterSignalReorder 线性重映射。 * 拖拽中禁用组名悬浮;双击改名与 InvalidateRows 均正确终止拖拽态。 验证:删 .o 全量重编译零 error / 零 warning;产物 873,984 B(< 1MB 预警阈值)。 本轮未使用鼠标/键盘/屏幕,未运行任何 GUI 探针,仅编译验证 + 源码级核对。
1318 lines
43 KiB
C++
1318 lines
43 KiB
C++
// SignalPanel 实现分片 —— 交互(命中、鼠标/键盘派发、光标形状)。
|
||
//
|
||
// 共用的常量与工具函数在 SignalPanelInternal.hpp(命名空间 `spdetail`,全部 inline)。
|
||
// 六个分片按**职责**划分(不再按函数名前缀):
|
||
// SignalPanel.cpp 核心:状态、行集合构建、布局与几何、分组/信号可见性
|
||
// SignalPanelPaint.cpp 绘制:列表行(含行位图缓存)、样式区、筛选框、滚动条
|
||
// SignalPanelInteraction.cpp 交互:命中测试、鼠标/键盘派发、光标形状(**本文件**)
|
||
// SignalPanelEdit.cpp 内联编辑:筛选框、组名/信号名、数值输入、颜色与透明度
|
||
// SignalPanelScroll.cpp 动画与滚动:分组折起、联动滚动、"滚动到目标信号"
|
||
// SignalPanelMenu.cpp 弹出菜单:系数/偏移、镜像、右键菜单与命令回执
|
||
#include "SignalPanelInternal.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
|
||
|
||
int SignalPanel::HitRow(int x, int y, RowPart& part)
|
||
{
|
||
part = RowPart::NONE;
|
||
EnsureRows();
|
||
|
||
if (m_listRect.Height <= 0.0f)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
if (fy < m_listRect.Y || fy > m_listRect.Y + m_listRect.Height)
|
||
{
|
||
return -1;
|
||
}
|
||
if (fx < m_listRect.X || fx > m_listRect.X + m_listRect.Width)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
// 「悬浮组名」绘制在最上层,因此命中判定也必须最优先 —— 否则点击会**穿透到
|
||
// 它下面的那一行**(用户 2026-10-16 报告:组名悬浮时它上面的折叠三角、复选框、
|
||
// 关闭 ✕、名称区域全都点不动,点击被下层信号行吃掉)。
|
||
const int rowCount = static_cast<int>(m_rows.size());
|
||
const int stickyRow = StickyGroupRowIndex(rowCount);
|
||
if (stickyRow >= 0)
|
||
{
|
||
const float stickyTop = StickyGroupRowTop(stickyRow, rowCount);
|
||
const float stickyHeight = m_rows[static_cast<std::size_t>(stickyRow)].height;
|
||
if (fy >= stickyTop && fy < stickyTop + stickyHeight)
|
||
{
|
||
// 用覆盖几何计算行内区域(`RowRect` 是行内所有派生的唯一几何来源)。
|
||
m_rowYOverrideActive = true;
|
||
m_rowYOverrideY = stickyTop;
|
||
part = RowHitPart(stickyRow, fx, fy);
|
||
m_rowYOverrideActive = false;
|
||
return stickyRow;
|
||
}
|
||
}
|
||
|
||
// 逐行比较“实际绘制位置”(含折起/展开动画的刚性平移),不能只看滚动偏移:
|
||
// 动画中信号行的位置与“自然内容坐标”不再一一对应,用后者会点错行。
|
||
float rowTop = m_listRect.Y - m_scrollOffset;
|
||
for (std::size_t index = 0; index < m_rows.size(); ++index)
|
||
{
|
||
const float shift = (index < m_rowShift.size()) ? m_rowShift[index] : 0.0f;
|
||
const float previousShift =
|
||
(index > 0 && (index - 1) < m_rowShift.size()) ? m_rowShift[index - 1] : 0.0f;
|
||
rowTop -= (shift - previousShift);
|
||
|
||
const ListRow& row = m_rows[index];
|
||
const float rowBottom = rowTop + row.height;
|
||
if (row.height > 0.0f && fy >= rowTop && fy < rowBottom)
|
||
{
|
||
const int rowIndex = static_cast<int>(index);
|
||
// 折起/展开动画中已“藏进组名栏”的行不再参与命中(否则它会白白吸走一次点击)。
|
||
if (rowBottom > AnimationHideTopForRow(rowIndex, stickyRow) + 0.5f)
|
||
{
|
||
part = RowHitPart(rowIndex, fx, fy);
|
||
return rowIndex;
|
||
}
|
||
return -1;
|
||
}
|
||
rowTop = rowBottom;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
bool SignalPanel::OnMouseMove(int x, int y)
|
||
{
|
||
// 拖拽优先级最高。
|
||
if (m_draggingOpacity)
|
||
{
|
||
return ApplyOpacityFromX(static_cast<float>(x));
|
||
}
|
||
if (m_draggingWidth)
|
||
{
|
||
return ApplyWidthFromX(static_cast<float>(x));
|
||
}
|
||
// 信号条目拖拽:按下后越过阈值才真正进入拖拽态(之前只是"可能是点击")。
|
||
if (m_signalDragPending || m_signalDragging)
|
||
{
|
||
const int dx = x - m_signalDragStartX;
|
||
const int dy = y - m_signalDragStartY;
|
||
if (!m_signalDragging && (dx * dx + dy * dy) >= 16)
|
||
{
|
||
m_signalDragging = true;
|
||
m_signalDragShift.assign(m_rows.size(), 0.0f);
|
||
m_signalDragShiftCur.assign(m_rows.size(), 0.0f);
|
||
m_hoverRow = -1;
|
||
m_hoverPart = RowPart::NONE;
|
||
}
|
||
if (m_signalDragging)
|
||
{
|
||
UpdateSignalDrag(x, y);
|
||
return true;
|
||
}
|
||
return false; // 仍在阈值内:按普通移动继续处理(悬停高亮等)
|
||
}
|
||
if (m_draggingScrollBar)
|
||
{
|
||
const gd::RectF thumb = ScrollThumbRect();
|
||
// ⚠️ 拖拽期间滑块高度不会变(内容高度与列表高度都没变),因此 `thumb.Height` 可安全
|
||
// 用于本次换算;但 `m_scrollBarRect.Y` 必须取**按下时**记录的值(局部刷新/重排可能
|
||
// 改掉它)。坐标一律用**客户区** y,因此鼠标移到面板外(如绘图区)也能继续跟随。
|
||
const float travel = static_cast<float>(y - m_scrollGrabOffset) - m_scrollBarRect.Y;
|
||
const float usable = m_scrollBarRect.Height - thumb.Height;
|
||
if (usable > 0.0f)
|
||
{
|
||
const float progress = std::max(0.0f, std::min(1.0f, travel / usable));
|
||
m_scrollOffset = progress * (m_contentHeight - m_listRect.Height);
|
||
ClampScroll();
|
||
}
|
||
return true;
|
||
}
|
||
|
||
RowPart part = RowPart::NONE;
|
||
const int hoverRow = HitRow(x, y, part);
|
||
|
||
StyleHit hoverStyle = StyleHit::NONE;
|
||
int hoverStyleIndex = -1;
|
||
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
|
||
if (UiPointInRect(m_toggleAllRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::TOGGLE_ALL;
|
||
}
|
||
else if (UiPointInRect(FilterClearRect(), fx, fy))
|
||
{
|
||
// 筛选框右侧的清空 ✕(仅在筛选框有内容时存在)。
|
||
hoverStyle = StyleHit::FILTER_CLEAR;
|
||
}
|
||
else if (m_tab != nullptr && StyleOf(SelectedRef()) != nullptr)
|
||
{
|
||
// 「目标」行的信号名 / GX 标签(优先于其余样式控件:它们几何不重叠,
|
||
// 但显式放在最前更易读)。
|
||
UpdateTargetRowGeometry();
|
||
if (UiPointInRect(m_targetRowRect, fx, fy))
|
||
{
|
||
if (m_targetBadgeRect.Width > 0.0f && UiPointInRect(m_targetBadgeRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::TARGET_BADGE;
|
||
}
|
||
else if (m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::TARGET_NAME;
|
||
}
|
||
}
|
||
for (int index = 0; hoverStyle == StyleHit::NONE && index < m_colorCount; ++index)
|
||
{
|
||
if (UiPointInRect(m_colorRects[static_cast<std::size_t>(index)], fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::COLOR;
|
||
hoverStyleIndex = index;
|
||
break;
|
||
}
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_customChipRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::CUSTOM_CHIP;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE)
|
||
{
|
||
for (int index = 0; index < kModeOptionCount; ++index)
|
||
{
|
||
// 置灰的选项(单点信号的「线段」/「点线」)不参与悬停高亮 ——
|
||
// 它们点不动,高亮会让用户以为可以点击。
|
||
if (ModeOptionEnabled(index) && UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::MODE;
|
||
hoverStyleIndex = index;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (hoverStyle == StyleHit::NONE &&
|
||
UiPointInRect(SliderHitRect(m_opacityTrackRect, m_scale), fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::OPACITY_SLIDER;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE &&
|
||
UiPointInRect(SliderHitRect(m_widthTrackRect, m_scale), fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::WIDTH_SLIDER;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_opacityValueRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::OPACITY_VALUE;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthValueRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::WIDTH_VALUE;
|
||
}
|
||
// 两行右侧的复位按钮(在数值框右边,命中区不重叠,但顺序放在数值之后更安全)。
|
||
if (hoverStyle == StyleHit::NONE && m_widthResetRect.Width > 0.0f &&
|
||
UiPointInRect(m_widthResetRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::WIDTH_RESET;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && m_opacityResetRect.Width > 0.0f &&
|
||
UiPointInRect(m_opacityResetRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::OPACITY_RESET;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustGain], fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::GAIN_ADJUST;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::MIRROR_ADJUST;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::ABSOLUTE_TOGGLE;
|
||
}
|
||
}
|
||
|
||
if (hoverRow == m_hoverRow && hoverStyle == m_hoverStyle && hoverStyleIndex == m_hoverStyleIndex &&
|
||
part == m_hoverPart)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
m_hoverRow = hoverRow;
|
||
m_hoverPart = part;
|
||
m_hoverStyle = hoverStyle;
|
||
m_hoverStyleIndex = hoverStyleIndex;
|
||
|
||
// 供绘图区联动高亮的悬停信号(用户 2026-10-15 要求):
|
||
// * 列表里悬停某条信号行 → 高亮它所在的网格 + 该信号的曲线;
|
||
// * 样式区「目标」行的信号名 / GX 标签上悬停 → 同样联动高亮。
|
||
// ⚠️ ✕(移除该信号)不算"悬停到该信号":它是一条即将被删除的信号,
|
||
// 高亮它会让人误以为要保留它。
|
||
SignalRef hovered;
|
||
if (hoverRow >= 0 && hoverRow < static_cast<int>(m_rows.size()) &&
|
||
m_rows[static_cast<std::size_t>(hoverRow)].kind == RowKind::SIGNAL &&
|
||
part != RowPart::REMOVE_SIGNAL)
|
||
{
|
||
hovered = SignalRef{m_rows[static_cast<std::size_t>(hoverRow)].groupIndex,
|
||
m_rows[static_cast<std::size_t>(hoverRow)].signalIndex};
|
||
}
|
||
else if (hoverStyle == StyleHit::TARGET_NAME || hoverStyle == StyleHit::TARGET_BADGE)
|
||
{
|
||
hovered = SelectedRef();
|
||
}
|
||
m_hoveredSignal = hovered;
|
||
return true;
|
||
}
|
||
|
||
bool SignalPanel::OnLButtonDown(int x, int y)
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
|
||
// 新的按下开始:上一轮留下的"待确认关闭"一律作废。
|
||
m_pendingClosePart = RowPart::NONE;
|
||
m_pendingCloseRow = -1;
|
||
m_pendingFilterClear = false;
|
||
|
||
// ⚠️⚠️ **内联改名期间,名称区域整行都属于“文本操作区”**:
|
||
// 行高大于输入框高度(上下各留 3 逻辑像素),鼠标起点落在内边距上时旧实现会继续往下走
|
||
// 到分组行分支 ⇒ 分组被展开/收起(用户 2026-10-19 报告)。
|
||
// 这里直接按“文本按下”处理(定位插入符 / 开始拖拽选区),绝不触发行点击。
|
||
if (m_editField != StyleEditField::NONE && m_editRow >= 0 &&
|
||
m_editRow < static_cast<int>(m_rows.size()))
|
||
{
|
||
const gd::RectF field = EditFieldRect();
|
||
if (field.Width > 0.0f &&
|
||
UiPointInRect(NameEditAbsorbRect(field), fx, fy))
|
||
{
|
||
OnEditMouseDown(x, y);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 「筛选信号」输入框:单击进入编辑;点击其它位置则先结束筛选编辑(文本已实时生效)。
|
||
if (IsOnFilterBox(x, y))
|
||
{
|
||
// 右侧 ✕:**抬起**时才清空(不退出编辑态,便于继续输入);
|
||
// 按下后移开再松开视为放弃。
|
||
if (UiPointInRect(FilterClearRect(), fx, fy))
|
||
{
|
||
m_pendingFilterClear = true;
|
||
return true;
|
||
}
|
||
BeginFilterEdit();
|
||
return true;
|
||
}
|
||
if (m_filterEditing)
|
||
{
|
||
// 文本已实时生效,这里只需收起编辑态,随后继续处理本次点击。
|
||
EndFilterEdit();
|
||
}
|
||
|
||
// 滚动条拖拽优先于其它命中测试。
|
||
if (m_scrollBarVisible && UiPointInRect(m_scrollBarRect, fx, fy))
|
||
{
|
||
CancelScrollAnimation();
|
||
const gd::RectF thumb = ScrollThumbRect();
|
||
m_draggingScrollBar = true;
|
||
if (UiPointInRect(thumb, fx, fy))
|
||
{
|
||
m_scrollGrabOffset = y - static_cast<int>(thumb.Y);
|
||
}
|
||
else
|
||
{
|
||
m_scrollGrabOffset = static_cast<int>(thumb.Height * 0.5f);
|
||
const float travel = fy - m_scrollBarRect.Y - static_cast<float>(m_scrollGrabOffset);
|
||
const float usable = m_scrollBarRect.Height - thumb.Height;
|
||
if (usable > 0.0f)
|
||
{
|
||
const float progress = std::max(0.0f, std::min(1.0f, travel / usable));
|
||
m_scrollOffset = progress * (m_contentHeight - m_listRect.Height);
|
||
ClampScroll();
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
m_pendingLaneMenu = false;
|
||
m_pendingColorPicker = false;
|
||
m_pendingGainMenu = false;
|
||
m_pendingMirrorMenu = false;
|
||
m_pendingTargetLaneMenu = false;
|
||
|
||
// 点击当前输入框以外的位置时提交输入。
|
||
if (m_editField != StyleEditField::NONE &&
|
||
!UiPointInRect(EditFieldRect(), fx, fy))
|
||
{
|
||
EndEdit(true);
|
||
}
|
||
|
||
// 全部显示/隐藏。
|
||
if (UiPointInRect(m_toggleAllRect, fx, fy))
|
||
{
|
||
m_tab->SetAllVisible(!AreAllSignalsVisible());
|
||
m_rowsDirty = true;
|
||
return true;
|
||
}
|
||
|
||
// 样式编辑区。
|
||
if (SignalStyle* style = StyleOf(SelectedRef()))
|
||
{
|
||
// ---- 顶层「目标」行 ----
|
||
// 信号名:单击**不进入编辑**(用户明确要求),只做联动(列表滚动 + 绘图区滚动到该网格);
|
||
// GX 标签:单击展开「所属网格」二级菜单(与信号列表行结尾的 GX 徽标**同一份菜单**)。
|
||
// ⚠️ GX 标签**不做**联动滚动(用户 2026-10-16 要求):点击即开菜单、保持原地,
|
||
// 但悬停仍然联动高亮(见 `OnMouseMove`)。
|
||
{
|
||
UpdateTargetRowGeometry();
|
||
const bool onName =
|
||
m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy);
|
||
const bool onBadge =
|
||
m_targetBadgeRect.Width > 0.0f && UiPointInRect(m_targetBadgeRect, fx, fy);
|
||
if (onBadge)
|
||
{
|
||
// 与网格徽标一致:按下只登记,**左键抬起**且指针仍在同一个徽标上时才展开菜单。
|
||
m_pendingTargetLaneMenu = true;
|
||
return true;
|
||
}
|
||
if (onName)
|
||
{
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
RequestPlotLaneScroll(SelectedRef());
|
||
return true;
|
||
}
|
||
}
|
||
|
||
int paletteCount = 0;
|
||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||
for (int index = 0; index < m_colorCount; ++index)
|
||
{
|
||
if (UiPointInRect(m_colorRects[static_cast<std::size_t>(index)], fx, fy))
|
||
{
|
||
style->color = palette[index % paletteCount];
|
||
// 联动:样式自定义区的任何交互都把列表滚动到正在编辑的目标信号。
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return true;
|
||
}
|
||
}
|
||
if (UiPointInRect(m_customChipRect, fx, fy))
|
||
{
|
||
// 先恢复到记忆的自定义颜色(即便随后收起浮窗也会生效),再在抬起时展开浮窗。
|
||
style->color = style->customColor;
|
||
m_pendingColorPicker = true;
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return true;
|
||
}
|
||
for (int index = 0; index < kModeOptionCount; ++index)
|
||
{
|
||
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
|
||
{
|
||
// 单点信号的「线段」/「点线」两项置灰 ⇒ 点了不生效(也不做联动滚动)。
|
||
if (!ModeOptionEnabled(index))
|
||
{
|
||
return true;
|
||
}
|
||
style->mode = kModeOptions[index];
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return true;
|
||
}
|
||
}
|
||
if (UiPointInRect(SliderHitRect(m_opacityTrackRect, m_scale), fx, fy))
|
||
{
|
||
m_draggingOpacity = true;
|
||
ApplyOpacityFromX(fx);
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return true;
|
||
}
|
||
if (UiPointInRect(SliderHitRect(m_widthTrackRect, m_scale), fx, fy))
|
||
{
|
||
m_draggingWidth = true;
|
||
ApplyWidthFromX(fx);
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return true;
|
||
}
|
||
// 数值框的命中优先于滑块:两者几何重叠时要能进入输入状态。
|
||
if (UiPointInRect(m_widthValueRect, fx, fy))
|
||
{
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return BeginEdit(StyleEditField::WIDTH);
|
||
}
|
||
if (UiPointInRect(m_opacityValueRect, fx, fy))
|
||
{
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return BeginEdit(StyleEditField::OPACITY);
|
||
}
|
||
// 两行右侧的**复位按钮**:把该行参数恢复为默认值(线宽按信号类型取默认、
|
||
// 不透明度回 100%)。与「系数/偏移」还原图标同一交互语义:就地回显、不弹任何东西。
|
||
if (m_widthResetRect.Width > 0.0f && UiPointInRect(m_widthResetRect, fx, fy))
|
||
{
|
||
ResetStyleLineWidth();
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return true;
|
||
}
|
||
if (m_opacityResetRect.Width > 0.0f && UiPointInRect(m_opacityResetRect, fx, fy))
|
||
{
|
||
ResetStyleOpacity();
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return true;
|
||
}
|
||
// 「调整」行:系数与镜像(按下只登记,**抬起**时才展开各自的二级面板,
|
||
// 与自定义颜色入口同一套交互)、绝对值(单击即切换)。
|
||
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy))
|
||
{
|
||
m_pendingGainMenu = true;
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return true;
|
||
}
|
||
if (UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy))
|
||
{
|
||
m_pendingMirrorMenu = true;
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return true;
|
||
}
|
||
if (UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy))
|
||
{
|
||
style->absolute = !style->absolute;
|
||
ScrollToSignalAnimated(SelectedRef());
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 列表行。
|
||
RowPart part = RowPart::NONE;
|
||
const int row = HitRow(x, y, part);
|
||
if (row < 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const ListRow item = m_rows[static_cast<std::size_t>(row)];
|
||
|
||
if (item.kind == RowKind::GROUP)
|
||
{
|
||
if (part == RowPart::CHECK)
|
||
{
|
||
// ⚠️ 筛选状态下组复选框**只作用于被筛选出来的信号**(用户 2026-11-03 明确要求):
|
||
// 全选时只勾上筛选结果,取消时也只取消筛选结果,
|
||
// 不会把未通过筛选的信号一起勾上/取消。
|
||
std::vector<bool> filterMask;
|
||
if (BuildFilterMask(item.groupIndex, filterMask))
|
||
{
|
||
const bool allVisible = m_tab->GroupAllVisibleSubset(item.groupIndex, filterMask);
|
||
if (allVisible)
|
||
{
|
||
// 取消:只取消筛选结果里的信号(其余保持原状)。
|
||
for (std::size_t i = 0; i < filterMask.size(); ++i)
|
||
{
|
||
if (filterMask[i])
|
||
{
|
||
m_tab->SetVisible(SignalRef{item.groupIndex, static_cast<int>(i)}, false);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
m_tab->SetGroupVisibleSubset(item.groupIndex, filterMask);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
const bool allVisible = m_tab->GroupAllVisible(item.groupIndex);
|
||
m_tab->SetGroupVisible(item.groupIndex, !allVisible);
|
||
}
|
||
m_rowsDirty = true;
|
||
return true;
|
||
}
|
||
if (part == RowPart::CLOSE)
|
||
{
|
||
// 关闭分组属于数据层操作,交由宿主窗口处理;但**抬起时才生效**,
|
||
// 这里只记录目标(按住后移开再松开视为放弃)。
|
||
m_pendingClosePart = RowPart::CLOSE;
|
||
m_pendingCloseRow = row;
|
||
return true;
|
||
}
|
||
// 折叠 / 展开分组:三角、「组X」标签、**名称本体**、以及右侧的「N 项」计数都生效。
|
||
// 用户 2026-10-17 要求:名称本体恢复单击展开/收起(此前为了避开“误入改名”
|
||
// 而禁掉了它,结果只能点三角、很不方便);误入改名改由**收紧双击阈值**解决
|
||
//(见 `OnDoubleClick`:只有快速双击才改名);
|
||
// 右侧的「N 项」计数**只折叠展开**,任何频率的点击都不进入改名(用户 2026-10-17 要求)。
|
||
{
|
||
// 只有名称区域(BODY)参与“快速双击进改名”的计时;三角 /「组X」/ 计数都不参与,
|
||
// 否则“点一次计数、紧接着点一次名称”会被误判成双击。
|
||
if (part == RowPart::BODY)
|
||
{
|
||
m_groupNameClickMs = GetTickCount64();
|
||
m_groupNameClickRow = row;
|
||
m_groupNameClickWasCollapsed = IsGroupCollapsed(item.groupIndex);
|
||
}
|
||
}
|
||
ToggleGroupFromRow(item.groupIndex, row);
|
||
return true;
|
||
}
|
||
|
||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||
|
||
// ✕:用户明确表示不再需要该信号 → 交由宿主从数据层移除并释放其采样内存。
|
||
// 必须在 SelectSignal 之前处理,避免选中一条即将被删除的信号;
|
||
// 且与其它 ✕ 一致,**抬起时才真正删除**(按住后移开再松开视为放弃)。
|
||
if (part == RowPart::REMOVE_SIGNAL)
|
||
{
|
||
m_pendingClosePart = RowPart::REMOVE_SIGNAL;
|
||
m_pendingCloseRow = row;
|
||
return true;
|
||
}
|
||
|
||
SelectSignal(ref);
|
||
|
||
if (part == RowPart::CHECK)
|
||
{
|
||
// 勾选/取消勾选会立即触发网格重新划分(取消后空网格随之消失)。
|
||
m_tab->ToggleVisible(ref);
|
||
m_rowsDirty = true;
|
||
}
|
||
else if (part == RowPart::LANE_BADGE)
|
||
{
|
||
// 在左键抬起时弹出,保证菜单不会随着按键松开而消失。
|
||
m_pendingLaneMenu = true;
|
||
m_pendingLaneRef = ref;
|
||
}
|
||
else
|
||
{
|
||
// 点击信号名(行主体):除了选中它,还要让绘图区的**目标网格**完整出现在可视区内
|
||
//(上百个独立网格时,旧有高亮根本无法定位)。悬停不触发这一步。
|
||
RequestPlotLaneScroll(ref);
|
||
// 同时登记"可能开始的拖拽排序":越过后阈值才真正进入拖拽态,
|
||
// 因此单击仍然是单击(用户 2026-11-03 要求按住可拖动排序)。
|
||
BeginSignalDrag(ref, row, x, y);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool SignalPanel::OnLButtonUp(int x, int y)
|
||
{
|
||
bool changed = false;
|
||
|
||
// ---- 信号条目拖拽 ----
|
||
// ⚠️ 必须放在最前面:拖拽结束时列表条目位置可能变化,不能再走下方的 ✕ / 菜单判定。
|
||
if (m_signalDragging)
|
||
{
|
||
FinishSignalDrag();
|
||
return true;
|
||
}
|
||
m_signalDragPending = false;
|
||
|
||
// ---- 关闭类按钮(✕):按下时记录目标,抬起时若指针仍在同一个 ✕ 上才执行 ----
|
||
if (m_pendingFilterClear)
|
||
{
|
||
m_pendingFilterClear = false;
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
const gd::RectF clearRect = FilterClearRect();
|
||
if (clearRect.Width > 0.0f && UiPointInRect(clearRect, fx, fy))
|
||
{
|
||
ClearFilter();
|
||
changed = true;
|
||
}
|
||
}
|
||
if (m_pendingClosePart != RowPart::NONE)
|
||
{
|
||
const RowPart pendingPart = m_pendingClosePart;
|
||
const int pendingRow = m_pendingCloseRow;
|
||
m_pendingClosePart = RowPart::NONE;
|
||
m_pendingCloseRow = -1;
|
||
|
||
RowPart part = RowPart::NONE;
|
||
const int row = HitRow(x, y, part);
|
||
if (row == pendingRow && part == pendingPart && row >= 0)
|
||
{
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||
if (m_host != nullptr)
|
||
{
|
||
if (pendingPart == RowPart::CLOSE)
|
||
{
|
||
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(item.groupIndex), 0);
|
||
}
|
||
else
|
||
{
|
||
// wParam = 分组下标,lParam = 组内信号下标(见 Application 的消息处理)。
|
||
PostMessageW(m_host, WM_APP + 2, static_cast<WPARAM>(item.groupIndex),
|
||
static_cast<LPARAM>(item.signalIndex));
|
||
}
|
||
}
|
||
changed = true;
|
||
}
|
||
}
|
||
|
||
if (m_draggingOpacity)
|
||
{
|
||
m_draggingOpacity = false;
|
||
changed = true;
|
||
}
|
||
if (m_draggingWidth)
|
||
{
|
||
m_draggingWidth = false;
|
||
changed = true;
|
||
}
|
||
if (m_draggingScrollBar)
|
||
{
|
||
m_draggingScrollBar = false;
|
||
changed = true;
|
||
}
|
||
|
||
if (m_pendingColorPicker)
|
||
{
|
||
m_pendingColorPicker = false;
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
if (UiPointInRect(m_customChipRect, fx, fy) && m_tab != nullptr)
|
||
{
|
||
if (const SignalStyle* style = StyleOf(SelectedRef()))
|
||
{
|
||
changed = OpenColorPickerPopup(*style) || changed;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 「系数」/「镜像」:按下时只登记,抬起时指针仍在同一按钮上才展开二级面板
|
||
//(与自定义颜色入口一致:按住后移开再松开视为放弃)。
|
||
if (m_pendingGainMenu)
|
||
{
|
||
m_pendingGainMenu = false;
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) && m_tab != nullptr)
|
||
{
|
||
changed = OpenGainMenu() || changed;
|
||
}
|
||
}
|
||
if (m_pendingMirrorMenu)
|
||
{
|
||
m_pendingMirrorMenu = false;
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
if (UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) && m_tab != nullptr)
|
||
{
|
||
changed = OpenMirrorMenu() || changed;
|
||
}
|
||
}
|
||
|
||
if (m_pendingLaneMenu)
|
||
{
|
||
m_pendingLaneMenu = false;
|
||
RowPart part = RowPart::NONE;
|
||
const int row = HitRow(x, y, part);
|
||
if (row >= 0 && part == RowPart::LANE_BADGE)
|
||
{
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||
if (ref == m_pendingLaneRef)
|
||
{
|
||
const gd::RectF badge = SignalBadgeRect(row);
|
||
OpenLaneMenu(ref, static_cast<int>(badge.X),
|
||
static_cast<int>(badge.Y + badge.Height));
|
||
changed = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 「目标」行的 GX 标签:与信号行的网格徽标同一套交互(抬起且仍在同一徽标上才展开)。
|
||
if (m_pendingTargetLaneMenu)
|
||
{
|
||
m_pendingTargetLaneMenu = false;
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
UpdateTargetRowGeometry();
|
||
if (m_targetBadgeRect.Width > 0.0f && UiPointInRect(m_targetBadgeRect, fx, fy) &&
|
||
m_tab != nullptr && SelectedRef().IsValid())
|
||
{
|
||
OpenLaneMenu(SelectedRef(), static_cast<int>(m_targetBadgeRect.X),
|
||
static_cast<int>(m_targetBadgeRect.Y + m_targetBadgeRect.Height));
|
||
changed = true;
|
||
}
|
||
}
|
||
|
||
return changed;
|
||
}
|
||
|
||
bool SignalPanel::OnDoubleClick(int x, int y)
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 双击进入改名:必须先把"可能开始的拖拽"作废,否则第二次按下会被当成新的拖拽起点,
|
||
// 改名后轻微移动就会拖着信号跑(用户 2026-11-03 要求拖拽排序,改名不能被它干扰)。
|
||
CancelSignalDrag();
|
||
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
|
||
// ⚠️⚠️ **编辑态下名称区域的双击必须被吃掉**:
|
||
// 窗口带 `CS_DBLCLKS`,编辑时快速再点一下会被合成为 `WM_LBUTTONDBLCLK`,
|
||
// 而旧实现直接进入行双击逻辑 ⇒ 分组被展开/收起(或再次进入改名)。
|
||
// 输入框内的双击本该是“选词”,本组件只提供整段选择,因此这里保持现状不变、
|
||
// 仅保证**不把双击当成行双击**(用户 2026-10-19 报告)。
|
||
if (m_editField != StyleEditField::NONE)
|
||
{
|
||
const gd::RectF field = EditFieldRect();
|
||
if (field.Width > 0.0f &&
|
||
(UiPointInRect(field, fx, fy) ||
|
||
(m_editRow >= 0 && m_editRow < static_cast<int>(m_rows.size()) &&
|
||
UiPointInRect(NameEditAbsorbRect(field), fx, fy))))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 样式区数值框:双击进入(或保持)输入状态。
|
||
if (UiPointInRect(m_widthValueRect, fx, fy))
|
||
{
|
||
return BeginEdit(StyleEditField::WIDTH);
|
||
}
|
||
if (UiPointInRect(m_opacityValueRect, fx, fy))
|
||
{
|
||
return BeginEdit(StyleEditField::OPACITY);
|
||
}
|
||
|
||
// 样式区顶部「目标」行:双击信号名(或它后面的 GX 标签)进入**改名**编辑态。
|
||
// ⚠️ 单击不进入编辑(见 `OnLButtonDown`),必须双击 —— 用户明确要求。
|
||
if (m_tab != nullptr && StyleOf(SelectedRef()) != nullptr)
|
||
{
|
||
UpdateTargetRowGeometry();
|
||
const bool onName =
|
||
m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy);
|
||
const bool onBadge =
|
||
m_targetBadgeRect.Width > 0.0f && UiPointInRect(m_targetBadgeRect, fx, fy);
|
||
if (onName || onBadge)
|
||
{
|
||
// `row < 0` 表示编辑目标是"当前选中信号"(即目标行显示的信号)。
|
||
m_rowsDirty = true;
|
||
return BeginEdit(StyleEditField::SIGNAL_NAME, -1);
|
||
}
|
||
}
|
||
|
||
RowPart part = RowPart::NONE;
|
||
const int row = HitRow(x, y, part);
|
||
if (row < 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const ListRow item = m_rows[static_cast<std::size_t>(row)];
|
||
|
||
// 分组行:双击折叠三角用于折叠;双击**名称区域**进入重命名 ——
|
||
// ⚠️ 但只有**快速双击**才算(用户 2026-10-17 要求降低灵敏度):
|
||
// 按下那一次已经把分组展开/收起了(`OnLButtonDown` 统一处理三角/「组X」/名称),
|
||
// 因此这里要把它**撤销**回点击前的状态,然后才进入改名;
|
||
// 超过 `kGroupRenameDoubleClickMs` 的“慢双击”只当**又一次单击**(再切一次折叠)。
|
||
if (item.kind == RowKind::GROUP)
|
||
{
|
||
if (part == RowPart::GROUP_COUNT)
|
||
{
|
||
// 「N 项」计数:**只折叠展开**,绝不进入改名(用户 2026-10-17 要求)。
|
||
// 按下那一 次已经切过折叠状态,这里再切一次 = 两次点击各自生效。
|
||
ToggleGroupFromRow(item.groupIndex, row);
|
||
return true;
|
||
}
|
||
if (part == RowPart::DISCLOSURE)
|
||
{
|
||
// 与单击分组行同一入口(悬浮时先滚动就位再播放折起动画)。
|
||
ToggleGroupFromRow(item.groupIndex, row);
|
||
return true;
|
||
}
|
||
if (part != RowPart::BODY)
|
||
{
|
||
return false;
|
||
}
|
||
// 名称区域:判定“快速双击”。
|
||
const std::uint64_t now = GetTickCount64();
|
||
const bool fastDoubleClick =
|
||
(m_groupNameClickRow == row) && (now >= m_groupNameClickMs) &&
|
||
(now - m_groupNameClickMs <= kGroupRenameDoubleClickMs);
|
||
if (!fastDoubleClick)
|
||
{
|
||
// 慢双击 / 两次无关点击:只当另一次单击(切换折叠)。
|
||
m_groupNameClickMs = now;
|
||
m_groupNameClickRow = row;
|
||
m_groupNameClickWasCollapsed = IsGroupCollapsed(item.groupIndex);
|
||
ToggleGroupFromRow(item.groupIndex, row);
|
||
return true;
|
||
}
|
||
// 快速双击 ⇒ 改名:先撤销“按下”那一次对折叠状态的切换,再进入编辑。
|
||
m_groupNameClickMs = 0;
|
||
m_groupNameClickRow = -1;
|
||
SetGroupCollapsed(item.groupIndex, m_groupNameClickWasCollapsed);
|
||
// ⚠️ **必须同时取消这个分组正在播放的折起/展开动画**:第一次单击已启动
|
||
// 150–320ms 的收起动画(`m_groupAnims` 里还留着 `collapsing` 条目)。只回滚折叠
|
||
// 状态不清动画的话,改名期间该组仍在播放“收起再瞬间弹回”,输入框画在正在平移的行上。
|
||
for (std::size_t index = 0; index < m_groupAnims.size();)
|
||
{
|
||
if (m_groupAnims[index].groupIndex == item.groupIndex)
|
||
{
|
||
m_groupAnims.erase(m_groupAnims.begin() + static_cast<std::ptrdiff_t>(index));
|
||
}
|
||
else
|
||
{
|
||
++index;
|
||
}
|
||
}
|
||
m_rowsDirty = true;
|
||
EnsureRows();
|
||
return BeginEdit(StyleEditField::GROUP_TITLE, GroupRowIndex(item.groupIndex));
|
||
}
|
||
|
||
// 复选框、网格徽标与「移除信号」✕ 各自由单击处理,双击不再切换可见性/进入重命名。
|
||
if (part == RowPart::CHECK || part == RowPart::LANE_BADGE || part == RowPart::REMOVE_SIGNAL)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||
SelectSignal(ref);
|
||
m_rowsDirty = true;
|
||
return BeginEdit(StyleEditField::SIGNAL_NAME, row);
|
||
}
|
||
|
||
bool SignalPanel::DoubleClickIsPlainPress(int x, int y)
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
|
||
// 真正有双击语义的位置(保持双击行为):
|
||
// * 线宽 / 不透明度数值框:双击进入(或保持)输入状态;
|
||
// * 样式区「目标」行的名称与 GX 标签:双击进入改名;
|
||
// * 内联输入框自身:双击用于选词 / 定位插入符。
|
||
if (m_editField != StyleEditField::NONE && UiPointInRect(EditFieldRect(), fx, fy))
|
||
{
|
||
return false;
|
||
}
|
||
if (UiPointInRect(m_widthValueRect, fx, fy) || UiPointInRect(m_opacityValueRect, fx, fy))
|
||
{
|
||
return false;
|
||
}
|
||
// 线宽 / 透明度两行右侧的复位按钮是"单次点击即生效"的按钮:双击不得被解释成别的语义。
|
||
if ((m_widthResetRect.Width > 0.0f && UiPointInRect(m_widthResetRect, fx, fy)) ||
|
||
(m_opacityResetRect.Width > 0.0f && UiPointInRect(m_opacityResetRect, fx, fy)))
|
||
{
|
||
return false;
|
||
}
|
||
UpdateTargetRowGeometry();
|
||
if (m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy))
|
||
{
|
||
return false;
|
||
}
|
||
if (m_targetBadgeRect.Width > 0.0f && UiPointInRect(m_targetBadgeRect, fx, fy))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
RowPart part = RowPart::NONE;
|
||
const int row = HitRow(x, y, part);
|
||
if (row < 0)
|
||
{
|
||
// 列表之外(样式区按钮 / 筛选框 / 面板空白):全部只有单击语义。
|
||
return true;
|
||
}
|
||
// 行的**名称区域**(分组 / 信号)都靠双击改名 ⇒ 保留双击语义。
|
||
if (part == RowPart::BODY)
|
||
{
|
||
return false;
|
||
}
|
||
// 其余全部(复选框、折叠三角、「组X」、✕、GX 徽标、全部显示隐藏、色块、绘制方式、
|
||
// 滑块、「调整」按钮、筛选框…)都只有单击语义 ⇒ 快速连点必须当作一次次按下。
|
||
return true;
|
||
}
|
||
|
||
bool SignalPanel::OnRButtonDown(int x, int y)
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
RowPart part = RowPart::NONE;
|
||
const int row = HitRow(x, y, part);
|
||
if (row < 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||
if (item.kind == RowKind::GROUP)
|
||
{
|
||
OpenGroupMenu(item.groupIndex, x, y);
|
||
return true;
|
||
}
|
||
|
||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||
SelectSignal(ref);
|
||
OpenSignalMenu(ref, x, y);
|
||
return true;
|
||
}
|
||
|
||
bool SignalPanel::OnMouseWheel(int delta)
|
||
{
|
||
// 用户主动滚动 ⇒ 立即取消正在进行的目标信号滚动动画(否则两者会互相打架)。
|
||
CancelScrollAnimation();
|
||
|
||
if (m_dataSet == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const float previous = m_scrollOffset;
|
||
m_scrollOffset -= static_cast<float>(delta) / 120.0f * 54.0f * m_scale;
|
||
ClampScroll();
|
||
return std::fabs(previous - m_scrollOffset) > 0.01f;
|
||
}
|
||
|
||
bool SignalPanel::OnMouseLeave()
|
||
{
|
||
const bool changed =
|
||
(m_hoverRow >= 0) || (m_hoverStyle != StyleHit::NONE) || m_hoveredSignal.IsValid();
|
||
m_hoverRow = -1;
|
||
m_hoverPart = RowPart::NONE;
|
||
m_hoverStyle = StyleHit::NONE;
|
||
m_hoverStyleIndex = -1;
|
||
// 鼠标离开面板 → 取消绘图区的联动高亮(用户要求"移开鼠标时恢复")。
|
||
m_hoveredSignal = SignalRef();
|
||
// ⚠️ 拖拽中**不取消**:鼠标被拖到列表外(甚至面板外)时条目应继续跟随,
|
||
// 松手才提交(用户 2026-11-03 要求"拖拽时条目跟随鼠标")。
|
||
return changed;
|
||
}
|
||
|
||
// ======================================================================
|
||
// 信号条目拖拽排序(用户 2026-11-03 要求)
|
||
//
|
||
// 交互:左键按住信号行主体 → 越过 4px 阈值才进入拖拽(避免与单击选中/联动滚动抢事件);
|
||
// 拖拽中被拖条目**脱离原位置**跟随鼠标,其它信号行/分组行按插入位置做避让位移
|
||
//(目标值由 `UpdateSignalDragTargets` 给出,实际位移逐帧插值到 `m_signalDragShiftCur`);
|
||
// 松手时把(源分组、源下标、目标分组、目标下标)提交给宿主。
|
||
// ======================================================================
|
||
|
||
void SignalPanel::BeginSignalDrag(const SignalRef& ref, int row, int x, int y)
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr || !ref.IsValid() || row < 0 ||
|
||
row >= static_cast<int>(m_rows.size()))
|
||
{
|
||
return;
|
||
}
|
||
m_signalDragPending = true;
|
||
m_signalDragging = false;
|
||
m_signalDragRef = ref;
|
||
m_signalDragRow = row;
|
||
m_signalDragStartX = x;
|
||
m_signalDragStartY = y;
|
||
m_signalDragMouseY = y;
|
||
m_signalDragDropGroup = -1;
|
||
m_signalDragDropIndex = -1;
|
||
m_signalDragDropRow = -1;
|
||
m_signalDragDropBefore = false;
|
||
m_signalDragShift.clear();
|
||
m_signalDragShiftCur.clear();
|
||
}
|
||
|
||
bool SignalPanel::UpdateSignalDrag(int x, int y)
|
||
{
|
||
(void)x;
|
||
if (!m_signalDragging)
|
||
{
|
||
return false;
|
||
}
|
||
m_signalDragMouseY = y;
|
||
|
||
// 拖到列表上下边缘时自动滚动:跨组移动往往需要越过很长的列表
|
||
//(上百个信号的 MF4 里拖到另一个分组)。
|
||
const float edge = kPadding * m_scale;
|
||
if (static_cast<float>(y) < m_listRect.Y + edge)
|
||
{
|
||
m_scrollOffset -= 6.0f * m_scale;
|
||
ClampScroll();
|
||
}
|
||
else if (static_cast<float>(y) > m_listRect.Y + m_listRect.Height - edge)
|
||
{
|
||
m_scrollOffset += 6.0f * m_scale;
|
||
ClampScroll();
|
||
}
|
||
|
||
UpdateSignalDragTargets();
|
||
return true;
|
||
}
|
||
|
||
void SignalPanel::UpdateSignalDragTargets()
|
||
{
|
||
const std::size_t rowCount = m_rows.size();
|
||
m_signalDragDropGroup = -1;
|
||
m_signalDragDropIndex = -1;
|
||
m_signalDragDropRow = -1;
|
||
m_signalDragDropBefore = false;
|
||
m_signalDragShift.assign(rowCount, 0.0f);
|
||
if (m_signalDragShiftCur.size() != rowCount)
|
||
{
|
||
m_signalDragShiftCur.resize(rowCount, 0.0f);
|
||
}
|
||
if (m_dataSet == nullptr || !m_signalDragging || m_signalDragRow < 0 ||
|
||
m_signalDragRow >= static_cast<int>(rowCount))
|
||
{
|
||
return;
|
||
}
|
||
|
||
const float mouseY = static_cast<float>(m_signalDragMouseY);
|
||
|
||
// 鼠标所在的"自然行":`RowRect` 已叠加避让位移,这里减回去才代表用户看到的静止布局。
|
||
int hoverRow = -1;
|
||
bool below = false;
|
||
for (std::size_t index = 0; index < rowCount; ++index)
|
||
{
|
||
if (static_cast<int>(index) == m_signalDragRow)
|
||
{
|
||
continue;
|
||
}
|
||
const gd::RectF rect = RowRect(static_cast<int>(index));
|
||
const float top = rect.Y - m_signalDragShiftCur[index];
|
||
const float height = m_rows[index].height;
|
||
if (mouseY >= top && mouseY < top + height)
|
||
{
|
||
hoverRow = static_cast<int>(index);
|
||
below = (mouseY >= top + height * 0.5f);
|
||
break;
|
||
}
|
||
}
|
||
|
||
int dstRow = -1;
|
||
int dropGroup = -1;
|
||
int dropIndex = -1;
|
||
if (hoverRow < 0)
|
||
{
|
||
if (mouseY < m_listRect.Y && !m_rows.empty())
|
||
{
|
||
// 列表上方:落到第一个分组的最前面。
|
||
dstRow = 0;
|
||
dropGroup = m_rows.front().groupIndex;
|
||
dropIndex = 0;
|
||
}
|
||
else if (!m_rows.empty())
|
||
{
|
||
// 列表下方:落到最后一个含信号的分组的末尾。
|
||
for (int row = static_cast<int>(rowCount) - 1; row >= 0; --row)
|
||
{
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||
if (item.kind != RowKind::SIGNAL)
|
||
{
|
||
continue;
|
||
}
|
||
const SignalGroup* group = m_dataSet->GroupAt(item.groupIndex);
|
||
dstRow = static_cast<int>(rowCount);
|
||
dropGroup = item.groupIndex;
|
||
dropIndex = (group != nullptr) ? static_cast<int>(group->signals.size()) : 0;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(hoverRow)];
|
||
if (item.kind == RowKind::GROUP)
|
||
{
|
||
// 组名行:插入到该组第一个信号之前(组名本身不让位)。
|
||
dstRow = hoverRow + 1;
|
||
dropGroup = item.groupIndex;
|
||
dropIndex = 0;
|
||
}
|
||
else
|
||
{
|
||
dstRow = below ? (hoverRow + 1) : hoverRow;
|
||
dropGroup = item.groupIndex;
|
||
dropIndex = item.signalIndex + (below ? 1 : 0);
|
||
}
|
||
}
|
||
|
||
if (dstRow < 0 || dropGroup < 0 || dropIndex < 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 同组时补回"源信号被摘出"造成的下标偏移(数据层先删除再插入)。
|
||
if (dropGroup == m_signalDragRef.groupIndex && dropIndex > m_signalDragRef.signalIndex)
|
||
{
|
||
--dropIndex;
|
||
}
|
||
|
||
m_signalDragDropGroup = dropGroup;
|
||
m_signalDragDropIndex = dropIndex;
|
||
m_signalDragDropRow = dstRow;
|
||
|
||
// 避让:被跨过的行整体让出 / 补上一个行高。
|
||
const int srcRow = m_signalDragRow;
|
||
const float laneHeight = m_rows[static_cast<std::size_t>(srcRow)].height;
|
||
if (dstRow > srcRow)
|
||
{
|
||
for (int row = srcRow + 1; row < dstRow && row < static_cast<int>(rowCount); ++row)
|
||
{
|
||
m_signalDragShift[static_cast<std::size_t>(row)] = -laneHeight;
|
||
}
|
||
}
|
||
else if (dstRow < srcRow)
|
||
{
|
||
for (int row = dstRow; row < srcRow; ++row)
|
||
{
|
||
m_signalDragShift[static_cast<std::size_t>(row)] = laneHeight;
|
||
}
|
||
}
|
||
}
|
||
|
||
void SignalPanel::CancelSignalDrag()
|
||
{
|
||
m_signalDragPending = false;
|
||
m_signalDragging = false;
|
||
m_signalDragRow = -1;
|
||
m_signalDragDropGroup = -1;
|
||
m_signalDragDropIndex = -1;
|
||
m_signalDragDropRow = -1;
|
||
m_signalDragDropBefore = false;
|
||
m_signalDragShift.clear();
|
||
m_signalDragShiftCur.clear();
|
||
}
|
||
|
||
void SignalPanel::FinishSignalDrag()
|
||
{
|
||
const SignalRef ref = m_signalDragRef;
|
||
const int dropGroup = m_signalDragDropGroup;
|
||
const int dropIndex = m_signalDragDropIndex;
|
||
const bool moved = m_signalDragging && dropGroup >= 0 && dropIndex >= 0 &&
|
||
(dropGroup != ref.groupIndex || dropIndex != ref.signalIndex);
|
||
CancelSignalDrag();
|
||
|
||
if (!moved || m_host == nullptr || !ref.IsValid())
|
||
{
|
||
return;
|
||
}
|
||
// wParam = (源分组 << 16) | 源信号下标;lParam = (目标下标 << 16) | 目标分组。
|
||
// 目标下标用 0xFFFF 表示"末尾"(与本消息的约定一致,见 Application 的消息处理)。
|
||
const LONG_PTR from = (static_cast<LONG_PTR>(ref.groupIndex) << 16) |
|
||
(static_cast<LONG_PTR>(ref.signalIndex) & 0xFFFF);
|
||
const LONG_PTR to = (static_cast<LONG_PTR>(dropIndex) << 16) |
|
||
(static_cast<LONG_PTR>(dropGroup) & 0xFFFF);
|
||
PostMessageW(m_host, WM_APP + 8, static_cast<WPARAM>(from), static_cast<LPARAM>(to));
|
||
}
|
||
|
||
float SignalPanel::SignalDragRowShiftFor(int row) const
|
||
{
|
||
if (row < 0 || static_cast<std::size_t>(row) >= m_signalDragShiftCur.size())
|
||
{
|
||
return 0.0f;
|
||
}
|
||
return m_signalDragShiftCur[static_cast<std::size_t>(row)];
|
||
}
|
||
|
||
bool SignalPanel::AdvanceSignalDragAnimation()
|
||
{
|
||
if (!m_signalDragPending && !m_signalDragging)
|
||
{
|
||
return false;
|
||
}
|
||
if (m_signalDragShiftCur.size() != m_signalDragShift.size())
|
||
{
|
||
m_signalDragShiftCur.resize(m_signalDragShift.size(), 0.0f);
|
||
}
|
||
// 逐帧向目标位移插值(宿主约 10ms 一帧 ⇒ 约 5 帧收敛,观感是"平滑让位")。
|
||
const float factor = 0.32f;
|
||
for (std::size_t index = 0; index < m_signalDragShiftCur.size(); ++index)
|
||
{
|
||
const float target = m_signalDragShift[index];
|
||
float& current = m_signalDragShiftCur[index];
|
||
const float delta = target - current;
|
||
if (std::fabs(delta) < 0.5f)
|
||
{
|
||
current = target;
|
||
}
|
||
else
|
||
{
|
||
current += delta * factor;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void SignalPanel::PaintSignalDragGhost(gd::Graphics& g)
|
||
{
|
||
if (!m_signalDragging || m_signalDragRow < 0 ||
|
||
m_signalDragRow >= static_cast<int>(m_rows.size()) || m_theme == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
const gd::RectF natural = RowRect(m_signalDragRow);
|
||
const float height = m_rows[static_cast<std::size_t>(m_signalDragRow)].height;
|
||
if (height <= 0.5f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 被拖条目跟随鼠标(鼠标位于条目中部)。
|
||
float top = static_cast<float>(m_signalDragMouseY) - height * 0.5f;
|
||
top = std::max(m_listRect.Y, std::min(top, m_listRect.Y + m_listRect.Height - height));
|
||
|
||
const int state = g.Save();
|
||
g.SetClip(m_listRect, gd::CombineModeIntersect);
|
||
|
||
// 浮起的阴影:让条目明显"脱离"列表本体。
|
||
UiFillRoundRect(g, gd::RectF(natural.X + 2.0f * m_scale, top + 3.0f * m_scale,
|
||
natural.Width, height),
|
||
4.0f * m_scale, 0x33000000u);
|
||
|
||
// 复用行绘制:临时把行顶部覆盖到鼠标位置(与「组名悬浮」同一机制),
|
||
// 因此名称、复选框、GX 徽标、✕ 等全部随行一起移动,无需另写一套绘制。
|
||
m_rowYOverrideActive = true;
|
||
m_rowYOverrideY = top;
|
||
PaintSignalRow(g, m_signalDragRow);
|
||
m_rowYOverrideActive = false;
|
||
|
||
UiDrawRoundRect(g, gd::RectF(natural.X + 1.0f * m_scale, top + 1.0f * m_scale,
|
||
natural.Width - 2.0f * m_scale, height - 2.0f * m_scale),
|
||
4.0f * m_scale, m_theme->accent, 1.6f * m_scale);
|
||
|
||
// 插入位置指示线:明确告诉用户"松手后落在哪里"。
|
||
if (m_signalDragDropRow >= 0)
|
||
{
|
||
float lineY = 0.0f;
|
||
if (m_signalDragDropRow < static_cast<int>(m_rows.size()))
|
||
{
|
||
lineY = RowRect(m_signalDragDropRow).Y;
|
||
}
|
||
else if (!m_rows.empty())
|
||
{
|
||
const gd::RectF last = RowRect(static_cast<int>(m_rows.size()) - 1);
|
||
lineY = last.Y + last.Height;
|
||
}
|
||
if (lineY >= m_listRect.Y - 1.0f && lineY <= m_listRect.Y + m_listRect.Height + 1.0f)
|
||
{
|
||
UiDrawLine(g, m_listRect.X + 4.0f * m_scale, lineY,
|
||
m_listRect.X + m_listRect.Width - 4.0f * m_scale, lineY,
|
||
m_theme->accent, 2.0f * m_scale);
|
||
}
|
||
}
|
||
|
||
g.Restore(state);
|
||
}
|
||
|
||
} // namespace jdi
|
||
|