修复五项交互缺陷:折起后滚动条 / 单点信号样式 / 线宽实时生效 / 网格菜单限高滚动 / 点选信号滚动绘图区
1) 分组折起后再展开,信号列表的滚动条消失(筛选态与常规态均可复现)
- 根因:AdvanceGroupAnimations 只在“折起完成”时重建行集合;而“展开完成”那一帧
m_contentHeight 从收起态跳回展开态,却没有任何路径重算滚动条可见性 ⇒ 滚动条停在隐藏。
- 修法:任一方向动画结束都 m_rowsDirty = true + BuildRows(),并每帧 RefreshScrollBar(false)。
⚠️ 动画期间不夹取滚动偏移,避免打断与滚动同步的折起动画(scrollSynced)。
2) 单点信号(finiteCount == 1)默认绘制方式由「点线」改为「散点」
- ApplySinglePointDefaultStyle → DrawMode::SCATTER(仍取最大线宽)。
- 新增 SignalPanel::ModeOptionEnabled:单点信号的「线段」「点线」置灰、不可点、
且不参与悬停高亮(避免“看着能点其实点不动”);样式区的绘制与命中都用它。
- PlotView::UpdateAutoScatterStyles 跳过单点信号,否则“放大自动点线”会把它又改回「点线」。
3) 拖动线宽滑块完全不生效(拖动时线宽不变、松手瞬间变了、再移动鼠标又变回去)
- 根因一:AcquirePolyline 的折线缓存键没有 lineWidth,而 BuildPolyline 把线宽烘进
包络带几何(每个像素列的上下包络各向外扩 max(1,线宽)*scale*0.5,「梳状」判定也依赖它)
⇒ 稠密信号(采样数 > 绘图区宽度)永远复用旧线宽的顶点与包络带。
- 根因二:样式补丁只更新“静态场景缓存 + 后备缓冲区”,而每网格图层(宽基底)仍是旧样式像素,
下一帧命中“宽基底快路径”时把新样式覆盖回旧像素。
- 修法:PolylineCacheEntry 增加 lineWidth 并进比较;
样式补丁之后 QueueLaneRebuild(受影响网格) + RenderPendingLaneRebuilds(1)(沿用每帧至多 1 个的预算)。
4) 「目标」行的 GX 标签复用「所属网格」菜单,并先给 PopupMenu 增加「限高 + 条目区滚动 + 尾部固定项」
- MenuLayoutConstraint::maxHeight(OpenLaneMenu 传客户区高度的 62%);
- 固定区 = 最后一个分隔线及其之后的条目(即「新建网格」)始终钉在菜单底部,其余条目可滚动;
- 滚轮滚动条目区(Application 在菜单内滚动时不再关闭菜单,其余情形保持既有行为);
- 键盘移动悬停项自动滚入可视区(EnsureItemVisible);ItemAtPoint 要求条目同时命中滚动视口;
- 滚动区文本用 UiTextClipped(GDI 文本不遵循 GDI+ SetClip),并跳过完全不可见的条目(性能);
- BuildLayout 不再无条件清零 m_scrollOffset(否则 RebuildLayout 会把滚轮位置吃掉);
- 「目标」行 GX 标签点击**不参与**联动滚动(悬停联动保留)。
5) 点选信号名 ⇒ 绘图区自动把目标网格滚动到可视区内(上百个独立网格时旧有高亮无法定位)
- SignalPanel::RequestPlotLaneScroll / TakePlotLaneScrollRequest;
Application::ApplyPendingPlotLaneScroll(WM_LBUTTONDOWN 的面板分支);
PlotView::ScrollLaneIntoView(在可视区上方就上滚、在下方就下滚、已完整可见则不动)。
- 只有“点击信号名”触发;悬停绝不改变视图;勾选框 / GX 徽标 / 移除 ✕ 也不触发。
验证(全部离屏执行,窗口 SW_HIDE + 只注入合成消息 + 进程与线程 BELOW_NORMAL,
不移动真实光标、不抢焦点、不截屏):
- 新增 .preflight/check(不入库)68/68 通过;
- A/B:五处修复全部回退 ⇒ 17 项如实失败(B 4 / C 4 / D 7 / E 2);“折起后滚动条”需要
逐帧重绘驱动动画才能复现,单独回退时 ⇒ 5 项如实失败;恢复后 68/68;
- make release / make debug 零警告。
其它:
- 按要求删除工作区 Tools/(structcheck 脚本)并移除 Makefile 的 structcheck 目标;
- Documents/agent_mem.md 同步:新增 §9.38、§11.2 第 97–103 条与变更日志一行。
This commit is contained in:
1 parent
9cf6b2fedc
commit
2904699eae
19 files changed
+743
-350
No files matched your search
@@ -278,6 +278,17 @@ namespace jdi
|
||||
*/
|
||||
bool ApplyPendingPanelScroll();
|
||||
|
||||
/**
|
||||
* @brief 应用信号面板提交的「把绘图区滚动到目标网格」的联动请求。
|
||||
*
|
||||
* 用户 2026-10-16 要求:点击信号选择区域的**信号名**(或样式区「目标」行的信号名)时,
|
||||
* 若该信号所在的网格不在绘图区可视区内,就自动调节绘图区的滚动条让该网格完整出现
|
||||
*(上百个独立网格时,仅靠网格高亮无法定位到画面之外的网格)。悬停不触发。
|
||||
*
|
||||
* @return 存在请求并已应用返回 true。
|
||||
*/
|
||||
bool ApplyPendingPlotLaneScroll();
|
||||
|
||||
/**
|
||||
* @brief 派生信号构建请求(多次迭代均值滤波 / 弱峰调整共用同一套插入流程)。
|
||||
*/
|
||||
|
||||
@@ -603,9 +603,27 @@ namespace jdi
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSEWHEEL:
|
||||
{
|
||||
// 可滚动菜单(条目过多、按最大高度截断的菜单,如「所属网格」):
|
||||
// 鼠标在菜单内时滚轮用来翻动条目区,**不关闭菜单**;其余情形仍是
|
||||
// “滚轮关闭菜单”(保持既有行为)。
|
||||
POINT wheelPoint = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
|
||||
ScreenToClient(hwnd, &wheelPoint);
|
||||
if (m_menu.IsScrollable() && m_menu.ContainsPoint(wheelPoint.x, wheelPoint.y))
|
||||
{
|
||||
if (m_menu.OnMouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)))
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
m_menu.Close();
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
case WM_RBUTTONDOWN:
|
||||
case WM_MBUTTONDOWN:
|
||||
case WM_MOUSEWHEEL:
|
||||
m_menu.Close();
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
@@ -966,6 +984,8 @@ namespace jdi
|
||||
m_activeTarget = Target::PANEL;
|
||||
if (m_panel.OnLButtonDown(x, y))
|
||||
{
|
||||
// 点击信号名 ⇒ 让绘图区把目标网格滚动到可视区内(用户 2026-10-16 要求)。
|
||||
ApplyPendingPlotLaneScroll();
|
||||
InvalidateRect(hwnd, nullptr, TRUE);
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -794,6 +794,17 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::ApplyPendingPlotLaneScroll()
|
||||
{
|
||||
int laneIndex = -1;
|
||||
if (!m_panel.TakePlotLaneScrollRequest(laneIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 目标网格不在绘图区可视区内时才真正滚动(已完整可见则不动视图)。
|
||||
return m_plot.ScrollLaneIntoView(laneIndex);
|
||||
}
|
||||
|
||||
bool Application::ApplyPendingDerivedBuild()
|
||||
{
|
||||
// 两类「构建为新信号」请求(均值滤波 / 弱峰调整)机制完全一致,
|
||||
|
||||
@@ -923,6 +923,23 @@ namespace jdi
|
||||
*/
|
||||
bool TakePanelScrollRequest(SignalRef& ref);
|
||||
|
||||
/**
|
||||
* @brief 把指定网格纵向滚动到**完整可见**(网格不在可视区时自动调节滚动条)。
|
||||
*
|
||||
* 用户 2026-10-16 要求:信号很多时(例如把 MF4 的上百条信号各自放进独立网格)
|
||||
* 每个网格只占绘图区的一小段,绝大多数网格都在可视区之外 —— 在信号列表里点选一条
|
||||
* 信号后,用户无法从画面上的高亮看出它究竟在哪个网格。因此点选信号时:
|
||||
* * 目标网格在可视区**上方** ⇒ 向上滚动直到它的上边界进入绘图区上沿;
|
||||
* * 目标网格在可视区**下方** ⇒ 向下滚动直到它的下边界进入绘图区下沿;
|
||||
* * 已经完整可见 ⇒ 不动(避免无意义地改变视图)。
|
||||
*
|
||||
* ⚠️ 只在**点击**信号时调用(悬停只做高亮联动,绝不改变视图)。
|
||||
*
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @return 视图(网格滚动量)发生变化返回 true。
|
||||
*/
|
||||
bool ScrollLaneIntoView(int laneIndex);
|
||||
|
||||
/**
|
||||
* @brief 处理编辑态下的字符输入。
|
||||
* @param [IN] character 输入字符。
|
||||
@@ -2345,6 +2362,16 @@ namespace jdi
|
||||
bool mirrorHorizontal = false; /**< 是否水平镜像(取值下标反序)。 */
|
||||
float offsetY = 0.0f; /**< Y 轴偏移。 */
|
||||
double offsetTime = 0.0; /**< 时间轴偏移(秒)。 */
|
||||
/**
|
||||
* @brief 线宽(逻辑像素)。
|
||||
*
|
||||
* ⚠️ **必须进缓存键**:`BuildPolyline` 会把线宽**烘进包络带几何**
|
||||
* (`strokeHalf = max(1, 线宽) * scale * 0.5`,逐列上下包络各向外扩这么多),
|
||||
* 且「梳状」判定也依赖扩展后的列跨度。旧实现漏了这一项 ⇒ 改变线宽时
|
||||
* 稠密信号会命中旧条目、一直按**旧线宽**绘制(用户 2026-10-16 报告:
|
||||
* 拖动线宽滑块时信号线宽不变,松开后再移动鼠标又变回去)。
|
||||
*/
|
||||
float lineWidth = 1.0f;
|
||||
std::vector<gd::PointF> points; /**< 折线顶点。 */
|
||||
std::vector<std::size_t> breaks; /**< 每段折线在 points 中的起始下标。 */
|
||||
|
||||
|
||||
@@ -180,6 +180,48 @@ namespace jdi
|
||||
Tab().LaneScroll() = static_cast<float>(ClampDouble(Tab().LaneScroll(), 0.0, maxOffset));
|
||||
}
|
||||
|
||||
bool PlotView::ScrollLaneIntoView(int laneIndex)
|
||||
{
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()) || m_plotRect.Height <= 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// ⚠️ 不能直接用 `laneIndex * 网格高`:网格之间虽然目前没有间距,但内容高度会随
|
||||
// “最小网格高”夹取而大于绘图区高,公式里已经含有 `- LaneScroll()`。
|
||||
// 从当前绘制位置反推**内容偏移**最稳妥:rect.Y = plotRect.Y + contentTop - LaneScroll()。
|
||||
const gd::RectF& rect = m_lanes[static_cast<std::size_t>(laneIndex)].rect;
|
||||
const float contentTop = rect.Y - m_plotRect.Y + Tab().LaneScroll();
|
||||
const float laneHeight = rect.Height;
|
||||
|
||||
const float previous = Tab().LaneScroll();
|
||||
float scroll = previous;
|
||||
if (contentTop - scroll < 0.0f)
|
||||
{
|
||||
// 目标网格在可视区上方:把它的上边界对齐到绘图区上沿。
|
||||
scroll = contentTop;
|
||||
}
|
||||
else if (contentTop + laneHeight - scroll > m_plotRect.Height)
|
||||
{
|
||||
// 目标网格在可视区下方:把它的下边界对齐到绘图区下沿。
|
||||
scroll = contentTop + laneHeight - m_plotRect.Height;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false; // 已经完整可见
|
||||
}
|
||||
|
||||
Tab().LaneScroll() = scroll;
|
||||
ClampLaneScroll();
|
||||
if (std::fabs(Tab().LaneScroll() - previous) <= 0.01f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 网格滚动量参与场景/图层指纹,因此重建可视几何(各网格 rect 与 yMin/yMax 依赖滚动量)。
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::LaneSpansMultipleGroups(int laneIndex) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
|
||||
@@ -2916,6 +2916,17 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
|
||||
// ⚠️ **只有一个有效采样点**的信号**不参与**自动切换:
|
||||
// 它们的初始样式固定为「散点」(见 `ApplySinglePointDefaultStyle`),而信号面板的
|
||||
// 「线段」/「点线」按钮已对其置灰(用户 2026-10-16 要求)——
|
||||
// 若这里再自动写成「点线」,用户会看到一个“已置灰却仍处于点线态”的矛盾状态。
|
||||
if (data->finiteCount == 1)
|
||||
{
|
||||
style->autoScatterActive = false;
|
||||
style->autoScatterUserOverride = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ⚠️ **先**判定“用户是否手动改过绘制方式”:本机制只会写入「点线」,
|
||||
// 所以自动启用期间 mode 不等于「点线」只可能是用户改的。
|
||||
// 这一步必须在下面的分支之前做 —— 用户改完样式可能**紧接着就缩出去**
|
||||
|
||||
@@ -175,6 +175,9 @@ namespace jdi
|
||||
const bool mirrorHorizontal = (resolved.style != nullptr) && resolved.style->mirrorHorizontal;
|
||||
const float offsetY = (resolved.style != nullptr) ? resolved.style->offsetY : 0.0f;
|
||||
const double offsetTime = (resolved.style != nullptr) ? resolved.style->offsetTime : 0.0;
|
||||
// ⚠️ 线宽也必须进键:`BuildPolyline` 把它烘进包络带几何(逐列外扩半个线宽)
|
||||
//(详见 `PolylineCacheEntry::lineWidth` 的说明;漏掉它会让线宽改动在**稠密信号**上不生效)。
|
||||
const float lineWidth = (resolved.style != nullptr) ? resolved.style->lineWidth : 1.0f;
|
||||
|
||||
for (const PolylineCacheEntry& entry : m_polylineCache)
|
||||
{
|
||||
@@ -184,7 +187,8 @@ namespace jdi
|
||||
entry.laneHeight == lane.rect.Height && entry.laneYMin == lane.yMin &&
|
||||
entry.laneYMax == lane.yMax && entry.gain == gain && entry.absolute == absolute &&
|
||||
entry.mirrorVertical == mirrorVertical && entry.mirrorHorizontal == mirrorHorizontal &&
|
||||
entry.offsetY == offsetY && entry.offsetTime == offsetTime)
|
||||
entry.offsetY == offsetY && entry.offsetTime == offsetTime &&
|
||||
entry.lineWidth == lineWidth)
|
||||
{
|
||||
return &entry;
|
||||
}
|
||||
@@ -215,6 +219,7 @@ namespace jdi
|
||||
entry.mirrorHorizontal = mirrorHorizontal;
|
||||
entry.offsetY = offsetY;
|
||||
entry.offsetTime = offsetTime;
|
||||
entry.lineWidth = lineWidth;
|
||||
|
||||
BuildPolyline(resolved, lane, entry.points, entry.breaks, entry.band, entry.bandBreaks,
|
||||
entry.bandMode, entry.bandSpiky);
|
||||
@@ -924,6 +929,17 @@ namespace jdi
|
||||
m_fastFrame = true;
|
||||
RepaintSceneCachePatch(g, patchLanes);
|
||||
m_sceneDegraded = true;
|
||||
// ⚠️ **同步重建受影响的宽基底图层**:补丁只更新了“静态场景缓存 + 后备缓冲区”,
|
||||
// 而每网格图层(宽基底)里仍是**旧样式**的像素。若不在这里重建,紧接着命中
|
||||
// “宽基底快路径”的那一帧会把刚画好的新样式**覆盖回旧像素** ——
|
||||
// 用户报告:拖动线宽滑块期间信号一直按旧的线宽绘制、松手后刷新,
|
||||
// 再移动鼠标又变回旧线宽(正是这个覆盖造成的)。
|
||||
// 按既有预算机制重建(每帧至多 1 个网格,不会在交互帧里做整批重建)。
|
||||
for (const int laneIndex : patchLanes)
|
||||
{
|
||||
QueueLaneRebuild(laneIndex);
|
||||
}
|
||||
RenderPendingLaneRebuilds(1);
|
||||
paintOverlays();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,6 +199,28 @@ namespace jdi
|
||||
*/
|
||||
bool EndFilterEdit();
|
||||
|
||||
/**
|
||||
* @brief 取出并清除「请把绘图区滚动到该网格」的联动请求。
|
||||
*
|
||||
* 用户 2026-10-16 要求:点击信号选择区域的**信号名**(或样式区「目标」行的信号名)时,
|
||||
* 若该信号所在网格不在绘图区可视范围内,则自动调节绘图区的滚动条让该网格完整出现。
|
||||
* 悬停时**不做**这个滚动(只保留既有的网格高亮联动)。
|
||||
*
|
||||
* @param [OUT] laneIndex 需要滚动到可视区内的网格下标。
|
||||
* @return 存在待处理的请求返回 true。
|
||||
*/
|
||||
bool TakePlotLaneScrollRequest(int& laneIndex)
|
||||
{
|
||||
if (!m_plotLaneRequestFlag)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_plotLaneRequestFlag = false;
|
||||
laneIndex = m_pendingPlotLane;
|
||||
m_pendingPlotLane = -1;
|
||||
return laneIndex >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 向当前输入框送入一个字符。
|
||||
* @param [IN] character 输入字符('\r' 提交、'\b' 退格、0x03 复制)。
|
||||
@@ -644,8 +666,11 @@ namespace jdi
|
||||
|
||||
/**
|
||||
* @brief 依据内容高度重算滚动条可见性与列表可用宽度。
|
||||
* @param [IN] clampScroll 是否顺带把滚动偏移夹到合法范围。
|
||||
* ⚠️ 分组折起/展开动画期间必须传 `false`:与滚动同步的折起动画正按自己的进度
|
||||
* 推进 `m_scrollOffset`,此时夹取会打断它(折起后内容比列表还短时尤其明显)。
|
||||
*/
|
||||
void RefreshScrollBar();
|
||||
void RefreshScrollBar(bool clampScroll = true);
|
||||
|
||||
/**
|
||||
* @brief 限制滚动偏移量到合法范围。
|
||||
@@ -992,6 +1017,23 @@ namespace jdi
|
||||
*/
|
||||
bool ApplyOpacityFromX(float x);
|
||||
|
||||
/**
|
||||
* @brief 断言某「样式」选项对当前目标信号是否可用(不可用时置灰且不可点)。
|
||||
*
|
||||
* ⚠️ **只有一个有效采样点**的信号固定使用「散点」:单个数据点在「线段」下根本画不出来,
|
||||
* 而「点线」与「放大自动点线」机制语义重叠,因此这两个选项对单点信号置灰
|
||||
*(用户 2026-10-16 要求)。
|
||||
* @param [IN] optionIndex 选项下标(与 `kModeOptions` 同序)。
|
||||
* @return 可用返回 true。
|
||||
*/
|
||||
bool ModeOptionEnabled(int optionIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 登记「点击信号 ⇒ 请宿主把绘图区滚动到该网格」的联动请求。
|
||||
* @param [IN] ref 被点击的信号。
|
||||
*/
|
||||
void RequestPlotLaneScroll(const SignalRef& ref);
|
||||
|
||||
/**
|
||||
* @brief 依据线宽滑块横坐标调整当前信号线宽。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
@@ -1144,6 +1186,11 @@ namespace jdi
|
||||
bool m_pendingColorPicker = false; /**< 左键抬起时是否弹出拾色器。 */
|
||||
bool m_pendingGainMenu = false; /**< 左键抬起时是否弹出「系数」的二级面板。 */
|
||||
bool m_pendingMirrorMenu = false; /**< 左键抬起时是否弹出「镜像」的二级面板。 */
|
||||
bool m_pendingTargetLaneMenu = false; /**< 左键抬起时是否弹出「目标」行 GX 标签的网格菜单。 */
|
||||
|
||||
/** 待宿主处理的「把绘图区滚动到该网格」请求(见 `TakePlotLaneScrollRequest`)。 */
|
||||
bool m_plotLaneRequestFlag = false;
|
||||
int m_pendingPlotLane = -1;
|
||||
|
||||
/**
|
||||
* @brief 待确认的「关闭类」按钮(✕)。
|
||||
|
||||
@@ -48,6 +48,9 @@ namespace jdi
|
||||
m_groupAnims.clear();
|
||||
m_rowShift.clear();
|
||||
m_scrollAnimActive = false;
|
||||
m_pendingTargetLaneMenu = false;
|
||||
m_plotLaneRequestFlag = false;
|
||||
m_pendingPlotLane = -1;
|
||||
}
|
||||
|
||||
void SignalPanel::SetScale(float scale)
|
||||
@@ -453,7 +456,7 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
void SignalPanel::RefreshScrollBar()
|
||||
void SignalPanel::RefreshScrollBar(bool clampScroll)
|
||||
{
|
||||
if (m_listFullWidth <= 0.0f)
|
||||
{
|
||||
@@ -475,7 +478,10 @@ namespace jdi
|
||||
m_scrollBarRect = gd::RectF();
|
||||
}
|
||||
|
||||
ClampScroll();
|
||||
if (clampScroll)
|
||||
{
|
||||
ClampScroll();
|
||||
}
|
||||
}
|
||||
|
||||
void SignalPanel::ClampScroll()
|
||||
@@ -812,6 +818,41 @@ namespace jdi
|
||||
return m_tab->LaneOfSignal(SelectedRef());
|
||||
}
|
||||
|
||||
bool SignalPanel::ModeOptionEnabled(int optionIndex) const
|
||||
{
|
||||
if (optionIndex < 0 || optionIndex >= kModeOptionCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 单点信号固定用「散点」(见函数说明):其余两种绘制方式置灰且不可点。
|
||||
if (m_dataSet == nullptr || !SelectedRef().IsValid())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const SignalData* signal = m_dataSet->Resolve(SelectedRef());
|
||||
if (signal == nullptr || signal->finiteCount != 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return kModeOptions[optionIndex] == DrawMode::SCATTER;
|
||||
}
|
||||
|
||||
void SignalPanel::RequestPlotLaneScroll(const SignalRef& ref)
|
||||
{
|
||||
if (m_tab == nullptr || !ref.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 未勾选绘制到网格时 `LaneOfSignal` 返回 -1 ⇒ 不产生请求(没什么好滚的)。
|
||||
const int lane = m_tab->LaneOfSignal(ref);
|
||||
if (lane < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_pendingPlotLane = lane;
|
||||
m_plotLaneRequestFlag = true;
|
||||
}
|
||||
|
||||
void SignalPanel::UpdateTargetRowGeometry()
|
||||
{
|
||||
if (m_fonts == nullptr || m_dataSet == nullptr)
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
const std::uint64_t now = GetTickCount64();
|
||||
bool rowsBecameStale = false;
|
||||
bool animationFinished = false;
|
||||
for (std::size_t index = 0; index < m_groupAnims.size();)
|
||||
{
|
||||
const GroupAnim& anim = m_groupAnims[index];
|
||||
@@ -117,12 +117,7 @@ namespace jdi
|
||||
{
|
||||
m_scrollOffset = anim.scrollTo;
|
||||
}
|
||||
// 折起完成后该组的信号行要**真正移除** ⇒ 行集合需重建
|
||||
//(展开完成后行集合本来就是展开态,无需重建;平移量归零即可)。
|
||||
if (anim.collapsing)
|
||||
{
|
||||
rowsBecameStale = true;
|
||||
}
|
||||
animationFinished = true;
|
||||
m_groupAnims.erase(m_groupAnims.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
}
|
||||
else
|
||||
@@ -131,13 +126,26 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️ **不设置 `m_rowsDirty`**:行高不随动画变化,只需重算逐行位移与内容高度 ——
|
||||
// ⚠️ **动画结束(折起与展开都要)后必须重建行集合**:
|
||||
// * 折起:该组的信号行要真正从行集合里移除;
|
||||
// * 展开:行集合不变,但 `m_contentHeight` 会在这一帧从“收起态”跳回“展开态”,
|
||||
// 而滚动条可见性只在 `EnsureRows` / `Layout` 里刷新 ⇒ 不重建的话滚动条会
|
||||
// 一直停在“隐藏”状态(用户报告:先折起再展开后,信号列表的滚动条消失)。
|
||||
// ⚠️ **不设置 `m_rowsDirty`**(动画进行中):行高不随动画变化,只需重算逐行位移与内容高度 ——
|
||||
// 这正是本动画方案比“逐行缩放”流畅得多的原因(无需每帧重建行集合)。
|
||||
UpdateAnimGeometry();
|
||||
if (rowsBecameStale)
|
||||
if (animationFinished)
|
||||
{
|
||||
m_rowsDirty = true;
|
||||
BuildRows(); // 内部会重算内容高度与逐行位移
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateAnimGeometry();
|
||||
}
|
||||
// 内容高度随动画逐帧变化 ⇒ 滚动条的可见性与列表可用宽度也要同步刷新(否则动画期间
|
||||
// 列表宽度/滚动条与内容不匹配)。⚠️ 不夹取滚动偏移:与滚动同步的折起动画正按自己的进度
|
||||
// 推进 `m_scrollOffset`,此时夹取会打断它。
|
||||
RefreshScrollBar(false);
|
||||
return !m_groupAnims.empty();
|
||||
}
|
||||
|
||||
@@ -1038,6 +1046,7 @@ namespace jdi
|
||||
m_pendingFilterClear = false;
|
||||
m_pendingGainMenu = false;
|
||||
m_pendingMirrorMenu = false;
|
||||
m_pendingTargetLaneMenu = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -1120,7 +1129,9 @@ namespace jdi
|
||||
{
|
||||
for (int index = 0; index < kModeOptionCount; ++index)
|
||||
{
|
||||
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
|
||||
// 置灰的选项(单点信号的「线段」/「点线」)不参与悬停高亮 ——
|
||||
// 它们点不动,高亮会让用户以为可以点击。
|
||||
if (ModeOptionEnabled(index) && UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
|
||||
{
|
||||
hoverStyle = StyleHit::MODE;
|
||||
hoverStyleIndex = index;
|
||||
@@ -1255,6 +1266,7 @@ namespace jdi
|
||||
m_pendingColorPicker = false;
|
||||
m_pendingGainMenu = false;
|
||||
m_pendingMirrorMenu = false;
|
||||
m_pendingTargetLaneMenu = false;
|
||||
|
||||
// 点击当前输入框以外的位置时提交输入。
|
||||
if (m_editField != StyleEditField::NONE &&
|
||||
@@ -1274,18 +1286,27 @@ namespace jdi
|
||||
// 样式编辑区。
|
||||
if (SignalStyle* style = StyleOf(SelectedRef()))
|
||||
{
|
||||
// ---- 顶层「目标」行:单击**不进入编辑**(用户明确要求),只做联动滚动 ----
|
||||
// 双击改名由 `OnDoubleClick` 处理;这里单击即把列表滚动到目标信号,
|
||||
// 便于在长列表(上百条信号的 MF4)里定位正在编辑的那条信号。
|
||||
// ---- 顶层「目标」行 ----
|
||||
// 信号名:单击**不进入编辑**(用户明确要求),只做联动(列表滚动 + 绘图区滚动到该网格);
|
||||
// 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 (onName || onBadge)
|
||||
if (onBadge)
|
||||
{
|
||||
// 与网格徽标一致:按下只登记,**左键抬起**且指针仍在同一个徽标上时才展开菜单。
|
||||
m_pendingTargetLaneMenu = true;
|
||||
return true;
|
||||
}
|
||||
if (onName)
|
||||
{
|
||||
ScrollToSignalAnimated(SelectedRef());
|
||||
RequestPlotLaneScroll(SelectedRef());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1314,6 +1335,11 @@ namespace jdi
|
||||
{
|
||||
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;
|
||||
@@ -1429,6 +1455,12 @@ namespace jdi
|
||||
m_pendingLaneMenu = true;
|
||||
m_pendingLaneRef = ref;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 点击信号名(行主体):除了选中它,还要让绘图区的**目标网格**完整出现在可视区内
|
||||
//(上百个独立网格时,旧有高亮根本无法定位)。悬停不触发这一步。
|
||||
RequestPlotLaneScroll(ref);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1550,6 +1582,22 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 「目标」行的 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;
|
||||
}
|
||||
|
||||
@@ -1859,7 +1907,13 @@ namespace jdi
|
||||
items.push_back(create);
|
||||
|
||||
const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
|
||||
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items);
|
||||
// ⚠️ 网格可能是**上百个**(例如把 MF4 的全部信号各自放进独立网格):
|
||||
// 不限制高度时菜单会长到屏幕之外、且逐条绘制上百行会明显卡顿。
|
||||
// 这里限制高度(最多约占客户区高度的 62%):超出的部分可滚动翻页,
|
||||
// 而最后一个分隔线与它下面的「新建网格」**始终钉在菜单底部**(用户 2026-10-16 要求)。
|
||||
MenuLayoutConstraint constraint;
|
||||
constraint.maxHeight = std::max(180.0f, bounds.Height * 0.62f);
|
||||
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items, false, constraint);
|
||||
}
|
||||
|
||||
bool SignalPanel::OnMenuCommand(const MenuResult& result)
|
||||
|
||||
@@ -539,20 +539,26 @@ namespace jdi
|
||||
}
|
||||
|
||||
// ---- 第二行:样式(线段 / 散点 / 点线)----
|
||||
// ⚠️ 单点信号固定使用「散点」:「线段」在单个数据点上完全画不出来、「点线」与
|
||||
// 「放大自动点线」语义重叠,因此它们对单点信号**置灰且不可点**(用户 2026-10-16 要求)。
|
||||
{
|
||||
paintRowLabel(1, L"样式");
|
||||
|
||||
for (int index = 0; index < kModeOptionCount; ++index)
|
||||
{
|
||||
const gd::RectF& rect = m_modeRects[static_cast<std::size_t>(index)];
|
||||
const bool enabled = ModeOptionEnabled(index);
|
||||
const bool active = (style->mode == kModeOptions[index]);
|
||||
const bool hovered = (m_hoverStyle == StyleHit::MODE && m_hoverStyleIndex == index);
|
||||
const bool hovered =
|
||||
enabled && (m_hoverStyle == StyleHit::MODE && m_hoverStyleIndex == index);
|
||||
|
||||
UiFillRoundRect(g, rect, kCorner * m_scale,
|
||||
active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg));
|
||||
UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f);
|
||||
UiText(g, DrawModeName(kModeOptions[index]), m_fonts->uiSmall, rect,
|
||||
active ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||
!enabled ? m_theme->textMuted
|
||||
: (active ? m_theme->textStrong : m_theme->text),
|
||||
TextAlign::CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user