修复浮动面板双击穿透、网格高亮触发来源、新增样式区「目标」行与联动平滑滚动

用户 2026-10-15 第六轮报告的四个问题与要求(详见 Documents/agent_mem.md §9.34):

1. 在网格内的实验性功能浮动面板上双击任意位置会穿透到下层网格,导致网格触发自适应。
   根因:PlotView::OnDoubleClick 在“差值标签 / 光标标签 / 光标线 / 图例”都不命中时
        无条件调用 FitAll()。
   修法:在该函数最前面(高于一切下层命中)用 TopPanelAtPoint() 判定,命中浮动面板
        则直接消费(面板自身双击无额外语义)。确立“上层元素的交互不得影响下层”的规则。

2. 未开启任何工具栏功能时鼠标移到网格上会让整个网格变色高亮(旧需求已不适用)。
   修法:取消“鼠标在网格里移动就高亮”的行为;改为在信号选择区域悬停某条信号时,
        若该信号已被勾选绘制到某个网格,则该网格变色高亮,并且该信号曲线本身也高亮;
        鼠标移开即恢复。悬停未勾选的信号不产生任何高亮。
   实现:PlotView 新增 m_externalHoverSignal + SetExternalHoverSignal()(与 m_hoverSignal
        分开保存,因为绘图区 OnMouseLeave 会清掉后者,而列表悬停时鼠标不在绘图区内)、
        EffectiveHoverLane();SignalPanel 新增 HoveredSignal(),并在 InvalidateRows()
        里一并作废;Application::Render() 每帧同步(覆盖 WM_MOUSELEAVE / 数据变化 /
        切换标签页等全部路径)。GX 徽标的悬停高亮仍按鼠标位置独立判定。

3. 在信号样式自定义区域最上方(预设颜色色块之上)新增「目标」行。
   内容:当前正在被编辑的目标信号名称;若它已绘制到网格,则在名称后方显示所在 GX 标签。
   交互:与信号选择区域的信号行一致 —— 名称/GX 标签悬停有高亮提示,且悬停时同样触发
        该信号所在网格高亮 + 曲线高亮;单击不进入编辑(只触发联动滚动),双击才进入改名
        (复用 StyleEditField::SIGNAL_NAME,m_editRow < 0 表示目标是选中信号)。
   实现:SignalPanel 新增 m_targetRowRect / PaintTargetRow / UpdateTargetRowGeometry
        (名称宽度与“是否有 GX 标签”都可能在不重排时变化,因此按需重算并带缓存键)、
        StyleHit::TARGET_NAME / TARGET_BADGE;DesiredStyleHeight() 与 LayoutStyleControls()
        相应多算一行。

4. 联动机制更新:从文件导入大量信号(如 MF4 的 124 条)时,被选中的信号可能被滚动到
   可见范围之外而找不到。
   修法:新增 SignalPanel::ScrollToSignalAnimated()(+ AdvanceScrollAnimation),
        触发联动时把信号列表平滑滚动到目标信号完整可见:
        * 目标在视界上方 ⇒ 向上滚动,且顶部让出一个组行高度,避免被悬浮组名遮挡;
        * 目标在视界下方 ⇒ 向下滚动到完整露出;
        * 已经完整可见则不动;
        * ease-out cubic(越远越快、靠近变慢),时长 = clamp(距离×0.35ms, 130, 300),
          总时长有上限;用户滚轮 / 拖滚动条立即取消动画。
   触发点:绘图区内把某条信号选为当前目标(点击图例、点击信号曲线、双击图例改名)
        ⇒ PlotView::SelectSignal() 发布请求(TakePanelScrollRequest()),宿主在鼠标消息
        处理后调用 ApplyPendingPanelScroll();样式自定义区的任何交互(预设色块、自定义
        颜色入口、样式按钮、线宽/透明度滑块与数值框、「系数/偏移」「镜像」二级菜单、
        绝对值)以及「目标」行的单击也都会触发同一次滚动。
   宿主:Application 新增 16ms 定时器(kScrollTimerId)驱动动画,并由 Render 自管开关
        (与提示浮层淡出同一套做法)。

验证(全部离屏,不占用鼠标 / 键盘 / 屏幕):
- .preflight/check(SW_HIDE + 只注入合成消息 + 进程/线程 BELOW_NORMAL)**73/73 通过**,
  连跑 3 次稳定:[A] 浮动面板双击不移视界,且对照组(空白处双击)必须触发自适应;
  [B] 鼠标在网格里移动不高亮;[C] 列表悬停 → 网格高亮 + 移开恢复 + 未勾选信号不高亮;
  [D] 目标行的布局/占位/GX 标签位置与显隐/悬停联动/单击不编辑 + 双击改名(初始文本正确);
  [E] 长列表 122 行:动画分多帧、单调、时长上限 300ms、目标完整可见且不被悬浮组名遮挡、
      已可见时不动、滚轮取消动画;[F] 样式区交互与绘图区联动都会滚动到目标并完整显示;
  [G] 布局回归(目标行在列表下方、样式区仍完整、默认无悬停与高亮)。
- A/B 判别:回退六处实现后 **15 项如实失败**(含"面板双击仍触发自适应"、"鼠标移到网格即高亮"、
  "目标行不联动"、"联动瞬移而非动画")。
- make release:全部源文件重新编译 **零警告**;链接用临时 `make release TARGET_FILE_NAME=Target_check` 通过。

同步记忆:Documents/agent_mem.md 新增 §9.34、§11.2 第 72–75 条硬约束、变更日志新行。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-15 20:32:16 +08:00
1 parent 73389f424c
commit cb9d3e4b8f
7 files changed
+848 -32

No files matched your search

+87 -15
View File
@@ -1061,6 +1061,48 @@ namespace jdi
{
m_dataSetWritable->SetSelected(ref);
}
// 联动(用户 2026-10-15 要求):绘图区内把某条信号选为当前目标时,
// 请求宿主把信号列表**平滑滚动**到它,保证在长列表里也能看到它。
if (ref.IsValid())
{
m_pendingPanelScroll = ref;
m_pendingPanelScrollFlag = true;
}
}
bool PlotView::TakePanelScrollRequest(SignalRef& ref)
{
if (!m_pendingPanelScrollFlag)
{
return false;
}
ref = m_pendingPanelScroll;
m_pendingPanelScrollFlag = false;
m_pendingPanelScroll = SignalRef();
return ref.IsValid();
}
void PlotView::SetExternalHoverSignal(const SignalRef& ref)
{
if (m_externalHoverSignal == ref)
{
return;
}
m_externalHoverSignal = ref;
}
int PlotView::EffectiveHoverLane() const
{
if (m_hoverLane >= 0)
{
return m_hoverLane;
}
// 外部悬停信号(信号列表 / 样式区「目标」行):只在该信号确实绘制到某个网格时生效。
if (m_tab != nullptr && m_externalHoverSignal.IsValid())
{
return m_tab->LaneOfSignal(m_externalHoverSignal);
}
return -1;
}
void PlotView::ClampLaneScroll()
@@ -6080,8 +6122,11 @@ namespace jdi
changed = true;
}
// 悬停网格:鼠标在面板上时高亮面板所属网格(用户明确要求)。
const int lane = overEdgePanel ? panelLane : LaneAtY(y);
// 悬停网格:**只有**鼠标悬停在实验性功能浮动面板上时才高亮该面板所属网格。
// ⚠️ 用户 2026-10-15 明确要求:鼠标在网格里移动**不再触发**网格变色高亮;
// 改为在信号列表 / 样式区「目标」行悬停到某条已绘制信号时联动高亮
// (见 `SetExternalHoverSignal` 与 `EffectiveHoverLane`)。
const int lane = overEdgePanel ? panelLane : -1;
if (lane != m_hoverLane)
{
m_hoverLane = lane;
@@ -6089,16 +6134,20 @@ namespace jdi
}
// 「GX」编号徽标的悬停高亮:徽标本体在静态场景 / 装饰缓存里被冻结,高亮是叠加层,
// 因此必须靠这个**单独的状态**触发重绘与取消(鼠标在同一个网格的徽标范围内移动时
// `m_hoverLane` 不变)。
// 因此必须靠这个**单独的状态**触发重绘与取消。徽标命中的网格直接用鼠标纵坐标算,
// 与上面的「悬停网格高亮」解耦(后者已不再跟随鼠标位置)。
int hoverBadge = -1;
if (m_dragMode == PlotDragMode::NONE && !overEdgePanel && lane >= 0)
if (m_dragMode == PlotDragMode::NONE && !overEdgePanel)
{
const gd::RectF badge = LaneBadgeRect(lane);
if (badge.Width > 0.0f &&
UiPointInRect(badge, static_cast<float>(x), static_cast<float>(y)))
const int underMouse = LaneAtY(y);
if (underMouse >= 0)
{
hoverBadge = lane;
const gd::RectF badge = LaneBadgeRect(underMouse);
if (badge.Width > 0.0f &&
UiPointInRect(badge, static_cast<float>(x), static_cast<float>(y)))
{
hoverBadge = underMouse;
}
}
}
if (hoverBadge != m_hoverBadge)
@@ -6826,6 +6875,19 @@ namespace jdi
return false;
}
// ⚠️ **上层元素的交互绝不能穿透到下层**:实验性功能的浮动面板浮在网格之上(且绘制在
// 光标标签 / 图例之上),因此在它上面双击必须由它消费掉。旧实现在所有下层命中都不成
// 立时无条件 `FitAll()`,于是"在面板上双击任意位置"会让网格莫名其妙地自适应一次
// (用户 2026-10-15 报告)。面板自身双击没有额外语义,直接消费即可。
{
int panelLane = -1;
int panelKind = 0;
if (TopPanelAtPoint(static_cast<float>(x), static_cast<float>(y), panelLane, panelKind))
{
return true;
}
}
// 双光标差值标签:双击进入差值编辑(输入值的量级始终为秒)。
// 注意:差值标签在"两光标过近"时会落到主时间轴内部,因此必须先于区域判定。
if (IsOnCursorSpanLabel(x, y))
@@ -7835,9 +7897,12 @@ namespace jdi
// 悬停网格:一层很淡的强调色叠加。为了能画在静态场景之上,
// 这里用半透明而非不透明混色,观感与原实现基本一致。
if (m_hoverLane >= 0 && m_hoverLane < static_cast<int>(m_lanes.size()))
// ⚠️ 高亮来源见 `EffectiveHoverLane()`(浮动面板悬停 / 信号列表悬停),
// **不再**由"鼠标在网格里"触发。
const int hoverLane = EffectiveHoverLane();
if (hoverLane >= 0 && hoverLane < static_cast<int>(m_lanes.size()))
{
const gd::RectF& laneRect = m_lanes[static_cast<std::size_t>(m_hoverLane)].rect;
const gd::RectF& laneRect = m_lanes[static_cast<std::size_t>(hoverLane)].rect;
const float clipTop = std::max(laneRect.Y, m_plotRect.Y);
const float clipBottom = std::min(laneRect.Y + laneRect.Height, m_plotRect.Y + m_plotRect.Height);
if (clipBottom > clipTop)
@@ -7849,16 +7914,23 @@ namespace jdi
}
// 悬停信号:外扩一圈半透明强调色作为"可拖拽"提示。
if (m_hoverSignal.IsValid())
// 鼠标在绘图区内直接命中时用 `m_hoverSignal`;信号列表 / 样式区悬停时用
// `m_externalHoverSignal`(二者高亮方式相同,用户要求列表悬停也要高亮曲线本身)。
SignalRef haloRef = m_hoverSignal;
if (!haloRef.IsValid())
{
const ResolvedSignal resolved = ResolveSignal(m_hoverSignal);
haloRef = m_externalHoverSignal;
}
if (haloRef.IsValid())
{
const ResolvedSignal resolved = ResolveSignal(haloRef);
if (resolved.data != nullptr && resolved.style != nullptr && resolved.visible)
{
const int laneIndex = m_tab->LaneOfSignal(m_hoverSignal);
const int laneIndex = m_tab->LaneOfSignal(haloRef);
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
{
const LaneVisual& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
const PolylineCacheEntry* cached = AcquirePolyline(m_hoverSignal, resolved, lane);
const PolylineCacheEntry* cached = AcquirePolyline(haloRef, resolved, lane);
if (cached != nullptr)
{
PaintSignalHaloFor(g, resolved, lane, *cached);