预览曲线按面板高亮 + 统计数字不再卡在“统计中…” + 中心轴面板改表格 + 默认主题 Light + 标签页 DPI 修复 + 布局边界幽灵元素

一、预览曲线悬停高亮(只开弱峰+中心轴时无反应;三个全开时总亮均值曲线)
   根因:PaintLaneDerivedOverlays 里只有 MEAN 分支传 highlight,且悬停状态没有记录面板种类。
   修法:新增 m_meanRowHoverKind,高亮条件 = 悬停行 == 本行 && 面板种类 == 本曲线来源;
         条目栏强调色同样按种类门控;悬停种类进入面板缓存键。

二、统计数字松手后停在“统计中…”
   根因:交互期间面板缓存键被冻结(为拖动跟手),冻结期满后没有重绘请求(鼠标已停、
         统计也已完成),最后一次绘制仍是冻结帧。
   修法:记录 m_panelKeysFrozen 并公开 PanelRefreshPending();Render 末尾把它并入
         滚动动画定时器的武装条件 ⇒ 空闲后自动补一帧真实数字,随后停表。

三、「中心轴边沿计数」面板改为表格
   表头固定:信号名 / 上升沿计数 / 下降沿计数;每行底色 = 该信号配色(未勾选的行普通底色),
   三列与表头严格对齐,内容可滚动,单击行展开两行参数滑块。
   - EdgePanelLayout 新增 tableHeader / tableColumns[3];EdgePanelLayoutFor 新增 centerTable 分支;
   - 新增 EdgeCenterTableHeader() / EdgeCountCellText();EnsureEdgeCenterItem 默认 collapsed;
   - PanelSlotKey 的 CENTER 分支改为按 item 判断并混合计数(表格已没有 rowStats)。

四、默认主题改为 Light(Application、ThemeFor/ThemeName 默认分支、CurrentTheme 兜底)。

五、标签页文本在 DPI 切换后被省略
   根因:标签宽度恰好等于“内边距+文本+间距+关闭按钮”,无任何余量;TabItemWidget 缓存了
         Configure 时的 scale 并用它反推可用文本宽度,而 TabBar::SetScale 不重建控件树
         ⇒ 125%→100% 后可用宽度少约 4.5px ⇒ 吃掉末字。
   修法:SetScale 变化时重建控件树;并把文本可用矩形改由 ArrangeWidgets 统一下发
         (SetState 增加 textRect 参数),绘制侧不再自行反推。

六、布局边界里的幽灵元素
   - 新增 SignalPanel::HasStyleTarget();BuildBoxes 与 QueryCursor 在无样式目标时
     不再上报/响应色块、样式、调整、线宽、透明度等控件;
   - Paint 的“无网格”分支显式清空 m_legendHits / m_cursorLabelRects / m_cursorSpanRect /
     m_freeMarkerHits / m_panelSlots / m_panelOrder(关闭整个分组后不再残留边界框);
   - 浮动面板之上的滚动条 / 回到最上方 / 内联输入框改为按可见性上报几何。

验证(离屏探针,SW_HIDE + 合成消息,不占用鼠标键盘屏幕)
  .preflight/p21.exe 9/9   .preflight/p22.exe 14/14   .preflight/p23.exe 7/7
  .preflight/p20 系列(9/11/5)全部回归通过
  make release / make debug 零 warning;Release 813,568 B
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-02 15:22:26 +08:00
1 parent 7a6fcd68e2
commit 8de8a8fb85
14 files changed
+934 -141

No files matched your search

@@ -324,6 +324,19 @@ namespace jdi
*/
SignalStyle* StyleOf(const SignalRef& ref);
/**
* @brief 当前是否存在**样式编辑目标**(选中信号且其样式可解析)。
*
* ⚠️ 样式区的色块 / 样式 / 调整 / 线宽 / 透明度这些控件的矩形是**无条件**由
* `LayoutStyleControls()` 算出来的(布局与"是否有目标"无关)。若不在使用处按本判据
* 门控,就会出现用户 2026-11-02 报告的现象:什么都没选中时,打开「显示布局边界」
* 能看到那一排控件的框,鼠标移到「系数/偏移 | 镜像 | 绝对值」上还会变成手型。
*
* 使用处:`BuildBoxes`(几何上报)、`QueryCursor`(手型光标)。
* 绘制与命中早已按 `StyleOf(SelectedRef()) != nullptr` 门控,这里只是补齐另外两处。
*/
bool HasStyleTarget() const;
/**
* @brief 获取当前选中信号(跨标签页共享,因此切换视图后选中项保持一致)。
* @return 信号引用。
+46 -23
View File
@@ -100,34 +100,46 @@ namespace jdi
boxes.Add(ScrollThumbRect(), 3);
// ── 样式区:五行 + 每行内部的色块/按钮/滑轨/数值 ──────────────────
for (std::size_t index = 0; index < m_styleRowRects.size(); ++index)
// ⚠️ **只在存在样式编辑目标时登记控件几何**(用户 2026-11-02 报告):
// `LayoutStyleControls()` 无条件算出这些矩形,而绘制在“没有目标”时只画占位文字。
// 旧实现照样上报 ⇒ 空面板上打开「显示布局边界」会看到一整排并不存在的控件框。
const bool hasStyleTarget = HasStyleTarget();
if (hasStyleTarget)
{
boxes.Add(m_styleRowRects[index], 2);
for (std::size_t index = 0; index < m_styleRowRects.size(); ++index)
{
boxes.Add(m_styleRowRects[index], 2);
}
for (const gd::RectF& swatch : m_colorRects)
{
boxes.Add(swatch, 3);
}
boxes.Add(m_customChipRect, 3);
for (const gd::RectF& mode : m_modeRects)
{
boxes.Add(mode, 3);
}
for (const gd::RectF& adjust : m_adjustRects)
{
boxes.Add(adjust, 3);
}
boxes.Add(m_widthTrackRect, 3);
boxes.Add(m_widthValueRect, 3);
boxes.Add(m_widthResetRect, 3);
boxes.Add(m_opacityTrackRect, 3);
boxes.Add(m_opacityValueRect, 3);
boxes.Add(m_opacityResetRect, 3);
}
for (const gd::RectF& swatch : m_colorRects)
{
boxes.Add(swatch, 3);
}
boxes.Add(m_customChipRect, 3);
for (const gd::RectF& mode : m_modeRects)
{
boxes.Add(mode, 3);
}
for (const gd::RectF& adjust : m_adjustRects)
{
boxes.Add(adjust, 3);
}
boxes.Add(m_widthTrackRect, 3);
boxes.Add(m_widthValueRect, 3);
boxes.Add(m_widthResetRect, 3);
boxes.Add(m_opacityTrackRect, 3);
boxes.Add(m_opacityValueRect, 3);
boxes.Add(m_opacityResetRect, 3);
// ── 「目标」行(样式区顶部,展示当前选中信号) ────────────────────
// 这一行**无论是否选中**都会绘制(未选中时显示占位文本),所以本体总是登记;
// 但信号名 / GX 徽标只在实际有目标时存在。
boxes.Add(m_targetRowRect, 2);
boxes.Add(m_targetNameRect, 3);
boxes.Add(m_targetBadgeRect, 3);
if (hasStyleTarget)
{
boxes.Add(m_targetNameRect, 3);
boxes.Add(m_targetBadgeRect, 3);
}
// ── 列表里**当前可见**的行,以及每行内部的元素 ────────────────────
// 被滚出列表的行不登记,否则边界会糊到列表区之外。
@@ -1454,6 +1466,17 @@ namespace jdi
return (m_dataSetWritable != nullptr) ? m_dataSetWritable->Style(ref) : nullptr;
}
bool SignalPanel::HasStyleTarget() const
{
// 与 `PaintStyleSection` / `OnLButtonDown` 的判据**完全同源**:选中信号且样式可解析。
if (m_tab == nullptr || m_dataSet == nullptr)
{
return false;
}
const SignalRef& selected = SelectedRef();
return selected.IsValid() && StyleOf(selected) != nullptr;
}
const SignalRef& SignalPanel::SelectedRef() const
{
static const SignalRef fallback;
+20 -14
View File
@@ -55,21 +55,27 @@ namespace jdi
// 样式区的可点击控件(「调整」行三个按钮、线宽/透明度数值框)也用手型光标。
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
// 样式区顶部「目标」行的信号名与 GX 标签:与信号列表行一致,用手型光标提示可交互。
UpdateTargetRowGeometry();
if ((m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy)) ||
(m_targetBadgeRect.Width > 0.0f && UiPointInRect(m_targetBadgeRect, fx, fy)))
// ⚠️ 样式区控件只在**存在编辑目标**时才可点(用户 2026-11-02 报告:没有选中信号时,
// 鼠标掠过「系数/偏移 | 镜像 | 绝对值」仍会变成手型,而点了什么也不会发生)。
if (HasStyleTarget())
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) ||
UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) ||
UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy) || UiPointInRect(m_widthValueRect, fx, fy) ||
UiPointInRect(m_opacityValueRect, fx, fy) ||
(m_widthResetRect.Width > 0.0f && UiPointInRect(m_widthResetRect, fx, fy)) ||
(m_opacityResetRect.Width > 0.0f && UiPointInRect(m_opacityResetRect, fx, fy)))
{
return LoadCursorW(nullptr, IDC_HAND);
// 样式区顶部「目标」行的信号名与 GX 标签:与信号列表行一致,用手型光标提示可交互。
UpdateTargetRowGeometry();
if ((m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy)) ||
(m_targetBadgeRect.Width > 0.0f && UiPointInRect(m_targetBadgeRect, fx, fy)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) ||
UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) ||
UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy) ||
UiPointInRect(m_widthValueRect, fx, fy) ||
UiPointInRect(m_opacityValueRect, fx, fy) ||
(m_widthResetRect.Width > 0.0f && UiPointInRect(m_widthResetRect, fx, fy)) ||
(m_opacityResetRect.Width > 0.0f && UiPointInRect(m_opacityResetRect, fx, fy)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
}
return nullptr;
}