修复组名编辑态点击穿透;浮动面板 GX 徽标悬停提示 + 点击滚动联动;最小网格高度提升到 81 逻辑像素

1) 组名/信号名处于编辑态时,在编辑框内双击、快速连点或框选编辑文本会让分组展开/收起。
   两处根因:
   - 行高大于输入框高度(上下各留 3 逻辑像素),起点落在内边距上时 OnEditMouseDown 返回
     false,宿主把按下交给 OnLButtonDown,命中分组行的 BODY 分支 ⇒ 折叠状态被切换;
   - 窗口带 CS_DBLCLKS,快速第二次按下被合成为 WM_LBUTTONDBLCLK,而 OnDoubleClick 没有
     编辑态保护,直接进入分组行双击逻辑 ⇒ 再次折叠 / 再次改名。
   修法:新增 NameEditAbsorbRect(输入框横向范围 × 整行高度)作为编辑态的“文本吸收区”:
   OnLButtonDown 命中即按文本操作处理(落点夹回输入框再算插入符),OnDoubleClick 命中即
   直接返回。横向仍限定在名称区域,同一行的复选框 / 「N 项」/ GX 徽标 / ✕ 继续可点。

2) 所有实验性功能浮动面板标题栏左侧的 GX 徽标:
   - EdgePanelLayout 新增 badge 矩形,成为绘制 / 悬停 / 手型光标 / 点击联动的唯一几何来源
     (命中顺序 close → badge → title,徽标在标题栏内部,放到 title 之后会被判成拖面板);
   - 悬停时徽标高亮 + 气泡提示(“G12 · 点击滚动到该网格” / “G12 · 已在视界内”),气泡在
     面板像素图层存进缓存之后绘制,避免被烘进位图后在鼠标移开后残留;
   - 点击调用 ScrollLaneIntoView:目标网格不可见时启动与“点选信号联动滚动”同源的平滑动画,
     已完整可见时不做任何改动。

3) 网格被压缩时的最小高度由 54 提升到 81 逻辑像素(1.5 倍)。同时修掉 m_minLaneHeight 被
   二次乘 m_scale 的隐患:Layout 里不再预先乘缩放,滚动条判定改用 laneCount × 81 × scale,
   与实际网格高度使用同一换算(125% 缩放下 128 个网格实测 101.25px = 81×1.25)。

验证(全部离屏:SW_HIDE 窗口 + 只注入合成消息 + BELOW_NORMAL,不占用鼠标键盘屏幕):
- 新增 .preflight/round3check(真实 TestData/MF4-Record.MF4,128 网格)31/31;
  A/B 临时禁用双击保护 ⇒ 1 项如实失败(证明断言有判别力);
- 回归:.preflight/editcheck 20/20、.preflight/check 137/137;
- make release / make debug 零警告。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-19 14:01:19 +08:00
1 parent 1e50494c41
commit be76f523f3
8 files changed
+307 -11

No files matched your search

+27 -1
View File
@@ -1525,6 +1525,14 @@ namespace jdi
gd::RectF panel; /**< 面板整体矩形。 */
gd::RectF title; /**< 标题栏(**唯一可拖拽区域**)。 */
gd::RectF close; /**< ✕ 关闭按钮。 */
/**
* @brief 标题栏左侧的「GX」编号徽标。
*
* 它是**绘制与命中的唯一几何来源**:悬停提示、手型光标、以及
* “点击把该网格滚动到视界内”都依赖它(用户 2026-10-19 要求)。
* 面板未展开时为空矩形。
*/
gd::RectF badge;
gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
int rows = 0; /**< 统计行数(每条可见信号一行)。 */
@@ -2060,6 +2068,15 @@ namespace jdi
*/
void PaintSignalInteraction(gd::Graphics& g);
/**
* @brief 绘制浮动面板「GX」徽标的悬停提示气泡(用户 2026-10-19 要求)。
*
* 气泡锚定在徽标下方,内容为“G 编号”与当前可执行的操
*(目标网格已在视界内 / 点击滚动到该网格)。
* ⚠️ 必须在**面板像素图层之后**绘制(见 `PaintEdgeCursor` 末尾),否则会被缓存进面板位图。
*/
void PaintPanelBadgeTooltip(gd::Graphics& g);
/**
* @brief 在绘图区底部绘制临时提示条(如“已放弃当前选区”)。
* @param [IN,OUT] g 绘图上下文。
@@ -2134,7 +2151,8 @@ namespace jdi
float m_labelBandWidth = 66.0f; /**< 网格内侧纵轴标尺带宽度(逻辑像素)。 */
float m_timeAxisHeight = 32.0f; /**< 时间轴高度(逻辑像素)。 */
float m_minLaneHeight = 54.0f; /**< 网格最小高度(逻辑像素)。 */
/** @brief 网格最小高度(**逻辑像素**,使用处乘 `m_scale`)。用户 2026-10-19 要求为 81(原 54 的 1.5 倍)。 */
float m_minLaneHeight = 81.0f;
float m_laneContentHeight = 0.0f; /**< 网格内容总高度。 */
bool m_scrollBarVisible = false; /**< 是否需要显示滚动条。 */
@@ -2260,6 +2278,14 @@ namespace jdi
* 因此必须单独记住这个状态才能触发重绘与取消高亮。
*/
int m_hoverBadge = -1;
/**
* @brief 上一帧鼠标是否悬停在某个浮动面板标题栏的「GX」徽标上(是则记录网格下标 / 面板种类)。
*
* 徽标绘制在面板**像素图层**里(缓存键只在鼠标位于面板内时才混入坐标),
* 而提示气泡是贴图之后的叠加层(见 `PaintPanelBadgeTooltip`),因此需要单独记录状态。
*/
int m_hoverPanelBadgeLane = -1;
int m_hoverPanelBadgeKind = -1;
/** 「弱峰调整」每行滑块右侧的复位控件是否处于悬停态(用于“进入/离开”时只提示一次)。 */
bool m_peakResetHover = false;
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
+6 -2
View File
@@ -109,7 +109,11 @@ namespace jdi
m_labelBandWidth = 66.0f * m_scale;
m_timeAxisHeight = 34.0f * m_scale;
m_minLaneHeight = 54.0f * m_scale;
// ⚠️ `m_minLaneHeight` 存的是**逻辑像素**(见头文件),使用处才乘 `m_scale`。
// 旧实现这里写成 `54.0f * m_scale`,而 `BuildLaneVisuals` 又乘了一次 `m_scale`,
// 在 125% 缩放下实际最小值被放大成 54×1.5625(而非 54×1.25),且与
// 「是否需要滚动条」的判定(只乘一次)不一致 ⇒ 会出现“网格溢出了却没有滚动条”。
m_minLaneHeight = 81.0f; // 用户 2026-10-19:最小网格高度提升到原来的 1.5 倍(54 → 81)
// 先为时间轴保留固定高度,剩余空间才交给网格,保证时间轴不会被挤压。
const float topPadding = 6.0f * m_scale;
@@ -119,7 +123,7 @@ namespace jdi
std::size_t laneCount = (m_tab != nullptr) ? m_tab->Lanes().size() : 0;
if (laneCount > 0)
{
const float needed = static_cast<float>(laneCount) * m_minLaneHeight;
const float needed = static_cast<float>(laneCount) * m_minLaneHeight * m_scale;
m_scrollBarVisible = (needed > availHeight);
}
else
@@ -878,6 +878,13 @@ namespace jdi
// ✕ 按钮:与面板/标签栏的其它 ✕ 一致,用手型光标作为可点击提示。
return LoadCursorW(nullptr, IDC_HAND);
}
// 「GX」编号徽标:悬停给出提示 + 手型光标;点击把该网格滚动到视界内。
// ⚠️ 必须排在标题栏之前(徽标位于标题栏矩形内部,否则会被判成“可拖拽”)。
if (layout.badge.Width > 0.0f &&
UiPointInRect(layout.badge, static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (UiPointInRect(layout.title, static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_SIZEALL);
@@ -1383,6 +1390,30 @@ namespace jdi
changed = true;
}
// 浮动面板标题栏左侧的「GX」编号徽标:悬停高亮 + 提示气泡(用户 2026-10-19 要求)。
// 徽标在面板像素图层里,因此悬停态必须由这里单独记录并触发重绘
//(面板缓存键在鼠标位于面板内时会混入坐标,所以重绘即可看到高亮)。
int hoverPanelBadgeLane = -1;
int hoverPanelBadgeKind = -1;
if (overEdgePanel && panelLane >= 0)
{
const EdgePanelLayout badgeLayout =
EdgePanelLayoutFor(panelLane, static_cast<EdgePanelKind>(hoverPanelKind));
if (badgeLayout.badge.Width > 0.0f &&
UiPointInRect(badgeLayout.badge, fx, static_cast<float>(y)))
{
hoverPanelBadgeLane = panelLane;
hoverPanelBadgeKind = hoverPanelKind;
}
}
if (hoverPanelBadgeLane != m_hoverPanelBadgeLane ||
hoverPanelBadgeKind != m_hoverPanelBadgeKind)
{
m_hoverPanelBadgeLane = hoverPanelBadgeLane;
m_hoverPanelBadgeKind = hoverPanelBadgeKind;
changed = true;
}
// 悬停网格:**只有**鼠标悬停在实验性功能浮动面板上时才高亮该面板所属网格。
// ⚠️ 用户 2026-10-15 明确要求:鼠标在网格里移动**不再触发**网格变色高亮;
// 改为在信号列表 / 样式区「目标」行悬停到某条已绘制信号时联动高亮
@@ -1663,6 +1694,19 @@ namespace jdi
return true;
}
// 「GX」编号徽标:点击把该网格滚动到视界内(用户 2026-10-19 要求)。
// ⚠️ 必须排在标题栏之前:徽标位于标题栏矩形内部,否则会变成“拖动面板”。
// `ScrollLaneIntoView` 自带“已经完整可见 ⇒ 什么都不做”,且是平滑动画。
if (layout.badge.Width > 0.0f && UiPointInRect(layout.badge, fx, fy))
{
m_dragMode = PlotDragMode::NONE;
if (ScrollLaneIntoView(index))
{
SetHint(L"正在滚动到 G" + std::to_wstring(index + 1) + L" 网格…");
}
return true;
}
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板:逐信号条目
// (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰调整是「名称 + 复选框 + 两行滑块」)。
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
@@ -2297,6 +2341,13 @@ namespace jdi
m_hoverBadge = -1;
changed = true;
}
if (m_hoverPanelBadgeLane != -1 || m_hoverPanelBadgeKind != -1)
{
// 浮动面板的「GX」徽标悬停提示必须随鼠标离开清除(否则提示气泡残留)。
m_hoverPanelBadgeLane = -1;
m_hoverPanelBadgeKind = -1;
changed = true;
}
if (m_hoverSignal.IsValid())
{
m_hoverSignal = SignalRef();
+68 -7
View File
@@ -5408,6 +5408,10 @@ namespace jdi
layout.title = gd::RectF(left, top, width, titleHeight);
const float closeSize = titleHeight;
layout.close = gd::RectF(left + width - closeSize, top, closeSize, closeSize);
// 标题栏左侧的「GX」编号徽标:绘制、悬停提示、手型光标与“点击滚动到该网格”
// 全部以这个矩形为准(唯一几何来源,避免“画的”和“点的”漂移)。
layout.badge = gd::RectF(left + pad, top + (titleHeight - kLaneBadgeHeight * m_scale) * 0.5f,
kLaneBadgeWidth * m_scale, kLaneBadgeHeight * m_scale);
if (itemized)
{
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
@@ -6005,6 +6009,7 @@ namespace jdi
mixRect(layout.panel);
mixRect(layout.title);
mixRect(layout.close);
mixRect(layout.badge);
mixRect(layout.edit);
mixRect(layout.button);
mix(static_cast<std::uint64_t>(layout.rows));
@@ -6376,14 +6381,18 @@ namespace jdi
WithAlpha(m_theme->accent, 0x2A));
// 标题:与左侧信号面板一致的 “GX” 徽标。
const float badgeWidth = kLaneBadgeWidth * m_scale;
const float badgeHeight = kLaneBadgeHeight * m_scale;
const gd::RectF badgeRect(panel.X + pad, panel.Y + (titleHeight - badgeHeight) * 0.5f,
badgeWidth, badgeHeight);
UiFillRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->cardBg);
UiDrawRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->border, 1.0f);
// ⚠️ 几何必须取 `layout.badge`(与悬停提示 / 手型光标 / 点击联动同源);
// 悬停时给一圈强调色描边,作为“可点击”的视觉提示(用户 2026-10-19 要求)。
const gd::RectF& badgeRect = layout.badge;
const bool badgeHot =
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) && badgeRect.Width > 0.0f &&
UiPointInRect(badgeRect, static_cast<float>(m_mouseX), static_cast<float>(m_mouseY));
UiFillRoundRect(g, badgeRect, badgeRect.Height * 0.5f,
badgeHot ? m_theme->accentSoft : m_theme->cardBg);
UiDrawRoundRect(g, badgeRect, badgeRect.Height * 0.5f,
badgeHot ? m_theme->accent : m_theme->border, badgeHot ? 1.4f : 1.0f);
UiText(g, L"G" + std::to_wstring(index + 1), m_fonts->monoSmall, badgeRect,
m_theme->text, TextAlign::CENTER);
badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
const std::wstring title =
meanFilter ? std::wstring(L"均值滤波")
: (weakPeak ? std::wstring(kWeakPeakTitle)
@@ -6728,6 +6737,58 @@ namespace jdi
m_panelSlots = std::move(nextSlots);
m_panelGlobalKey = panelGlobalKey;
m_panelValid = panelCacheReady;
// 「GX」徽标的悬停提示气泡:必须在**全部面板像素都存进图层之后**绘制,
// 否则气泡会被烘进面板缓存(下一帧即使鼠标移开也会重现)。
PaintPanelBadgeTooltip(g);
}
void PlotView::PaintPanelBadgeTooltip(gd::Graphics& g)
{
if (m_hoverPanelBadgeLane < 0 || m_fonts == nullptr || m_fonts->uiSmall == nullptr ||
m_tab == nullptr || m_hoverPanelBadgeKind < 0)
{
return;
}
const EdgePanelKind kind = static_cast<EdgePanelKind>(m_hoverPanelBadgeKind);
const EdgePanelLayout layout = EdgePanelLayoutFor(m_hoverPanelBadgeLane, kind);
if (layout.badge.Width <= 0.0f)
{
return;
}
// 目标网格是否已完整可见:决定提示文案(不可见时提示“点击会滚动过去”)。
bool laneVisible = false;
const std::size_t laneIndex = static_cast<std::size_t>(m_hoverPanelBadgeLane);
if (laneIndex < m_lanes.size())
{
const gd::RectF& rect = m_lanes[laneIndex].rect;
laneVisible = (rect.Y >= m_plotRect.Y - 0.5f) &&
(rect.Y + rect.Height <= m_plotRect.Y + m_plotRect.Height + 0.5f);
}
const std::wstring label = L"G" + std::to_wstring(m_hoverPanelBadgeLane + 1);
const std::wstring text =
label + (laneVisible ? L" · 已在视界内" : L" · 点击滚动到该网格");
const float padX = 8.0f * m_scale;
const float height = 22.0f * m_scale;
const float width = std::min(UiMeasureTextWidth(m_fonts->uiSmall, text) + padX * 2.0f,
std::max(10.0f, m_plotRect.Width - 4.0f * m_scale));
float left = layout.badge.X - 2.0f * m_scale;
left = std::max(m_plotRect.X + 2.0f * m_scale,
std::min(left, m_plotRect.X + m_plotRect.Width - width - 2.0f * m_scale));
// 默认贴在徽标下方;下方放不下就翻到上方(与弹出菜单的翻转规则一致)。
float top = layout.badge.Y + layout.badge.Height + 4.0f * m_scale;
if (top + height > m_plotRect.Y + m_plotRect.Height - 2.0f * m_scale)
{
top = layout.badge.Y - height - 4.0f * m_scale;
}
top = std::max(m_plotRect.Y + 2.0f * m_scale, top);
const gd::RectF bubble(left, top, width, height);
UiFillRoundRect(g, bubble, 4.0f * m_scale, m_theme->readoutBg);
UiDrawRoundRect(g, bubble, 4.0f * m_scale, m_theme->borderStrong, 1.0f);
UiText(g, text, m_fonts->uiSmall, bubble, m_theme->textStrong, TextAlign::CENTER);
}
void PlotView::PaintEditField(gd::Graphics& g)