自由标记体验重做 + 全部实验性面板可折起
- 网格内固定点:取值胶囊按已放置矩形做纵向避让(不再互相遮挡);行尾 ✕ 改为与 胶囊同高的正方形;移除固定点的纵向半透明竖线 - 新增「标记透明度」(0–100%),作用于本网格全部固定点的圆点/胶囊/描边/文字/✕; 半透明时文字改走 GDI+(GDI DrawTextW 忽略 alpha),100% 时像素零变化; ≤2% 时不绘制且不登记 ✕ 命中矩形(看不见的按钮不可点) - 面板按信号分组:信号色托底分组栏(展开三角 + 信号名 + N 标记点 + ✕,文字/三角/✕ 一律用自适应对比色),点击分组栏收起/展开;条目五列(值/枚举/增量/时刻/间隔), 首项增列与 Δt 显示 N/A,整个面板没有枚举文本时枚举列整列缺省;行尾仍有 ✕ - 面板内容超高时右侧出现滚动条(分组栏与条目一起滚动),滚轮在内容区翻页; 底部按钮与「标记透明度」滑块固定在底部、不参与滚动 - 底部按钮改为「光标选取模式」切换类按钮(外观与工具栏切换按钮一致,激活高亮), 激活时其上方出现「标记透明度」滑动条(与信号样式区的透明度滑块同款) - 所有实验性功能浮动面板支持展开/折起:标题栏 GX 徽标左侧新增「三角 + 横线」指示器, 单击切换、双击标题栏切换;折起的面板只有标题栏 - 数据层:LanePanelState.collapsed、Lane.freeMarkerOpacity/freeMarkerScroll/ freeMarkerCollapsedGroups,LaneCarry 与 SplitLane 一并继承 - 验证:新增离屏 .preflight/perf/mark2check 69/69(含两组 A/B);回归 datacheck 69/69、 verify 49/49、panelanim 158/158、errcheck 19/19;make release/debug 零警告
This commit is contained in:
1 parent
067128f46c
commit
3400ac7c29
9 files changed
+1225
-202
No files matched your search
@@ -57,7 +57,9 @@ namespace jdi
|
||||
EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */
|
||||
PEAK_LEVEL, /**< 拖动「弱峰调整」某信号的幅度滑块(-400% – +400%)。 */
|
||||
PEAK_ITERATIONS, /**< 拖动「弱峰调整」某信号的迭代次数滑块(1 … 该信号自定义上限)。 */
|
||||
LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
|
||||
LANE_REORDER, /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
|
||||
MARKER_OPACITY, /**< 拖动「自由标记」面板的「标记透明度」滑块。 */
|
||||
MARKER_SCROLL /**< 拖动「自由标记」面板内容的滚动条滑块。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -969,6 +971,46 @@ namespace jdi
|
||||
*/
|
||||
bool ToggleLaneFreeMarkerCursor(int laneIndex);
|
||||
|
||||
/**
|
||||
* @brief 设置某网格「自由标记」固定点的**显示不透明度**(0–1)。
|
||||
*
|
||||
* 只影响网格内固定数据点的显示内容(数据点 / 取值胶囊 / ✕),
|
||||
* 面板自身的像素与交互不受影响。
|
||||
*/
|
||||
bool SetLaneFreeMarkerOpacity(int laneIndex, float opacity);
|
||||
|
||||
/**
|
||||
* @brief 设置某网格「自由标记」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||||
* @return 偏移真的发生变化返回 true。
|
||||
*/
|
||||
bool SetLaneFreeMarkerScroll(int laneIndex, float scroll);
|
||||
|
||||
/**
|
||||
* @brief 切换某网格「自由标记」面板里某个信号分组的收起状态。
|
||||
* @return 切换后为「已收起」返回 true。
|
||||
*/
|
||||
bool ToggleLaneFreeMarkerGroup(int laneIndex, const SignalRef& ref);
|
||||
|
||||
/**
|
||||
* @brief 切换某个实验性功能浮动面板的**折起**状态(只显示标题栏)。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @param [IN] kind 面板种类。
|
||||
* @param [IN] collapsed 是否折起。
|
||||
* @return 状态发生变化返回 true。
|
||||
*/
|
||||
bool SetLanePanelCollapsed(int laneIndex, EdgePanelKind kind, bool collapsed);
|
||||
|
||||
/**
|
||||
* @brief 查询某个实验性功能浮动面板是否处于折起状态。
|
||||
*/
|
||||
bool LanePanelCollapsed(int laneIndex, EdgePanelKind kind) const;
|
||||
|
||||
/**
|
||||
* @brief 切换某个实验性功能浮动面板的折起状态。
|
||||
* @return 切换后的状态(true = 已折起)。
|
||||
*/
|
||||
bool ToggleLanePanelCollapsed(int laneIndex, EdgePanelKind kind);
|
||||
|
||||
/**
|
||||
* @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
@@ -1595,6 +1637,15 @@ namespace jdi
|
||||
gd::RectF panel; /**< 面板整体矩形。 */
|
||||
gd::RectF title; /**< 标题栏(**唯一可拖拽区域**)。 */
|
||||
gd::RectF close; /**< ✕ 关闭按钮。 */
|
||||
/**
|
||||
* @brief 标题栏左侧的「展开 / 折起」三角指示器(三角 + 后方一条横线)。
|
||||
*
|
||||
* 单击它切换面板的折起状态;折起后面板只剩标题栏(内容区完全省略)。
|
||||
* 它位于「GX」编号徽标的**左边**。
|
||||
*/
|
||||
gd::RectF disclosure;
|
||||
/** 面板当前是否处于折起状态(只显示标题栏)。 */
|
||||
bool collapsed = false;
|
||||
/**
|
||||
* @brief 标题栏左侧的「GX」编号徽标。
|
||||
*
|
||||
@@ -1654,6 +1705,35 @@ namespace jdi
|
||||
};
|
||||
/** 「自由标记」:每个固定点一行(按信号分组、组内按时间升序展开后的顺序)。 */
|
||||
std::vector<MarkerRow> markerRows;
|
||||
/**
|
||||
* @brief 「自由标记」的一个**信号分组栏**(以信号色为托底底色)。
|
||||
*
|
||||
* 左侧 = 展开/收起三角 + 信号名;右侧 = 标记点个数(`N 标记点`)+ ✕
|
||||
*(清除该信号全部固定点)。单击分组栏(✕ 除外)切换收起状态。
|
||||
* 只有至少有一个固定点的信号才会有分组栏。
|
||||
*/
|
||||
struct MarkerGroup
|
||||
{
|
||||
SignalRef ref;
|
||||
gd::RectF header; /**< 分组栏托底矩形。 */
|
||||
gd::RectF triangle; /**< 展开/收起三角的命中矩形。 */
|
||||
gd::RectF close; /**< 右侧✕的命中矩形。 */
|
||||
bool collapsed = false;
|
||||
};
|
||||
/** 「自由标记」:信号分组栏(顺序与面板显示顺序一致)。 */
|
||||
std::vector<MarkerGroup> markerGroups;
|
||||
/**
|
||||
* @brief 「自由标记」面板的**可滚动内容区**(分组栏 + 固定点条目)。
|
||||
*
|
||||
* 内容高度超过它时出现上下滚动条(不参与滚动的按钮与透明度滑块在它下方)。
|
||||
*/
|
||||
gd::RectF content;
|
||||
/** 「自由标记」面板的**内容总高度**(含被滚动出去的条目),用于换算滚动比例。 */
|
||||
float markerContentHeight = 0.0f;
|
||||
gd::RectF scrollTrack; /**< 内容区右侧的滚动条轨道(不需要滚动时为空矩形)。 */
|
||||
gd::RectF scrollThumb; /**< 滚动条滑块。 */
|
||||
gd::RectF opacityTrack; /**< 「标记透明度」滑动条轨道(仅光标模式激活时非空)。 */
|
||||
gd::RectF opacityValue; /**< 「标记透明度」数值文本矩形。 */
|
||||
/**
|
||||
* @brief 「自由标记」:每个信号名右侧的「清除该信号全部固定点」✕。
|
||||
*
|
||||
@@ -1667,25 +1747,29 @@ namespace jdi
|
||||
*/
|
||||
std::vector<gd::RectF> rowNameRects;
|
||||
/**
|
||||
* @brief 「自由标记」面板的四列文本区(① 取值 ② 增量 ③ 时刻 ④ 间隔)。
|
||||
* @brief 「自由标记」面板的五列文本区
|
||||
* (① 取值 ② 枚举文本 ③ 增量 ④ 时刻 ⑤ 间隔)。
|
||||
*
|
||||
* 每行都在这四列的**同一横坐标**上左对齐输出,因此同一信息在纵向上严格对齐
|
||||
*(用户 2026-10-20 明确要求:"将每个信息元素纵向对齐")。
|
||||
* 每行都在这五列的**同一横坐标**上左对齐输出,因此同一信息在纵向上严格对齐。
|
||||
* 列宽固定(不按文本测量),绘制时用 `UiEllipsize` 截断,避免布局期查询采样。
|
||||
* 当整个面板里没有任何枚举文本时,② 枚举列的宽度为 0(整列缺省)。
|
||||
*/
|
||||
struct MarkerColumn
|
||||
{
|
||||
float left = 0.0f;
|
||||
float width = 0.0f;
|
||||
};
|
||||
std::array<MarkerColumn, 4> markerColumns{};
|
||||
/** 四列的语义下标(`markerColumns` 的索引)。 */
|
||||
std::array<MarkerColumn, 5> markerColumns{};
|
||||
/** 面板里是否存在枚举文本(为 false 时 ② 列整列缺省)。 */
|
||||
bool markerHasEnumColumn = false;
|
||||
/** 五列的语义下标(`markerColumns` 的索引)。 */
|
||||
enum MarkerColumnIndex
|
||||
{
|
||||
MARKER_COL_VALUE = 0,
|
||||
MARKER_COL_DELTA = 1,
|
||||
MARKER_COL_TIME = 2,
|
||||
MARKER_COL_DT = 3
|
||||
MARKER_COL_ENUM = 1,
|
||||
MARKER_COL_DELTA = 2,
|
||||
MARKER_COL_TIME = 3,
|
||||
MARKER_COL_DT = 4
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1776,9 +1860,11 @@ namespace jdi
|
||||
* @param [IN] color 信号色。
|
||||
* @param [IN] close ✕ 按钮矩形(宽度为 0 表示不画)。
|
||||
* @param [IN] closeHot ✕ 是否处于悬停态。
|
||||
* @param [IN] alphaScale 整体不透明度系数(1 = 不透明;固定点受「标记透明度」控制)。
|
||||
*/
|
||||
void PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label, const std::wstring& text,
|
||||
std::uint32_t color, const gd::RectF& close, bool closeHot);
|
||||
std::uint32_t color, const gd::RectF& close, bool closeHot,
|
||||
float alphaScale = 1.0f);
|
||||
|
||||
/**
|
||||
* @brief 计算某个固定点相对**同信号**前一个固定点的增量与其时间间隔。
|
||||
@@ -2576,6 +2662,17 @@ namespace jdi
|
||||
/** 正在编辑的固定点(`m_editLane` 网格内的 `freeMarkerItems` 下标)。 */
|
||||
std::size_t m_editMarkerIndex = static_cast<std::size_t>(-1);
|
||||
|
||||
/** 「标记透明度」滑块拖拽的目标网格(-1 = 未拖拽)。 */
|
||||
int m_markerOpacityDragLane = -1;
|
||||
/** 面板内容滚动条拖拽的目标网格(-1 = 未拖拽)。 */
|
||||
int m_markerScrollDragLane = -1;
|
||||
/** 按下滑块时鼠标相对滚动条滑块上沿的偏移(避免滑块跳动)。 */
|
||||
float m_markerScrollGrabOffset = 0.0f;
|
||||
|
||||
/** 「自由标记」面板:分组栏三角的悬停态(用于给出可点击提示)。 */
|
||||
int m_freeMarkerTriangleHoverLane = -1;
|
||||
SignalRef m_freeMarkerTriangleHoverSignal;
|
||||
|
||||
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
|
||||
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
|
||||
bool m_spanHover = false; /**< 鼠标是否悬停在双光标差值标签上。 */
|
||||
|
||||
@@ -1693,6 +1693,115 @@ namespace jdi
|
||||
return enabled;
|
||||
}
|
||||
|
||||
bool PlotView::SetLaneFreeMarkerOpacity(int laneIndex, float opacity)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
const float clamped = std::max(0.0f, std::min(1.0f, opacity));
|
||||
if (std::fabs(lane.freeMarkerOpacity - clamped) < 0.0005f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
lane.freeMarkerOpacity = clamped;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::SetLaneFreeMarkerScroll(int laneIndex, float scroll)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
const float clamped = std::max(0.0f, scroll);
|
||||
if (std::fabs(lane.freeMarkerScroll - clamped) < 0.5f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
lane.freeMarkerScroll = clamped;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::ToggleLaneFreeMarkerGroup(int laneIndex, const SignalRef& ref)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
return lane.ToggleFreeMarkerGroupCollapsed(ref);
|
||||
}
|
||||
|
||||
bool PlotView::SetLanePanelCollapsed(int laneIndex, EdgePanelKind kind, bool collapsed)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
LanePanelState* state = nullptr;
|
||||
switch (kind)
|
||||
{
|
||||
case EdgePanelKind::COUNT:
|
||||
state = &lane.edgePanel;
|
||||
break;
|
||||
case EdgePanelKind::CENTER:
|
||||
state = &lane.edgeCenterPanel;
|
||||
break;
|
||||
case EdgePanelKind::MEAN:
|
||||
state = &lane.meanFilterPanel;
|
||||
break;
|
||||
case EdgePanelKind::PEAK:
|
||||
state = &lane.weakPeakPanel;
|
||||
break;
|
||||
case EdgePanelKind::MARK:
|
||||
state = &lane.freeMarkerPanel;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
if (state->collapsed == collapsed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state->collapsed = collapsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::LanePanelCollapsed(int laneIndex, EdgePanelKind kind) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
switch (kind)
|
||||
{
|
||||
case EdgePanelKind::COUNT:
|
||||
return lane.edgePanel.collapsed;
|
||||
case EdgePanelKind::CENTER:
|
||||
return lane.edgeCenterPanel.collapsed;
|
||||
case EdgePanelKind::MEAN:
|
||||
return lane.meanFilterPanel.collapsed;
|
||||
case EdgePanelKind::PEAK:
|
||||
return lane.weakPeakPanel.collapsed;
|
||||
case EdgePanelKind::MARK:
|
||||
return lane.freeMarkerPanel.collapsed;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool PlotView::ToggleLanePanelCollapsed(int laneIndex, EdgePanelKind kind)
|
||||
{
|
||||
const bool collapsed = !LanePanelCollapsed(laneIndex, kind);
|
||||
SetLanePanelCollapsed(laneIndex, kind, collapsed);
|
||||
return collapsed;
|
||||
}
|
||||
|
||||
bool PlotView::MeanFilterValueAt(const TimeAxis& axis, const SignalData& signal, int points, int iterations,
|
||||
double time, double& value) const
|
||||
{
|
||||
|
||||
@@ -941,6 +941,12 @@ namespace jdi
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
// 「展开 / 折起」三角指示器:可点击控件 ⇒ 手型光标。
|
||||
if (layout.disclosure.Width > 0.0f &&
|
||||
UiPointInRect(layout.disclosure, 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);
|
||||
@@ -953,15 +959,27 @@ namespace jdi
|
||||
// 「构建为新信号」按钮:手型光标作为可点击提示。
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
// 「自由标记」面板:行尾 ✕ 与信号名右侧 ✕ 都是可点击控件(手型)。
|
||||
// 「自由标记」面板:行尾 ✕ / 分组栏 ✕ 与三角都是可点击控件(手型),
|
||||
// 滚动条与「标记透明度」滑块则是拖动类控件(上下 / 左右箭头)。
|
||||
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK)
|
||||
{
|
||||
// 底部「启用光标模式 / 关闭光标模式」切换按钮。
|
||||
// 底部「光标选取模式」切换按钮。
|
||||
if (layout.button.Width > 0.0f &&
|
||||
UiPointInRect(layout.button, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
if (layout.opacityTrack.Width > 0.0f &&
|
||||
WeakPeakSliderHit(layout.opacityTrack, m_scale, static_cast<float>(x),
|
||||
static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||
}
|
||||
if (layout.scrollTrack.Width > 0.0f &&
|
||||
UiPointInRect(layout.scrollTrack, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||
}
|
||||
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||
{
|
||||
if (row.close.Width > 0.0f &&
|
||||
@@ -970,13 +988,18 @@ namespace jdi
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowNameCloses)
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
if (rect.Width > 0.0f &&
|
||||
UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y)))
|
||||
if (group.close.Width > 0.0f &&
|
||||
UiPointInRect(group.close, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
if (UiPointInRect(group.header, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
// 分组栏本体(含三角)单击 = 展开 / 收起。
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 「弱峰调整」的逐信号滑块:轨道 → 左右箭头;迭代次数右侧数值 → 手型(可点击改上限)。
|
||||
@@ -1439,6 +1462,57 @@ namespace jdi
|
||||
m_dragMoved = true;
|
||||
return true;
|
||||
}
|
||||
case PlotDragMode::MARKER_OPACITY:
|
||||
{
|
||||
// 「标记透明度」滑块:拖动时把滑块位置换算成 0–1 的不透明度。
|
||||
if (m_tab == nullptr || m_markerOpacityDragLane < 0 ||
|
||||
m_markerOpacityDragLane >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
break;
|
||||
}
|
||||
const EdgePanelLayout layout =
|
||||
EdgePanelLayoutFor(m_markerOpacityDragLane, EdgePanelKind::MARK);
|
||||
if (layout.opacityTrack.Width <= 0.0f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const double ratio = UiSliderRatioFromX(layout.opacityTrack, static_cast<float>(x));
|
||||
if (SetLaneFreeMarkerOpacity(m_markerOpacityDragLane, static_cast<float>(ratio)))
|
||||
{
|
||||
m_dragMoved = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case PlotDragMode::MARKER_SCROLL:
|
||||
{
|
||||
// 面板内容滚动条:按滑块在轨道里的位置换算滚动偏移。
|
||||
if (m_tab == nullptr || m_markerScrollDragLane < 0 ||
|
||||
m_markerScrollDragLane >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
break;
|
||||
}
|
||||
const EdgePanelLayout layout =
|
||||
EdgePanelLayoutFor(m_markerScrollDragLane, EdgePanelKind::MARK);
|
||||
const float travel = layout.scrollTrack.Height - layout.scrollThumb.Height;
|
||||
const float maxScroll =
|
||||
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
||||
if (layout.scrollTrack.Width <= 0.0f || travel <= 0.0f || maxScroll <= 0.0f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const float thumbTop =
|
||||
std::max(layout.scrollTrack.Y,
|
||||
std::min(static_cast<float>(y) - m_markerScrollGrabOffset,
|
||||
layout.scrollTrack.Y + travel));
|
||||
const float ratio = (thumbTop - layout.scrollTrack.Y) / travel;
|
||||
if (SetLaneFreeMarkerScroll(m_markerScrollDragLane, ratio * maxScroll))
|
||||
{
|
||||
m_dragMoved = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case PlotDragMode::NONE:
|
||||
default:
|
||||
break;
|
||||
@@ -1500,11 +1574,13 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 面板内 ✕ 的悬停(信号名 ✕ 与固定点行 ✕)。
|
||||
// 面板内 ✕ 的悬停(分组栏 ✕ 与固定点行 ✕)与分组栏三角的悬停。
|
||||
{
|
||||
int hoverLane = -1;
|
||||
std::size_t hoverItem = static_cast<std::size_t>(-1);
|
||||
SignalRef hoverSignal;
|
||||
int triLane = -1;
|
||||
SignalRef triSignal;
|
||||
int markLane = -1;
|
||||
int markKind = 0;
|
||||
if (m_tab != nullptr &&
|
||||
@@ -1512,10 +1588,14 @@ namespace jdi
|
||||
static_cast<EdgePanelKind>(markKind) == EdgePanelKind::MARK)
|
||||
{
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(markLane, EdgePanelKind::MARK);
|
||||
// 只认**内容区**内的命中:被滚动到内容区之外的条目(它们的矩形可能落在
|
||||
// 底部按钮 / 滑块区域)不得抢走那些控件的点击。
|
||||
const bool insideContent =
|
||||
UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y));
|
||||
// 固定点行 ✕ 优先(更具体)。
|
||||
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||
{
|
||||
if (row.close.Width > 0.0f &&
|
||||
if (insideContent && row.close.Width > 0.0f &&
|
||||
UiPointInRect(row.close, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
hoverLane = markLane;
|
||||
@@ -1525,18 +1605,22 @@ namespace jdi
|
||||
}
|
||||
if (hoverLane < 0)
|
||||
{
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowNameCloses.size(); ++rowIndex)
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
const gd::RectF& rect = layout.rowNameCloses[rowIndex];
|
||||
if (rect.Width > 0.0f &&
|
||||
UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y)) &&
|
||||
rowIndex < layout.rowRefs.size())
|
||||
if (insideContent && group.close.Width > 0.0f &&
|
||||
UiPointInRect(group.close, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
hoverLane = markLane;
|
||||
hoverItem = static_cast<std::size_t>(-1);
|
||||
hoverSignal = layout.rowRefs[rowIndex];
|
||||
hoverSignal = group.ref;
|
||||
break;
|
||||
}
|
||||
if (insideContent &&
|
||||
UiPointInRect(group.triangle, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
triLane = markLane;
|
||||
triSignal = group.ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1548,6 +1632,13 @@ namespace jdi
|
||||
m_freeMarkerPanelHoverSignal = hoverSignal;
|
||||
changed = true;
|
||||
}
|
||||
if (triLane != m_freeMarkerTriangleHoverLane ||
|
||||
!(triSignal == m_freeMarkerTriangleHoverSignal))
|
||||
{
|
||||
m_freeMarkerTriangleHoverLane = triLane;
|
||||
m_freeMarkerTriangleHoverSignal = triSignal;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标压在实验性功能面板上时不再触发信号悬停提示(面板本来就盖住波形),
|
||||
@@ -1911,6 +2002,15 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
// 标题栏左侧的「展开 / 折起」三角指示器:单击切换面板的折起状态。
|
||||
// 折起后面板只剩标题栏(再次点击三角、或双击标题栏可展开)。
|
||||
if (layout.disclosure.Width > 0.0f && UiPointInRect(layout.disclosure, fx, fy))
|
||||
{
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
ToggleLanePanelCollapsed(index, kind);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板:逐信号条目
|
||||
// (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰调整是「名称 + 复选框 + 两行滑块」)。
|
||||
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
|
||||
@@ -2154,37 +2254,80 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 「自由标记」面板:行尾 ✕ 移除该固定点;信号名右侧 ✕ 清除该信号的全部固定点。
|
||||
// 「自由标记」面板:底部的切换按钮 / 「标记透明度」滑块 / 内容滚动条,
|
||||
// 随后是分组栏(单击展开收起、右侧 ✕ 清空该信号)与固定点行 ✕。
|
||||
// 与其它逐行 ✕ / 复位控件同一语义:**按下即生效**(不进拖拽、不穿透)。
|
||||
if (kind == EdgePanelKind::MARK && m_tab != nullptr)
|
||||
{
|
||||
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
|
||||
// 底部切换按钮「启用光标模式 / 关闭光标模式」(默认关闭)。
|
||||
// 底部切换按钮「光标选取模式」(默认关闭)。
|
||||
if (layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
|
||||
{
|
||||
ToggleLaneFreeMarkerCursor(index);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||
// 「标记透明度」滑块:按下即写入该处数值并进入拖动。
|
||||
if (layout.opacityTrack.Width > 0.0f &&
|
||||
WeakPeakSliderHit(layout.opacityTrack, m_scale, fx, fy))
|
||||
{
|
||||
if (row.close.Width > 0.0f && UiPointInRect(row.close, fx, fy))
|
||||
SetLaneFreeMarkerOpacity(index,
|
||||
static_cast<float>(UiSliderRatioFromX(
|
||||
layout.opacityTrack, fx)));
|
||||
m_dragMode = PlotDragMode::MARKER_OPACITY;
|
||||
m_markerOpacityDragLane = index;
|
||||
return true;
|
||||
}
|
||||
// 内容滚动条:按在滑块上就带上抓取偏移,按在轨道上则直接跳转。
|
||||
if (layout.scrollTrack.Width > 0.0f &&
|
||||
UiPointInRect(layout.scrollTrack, fx, fy))
|
||||
{
|
||||
const float travel = layout.scrollTrack.Height - layout.scrollThumb.Height;
|
||||
const float maxScroll =
|
||||
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
||||
if (travel > 0.0f && maxScroll > 0.0f)
|
||||
{
|
||||
tabLane.RemoveFreeMarkerAt(row.itemIndex);
|
||||
m_dragMode = PlotDragMode::MARKER_SCROLL;
|
||||
m_markerScrollDragLane = index;
|
||||
const bool onThumb = UiPointInRect(layout.scrollThumb, fx, fy);
|
||||
m_markerScrollGrabOffset =
|
||||
onThumb ? (fy - layout.scrollThumb.Y)
|
||||
: (layout.scrollThumb.Height * 0.5f);
|
||||
const float thumbTop =
|
||||
std::max(layout.scrollTrack.Y,
|
||||
std::min(fy - m_markerScrollGrabOffset,
|
||||
layout.scrollTrack.Y + travel));
|
||||
SetLaneFreeMarkerScroll(
|
||||
index, ((thumbTop - layout.scrollTrack.Y) / travel) * maxScroll);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 分组栏:✕ → 清空该信号全部固定点;其它位置(含三角)→ 展开 / 收起。
|
||||
// ⚠️ 只认内容区内的命中:滚动出去的条目不得抢到底部按钮 / 滑块的点击。
|
||||
const bool insideContent = UiPointInRect(layout.content, fx, fy);
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
if (insideContent && group.close.Width > 0.0f && UiPointInRect(group.close, fx, fy))
|
||||
{
|
||||
tabLane.RemoveFreeMarkersOf(group.ref);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
m_freeMarkerPanelHoverLane = -1;
|
||||
m_freeMarkerPanelHoverItem = static_cast<std::size_t>(-1);
|
||||
m_freeMarkerPanelHoverSignal = SignalRef();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowNameCloses.size(); ++rowIndex)
|
||||
{
|
||||
const gd::RectF& rect = layout.rowNameCloses[rowIndex];
|
||||
if (rect.Width > 0.0f && UiPointInRect(rect, fx, fy) &&
|
||||
rowIndex < layout.rowRefs.size())
|
||||
if (insideContent && UiPointInRect(group.header, fx, fy))
|
||||
{
|
||||
tabLane.RemoveFreeMarkersOf(layout.rowRefs[rowIndex]);
|
||||
ToggleLaneFreeMarkerGroup(index, group.ref);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||
{
|
||||
if (insideContent && row.close.Width > 0.0f && UiPointInRect(row.close, fx, fy))
|
||||
{
|
||||
tabLane.RemoveFreeMarkerAt(row.itemIndex);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
m_freeMarkerPanelHoverLane = -1;
|
||||
m_freeMarkerPanelHoverItem = static_cast<std::size_t>(-1);
|
||||
@@ -2490,6 +2633,10 @@ namespace jdi
|
||||
// 弱峰滑块拖拽结束:清掉目标(避免留下上一轮的信号引用)。
|
||||
m_peakDragLane = -1;
|
||||
m_peakDragRef = SignalRef();
|
||||
// 「标记透明度」滑块 / 面板内容滚动条拖拽结束。
|
||||
m_markerOpacityDragLane = -1;
|
||||
m_markerScrollDragLane = -1;
|
||||
m_markerScrollGrabOffset = 0.0f;
|
||||
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。
|
||||
ReleaseHint();
|
||||
return changed;
|
||||
@@ -2504,6 +2651,37 @@ namespace jdi
|
||||
|
||||
const double notches = static_cast<double>(delta) / 120.0;
|
||||
|
||||
// 鼠标位于「自由标记」面板的可滚动内容区上:滚轮翻页该面板的内容(不滚动网格)。
|
||||
// ⚠️ 只认内容区:底部的切换按钮与「标记透明度」滑块不参与滚动,
|
||||
// 因此鼠标压在它们上面时仍然滚动网格(与“它们不属于内容”的语义一致)。
|
||||
if (m_tab != nullptr)
|
||||
{
|
||||
int panelLane = -1;
|
||||
int panelKind = 0;
|
||||
if (TopPanelAtPoint(static_cast<float>(m_mouseX), static_cast<float>(m_mouseY),
|
||||
panelLane, panelKind) &&
|
||||
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK)
|
||||
{
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, EdgePanelKind::MARK);
|
||||
if (layout.content.Width > 0.0f &&
|
||||
UiPointInRect(layout.content, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY)))
|
||||
{
|
||||
const float maxScroll =
|
||||
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
||||
if (maxScroll > 0.0f)
|
||||
{
|
||||
const float next = static_cast<float>(Tab().Lanes()[static_cast<std::size_t>(
|
||||
panelLane)]
|
||||
.freeMarkerScroll -
|
||||
notches * 40.0 * m_scale);
|
||||
SetLaneFreeMarkerScroll(panelLane, std::max(0.0f, std::min(next, maxScroll)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 滚轮不再承担缩放:默认纵向滚动网格,按住 Shift 时水平平移时间轴。
|
||||
if (shift)
|
||||
{
|
||||
@@ -2542,6 +2720,27 @@ namespace jdi
|
||||
int panelKind = 0;
|
||||
if (TopPanelAtPoint(static_cast<float>(x), static_cast<float>(y), panelLane, panelKind))
|
||||
{
|
||||
// 标题栏双击 = 展开 / 折起该面板(折起的面板只显示标题栏)。
|
||||
// ⚠️ 折起状态下标题栏是唯一可见区域,因此这里也是“双击重新展开”的入口。
|
||||
{
|
||||
const EdgePanelLayout titleLayout =
|
||||
EdgePanelLayoutFor(panelLane, static_cast<EdgePanelKind>(panelKind));
|
||||
if (titleLayout.title.Width > 0.0f &&
|
||||
UiPointInRect(titleLayout.title, static_cast<float>(x), static_cast<float>(y)) &&
|
||||
!(titleLayout.badge.Width > 0.0f &&
|
||||
UiPointInRect(titleLayout.badge, static_cast<float>(x),
|
||||
static_cast<float>(y))) &&
|
||||
!(titleLayout.close.Width > 0.0f &&
|
||||
UiPointInRect(titleLayout.close, static_cast<float>(x),
|
||||
static_cast<float>(y))) &&
|
||||
!(titleLayout.disclosure.Width > 0.0f &&
|
||||
UiPointInRect(titleLayout.disclosure, static_cast<float>(x),
|
||||
static_cast<float>(y))))
|
||||
{
|
||||
ToggleLanePanelCollapsed(panelLane, static_cast<EdgePanelKind>(panelKind));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 「自由标记」面板:双击某一固定点行 → 编辑它的时间(左侧是自身时刻、
|
||||
// 右侧是相对前一固定点的间隔,对应行内两列的位置)。
|
||||
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK && m_tab != nullptr &&
|
||||
@@ -2553,6 +2752,7 @@ namespace jdi
|
||||
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||
{
|
||||
if (row.rect.Width <= 0.0f ||
|
||||
!UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y)) ||
|
||||
!UiPointInRect(row.rect, static_cast<float>(x), static_cast<float>(y)) ||
|
||||
row.itemIndex >= tabLane.freeMarkerItems.size())
|
||||
{
|
||||
|
||||
@@ -331,13 +331,13 @@ namespace jdi
|
||||
|
||||
// ---------------------------------------------------------------- 自由标记
|
||||
|
||||
/** 「自由标记」面板里信号名行的高度(比普通条目略高:右侧多一个 ✕)。 */
|
||||
/** 「自由标记」面板里信号**分组栏**的高度(左侧三角 + 信号名,右侧标记点数 + ✕)。 */
|
||||
inline const float kFreeMarkerNameRowHeight = 22.0f;
|
||||
|
||||
/** 「自由标记」面板里每个固定点行的高度。 */
|
||||
inline const float kFreeMarkerItemRowHeight = 18.0f;
|
||||
/** 「自由标记」面板里每个固定点行的高度(五列 + 行尾 ✕)。 */
|
||||
inline const float kFreeMarkerItemRowHeight = 20.0f;
|
||||
|
||||
/** 「自由标记」面板与网格内固定点 ✕ 按钮的边长。 */
|
||||
/** 「自由标记」面板与行尾 ✕ 按钮的边长(网格内取值胶囊后的 ✕ 另按胶囊高度取正方形)。 */
|
||||
inline const float kFreeMarkerCloseSize = 13.0f;
|
||||
|
||||
/**
|
||||
@@ -347,21 +347,45 @@ namespace jdi
|
||||
* 而后者在 `TopPanelAtPoint` / `SyncPanelOrder` 里被高频调用(每次鼠标移动数次);
|
||||
* 逐行取采样值算宽度会变成"鼠标一动就做几十次采样查询"。
|
||||
*
|
||||
* 宽度 = 左右内边距 + 四列(取值/增量/时刻/间隔)+ 行 ✕ ⇒ 保证四列在纵向上对齐。
|
||||
* 宽度 = 左右内边距 + 五列(取值/枚举/增量/时刻/间隔)+ 行 ✕ ⇒ 保证五列在纵向上对齐。
|
||||
*/
|
||||
inline const float kFreeMarkerPanelWidth = 415.0f;
|
||||
inline const float kFreeMarkerPanelWidth = 424.0f;
|
||||
|
||||
/**
|
||||
* @brief 「自由标记」面板四列的逻辑宽度(**按此顺序**:取值 / 增量 / 时刻 / 间隔)。
|
||||
* @brief 「自由标记」面板五列的逻辑宽度
|
||||
* (**按此顺序**:取值 / 枚举文本 / 增量 / 时刻 / 间隔)。
|
||||
*
|
||||
* 固定列宽(不按文本测量,见上),绘制时用 `UiEllipsize` 截断超长文本。
|
||||
* 四列左对齐且右边界由列宽推得 ⇒ 同一信息在所有条目上纵向上严格对齐。
|
||||
* 当整个面板里没有任何枚举文本时,枚举这一列会被**整列缺省**(列宽记 0,
|
||||
* 后面的列左移),其它列的宽度不变。
|
||||
*/
|
||||
inline const float kFreeMarkerColumnWidths[4] = {124.0f, 86.0f, 78.0f, 90.0f};
|
||||
inline const float kFreeMarkerColumnWidths[5] = {74.0f, 80.0f, 60.0f, 72.0f, 78.0f};
|
||||
|
||||
/** 「自由标记」面板行内相邻两列之间的间距。 */
|
||||
inline const float kFreeMarkerColumnGap = 2.0f;
|
||||
|
||||
/** 「自由标记」面板内容滚动条的宽度(逻辑像素)。 */
|
||||
inline const float kFreeMarkerScrollWidth = 7.0f;
|
||||
|
||||
/**
|
||||
* @brief 「自由标记」面板**内容区**的最大高度(逻辑像素)。
|
||||
*
|
||||
* 超过它就出现上下滚动条(分组栏与固定点条目一起滚动);面板底部的按钮与
|
||||
* 「标记透明度」滑动条不参与滚动。
|
||||
*/
|
||||
inline const float kFreeMarkerContentMaxHeight = 260.0f;
|
||||
|
||||
/** 「自由标记」面板底部「标记透明度」滑动条行的高度(与信号样式区的行高同量级)。 */
|
||||
inline const float kFreeMarkerSliderRowHeight = 24.0f;
|
||||
|
||||
/**
|
||||
* @brief 所有实验性功能浮动面板标题栏左侧「展开/折起」指示器的宽度。
|
||||
*
|
||||
* 指示器 = 三角(朝右 = 已折起、朝下 = 已展开)+ 三角后方的一条横线;
|
||||
* 点击它切换面板的折起状态。`GX` 编号徽标向右让出这块宽度。
|
||||
*/
|
||||
inline const float kEdgePanelDisclosureWidth = 20.0f;
|
||||
|
||||
/**
|
||||
* @brief 非光标模式下判定"鼠标在信号曲线上"的纵向容差(逻辑像素)。
|
||||
*
|
||||
@@ -416,28 +440,29 @@ namespace jdi
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「自由标记」的时间列文本:`t:<时刻>`。
|
||||
* @brief 「自由标记」的时间列文本:`<时刻>s`。
|
||||
*
|
||||
* 数值格式与光标时间读数**完全相同**(`FormatValue` + `s` 后缀,
|
||||
* 即 `FormatCursorTime` 的口径);这里不调用 `FormatCursorTime` 是因为它在本头里
|
||||
* 定义在本段**之后**,直接展开可以避免前向声明。
|
||||
* 数值格式与光标时间读数**完全相同**(`FormatValue` + `s` 后缀)。
|
||||
*/
|
||||
inline std::wstring FreeMarkerTimeText(double time)
|
||||
{
|
||||
return L"t:" + FormatValue(time) + L"s";
|
||||
return FormatValue(time) + L"s";
|
||||
}
|
||||
|
||||
/** 「自由标记」面板里缺省(不适用)的单元格文本。 */
|
||||
inline const wchar_t* const kFreeMarkerNotAvailable = L"N/A";
|
||||
|
||||
/**
|
||||
* @brief 「自由标记」的时间间隔列文本:`Δt:<间隔>`。
|
||||
* @brief 「自由标记」的时间间隔列文本:`Δt:+<间隔>`。
|
||||
*
|
||||
* 平时用复合单位(自动匹配 ms / s / min,与双光标差值读数一致),
|
||||
* 双击后按**秒**编辑。首个固定点没有前一项 ⇒ 返回空串(该列留白)。
|
||||
* 双击后按**秒**编辑。首个固定点没有前一项 ⇒ 显示 `N/A`(该列不省略,保持列对齐)。
|
||||
*/
|
||||
inline std::wstring FreeMarkerDeltaText(bool hasPrev, double timeDelta)
|
||||
{
|
||||
if (!hasPrev)
|
||||
{
|
||||
return std::wstring();
|
||||
return std::wstring(kFreeMarkerNotAvailable);
|
||||
}
|
||||
// 时间间隔恒为正(按时间排序后当前点总在后方);显示时显式带 `+`。
|
||||
return L"Δt:+" + FormatDurationCompound(std::fabs(timeDelta));
|
||||
@@ -525,10 +550,10 @@ namespace jdi
|
||||
inline const wchar_t* const kWeakPeakIterationsSliderLabel = L"迭代次数";
|
||||
inline const wchar_t* const kWeakPeakTitle = L"弱峰调整"; /**< 弱峰调整面板标题。 */
|
||||
inline const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
|
||||
/** 「自由标记」面板底部切换按钮:光标模式**未启用**时的文案(点击后启用)。 */
|
||||
inline const wchar_t* const kFreeMarkerCursorOnLabel = L"启用光标模式";
|
||||
/** 「自由标记」面板底部切换按钮:光标模式**已启用**时的文案(点击后关闭)。 */
|
||||
inline const wchar_t* const kFreeMarkerCursorOffLabel = L"关闭光标模式";
|
||||
/** 「自由标记」面板底部切换按钮文案(切换按钮的激活态 = 光标模式开启)。 */
|
||||
inline const wchar_t* const kFreeMarkerCursorModeLabel = L"光标选取模式";
|
||||
/** 「自由标记」面板底部切换按钮激活时出现在其上方的滑动条标签。 */
|
||||
inline const wchar_t* const kFreeMarkerOpacityLabel = L"标记透明度";
|
||||
/**
|
||||
* @brief 边沿统计累积状态上限。
|
||||
*
|
||||
|
||||
@@ -5342,6 +5342,14 @@ namespace jdi
|
||||
{
|
||||
return layout;
|
||||
}
|
||||
// 面板的**折起**状态(只显示标题栏)。折起时仍然算宽度与位置,但内容区全部省略。
|
||||
const bool collapsed = meanFilter ? lane.meanFilterPanel.collapsed
|
||||
: (weakPeak ? lane.weakPeakPanel.collapsed
|
||||
: (freeMarker ? lane.freeMarkerPanel.collapsed
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.collapsed
|
||||
: lane.edgePanel.collapsed)));
|
||||
layout.collapsed = collapsed;
|
||||
|
||||
int rowCount = 0;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
@@ -5415,8 +5423,10 @@ namespace jdi
|
||||
height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * rowsPerSignal + pad;
|
||||
}
|
||||
|
||||
// 「自由标记」面板:逐信号一行名称(**只列出确实有固定点的信号**),下面跟着它的全部固定点;
|
||||
// 没有任何固定点时给一行灰色提示,最后始终带一行底部切换按钮。
|
||||
// 「自由标记」面板:按信号分组(**分组栏** + 下属固定点条目),
|
||||
// 只列出确实有固定点的信号;没有任何固定点时给一行灰色提示。
|
||||
// 内容区超高时出现滚动条;面板底部固定「标记透明度」滑块(仅光标模式激活时)
|
||||
// 与「光标选取模式」切换按钮(二者**不参与**滚动)。
|
||||
bool markerAny = false;
|
||||
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
|
||||
{
|
||||
@@ -5428,9 +5438,11 @@ namespace jdi
|
||||
}
|
||||
if (freeMarker)
|
||||
{
|
||||
const float nameRow = kFreeMarkerNameRowHeight * m_scale;
|
||||
const float groupRow = kFreeMarkerNameRowHeight * m_scale;
|
||||
const float itemRow = kFreeMarkerItemRowHeight * m_scale;
|
||||
const float markerButtonHeight = kEdgePanelButtonHeight * m_scale;
|
||||
const float sliderRow =
|
||||
lane.freeMarkerCursor ? (kFreeMarkerSliderRowHeight * m_scale) : 0.0f;
|
||||
float content = 0.0f;
|
||||
std::vector<std::size_t> order;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
@@ -5440,22 +5452,37 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// ⚠️ 用户明确要求:某个信号没有任何固定点时,它的名称条目**不出现**在面板上。
|
||||
// ⚠️ 用户明确要求:某个信号没有任何固定点时,它的分组栏**不出现**在面板上。
|
||||
SortedFreeMarkerIndices(lane, ref, order);
|
||||
if (order.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
content += nameRow;
|
||||
content += static_cast<float>(order.size()) * itemRow;
|
||||
content += groupRow;
|
||||
if (!lane.FreeMarkerGroupCollapsed(ref))
|
||||
{
|
||||
content += static_cast<float>(order.size()) * itemRow;
|
||||
}
|
||||
}
|
||||
if (!markerAny)
|
||||
{
|
||||
// 还没有任何固定点:第一行留作提示文案(绘制时画「在网格内单击以固定数据点」)。
|
||||
content += nameRow;
|
||||
// 还没有任何固定点:留一行提示文案(绘制时画「在网格内单击以固定数据点」)。
|
||||
content += groupRow;
|
||||
}
|
||||
// 底部「启用光标模式 / 关闭光标模式」按钮:上下各留一个 pad(与左右内边距一致)。
|
||||
height = titleHeight + pad + content + pad + markerButtonHeight + pad;
|
||||
// 内容区高度上限:太长的列表不能把面板顶出绘图区(超出部分靠滚动条翻页)。
|
||||
const float budget =
|
||||
m_plotRect.Height - (titleHeight + pad * 3.0f + markerButtonHeight + sliderRow);
|
||||
const float maxContent =
|
||||
std::max(2.0f * itemRow,
|
||||
std::min(kFreeMarkerContentMaxHeight * m_scale, std::max(2.0f * itemRow, budget)));
|
||||
const float view = std::min(content, maxContent);
|
||||
height = titleHeight + pad + view + pad + sliderRow + markerButtonHeight + pad;
|
||||
}
|
||||
|
||||
// 折起的面板:只剩标题栏(内容区完全不占高度)。
|
||||
if (collapsed)
|
||||
{
|
||||
height = titleHeight;
|
||||
}
|
||||
|
||||
// 首次使用(位置为负)时自动放到绘图区**右上角**,并按网格在**当前可见网格中的序数**
|
||||
@@ -5546,10 +5573,22 @@ 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.disclosure = gd::RectF(left + pad, top + (titleHeight - kEdgePanelDisclosureWidth * m_scale) * 0.5f,
|
||||
kEdgePanelDisclosureWidth * m_scale,
|
||||
kEdgePanelDisclosureWidth * m_scale);
|
||||
// 标题栏左侧的「GX」编号徽标:绘制、悬停提示、手型光标与“点击滚动到该网格”
|
||||
// 全部以这个矩形为准(唯一几何来源,避免“画的”和“点的”漂移)。
|
||||
layout.badge = gd::RectF(left + pad, top + (titleHeight - kLaneBadgeHeight * m_scale) * 0.5f,
|
||||
kLaneBadgeWidth * m_scale, kLaneBadgeHeight * m_scale);
|
||||
layout.badge =
|
||||
gd::RectF(left + pad + kEdgePanelDisclosureWidth * m_scale,
|
||||
top + (titleHeight - kLaneBadgeHeight * m_scale) * 0.5f, kLaneBadgeWidth * m_scale,
|
||||
kLaneBadgeHeight * m_scale);
|
||||
// 折起状态:到此为止(内容区、按钮、滚动条全部为空)。
|
||||
if (collapsed)
|
||||
{
|
||||
return layout;
|
||||
}
|
||||
if (itemized)
|
||||
{
|
||||
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
|
||||
@@ -5688,26 +5727,44 @@ namespace jdi
|
||||
|
||||
if (freeMarker)
|
||||
{
|
||||
// 「自由标记」面板:**只列出确实有固定点的信号**,每个信号一行名称
|
||||
//(右侧带「清除本信号全部固定点」✕),下面逐行列出它的固定点
|
||||
//(组内按时间升序 == 从左到右),行尾再带一个 ✕。
|
||||
const float nameRow = kFreeMarkerNameRowHeight * m_scale;
|
||||
// 「自由标记」面板:**只列出确实有固定点的信号**,每个信号一个**分组栏**
|
||||
//(信号色托底 + 左侧展开/收起三角 + 信号名 + 右侧「N 标记点」与 ✕),
|
||||
// 下面逐行列出它的固定点(组内按时间升序 == 从上到下),行尾再带一个 ✕。
|
||||
// 内容区超高时右侧出现滚动条;底部按钮与「标记透明度」滑块**固定**不滚动。
|
||||
const float groupRow = kFreeMarkerNameRowHeight * m_scale;
|
||||
const float itemRow = kFreeMarkerItemRowHeight * m_scale;
|
||||
const float closeSize = kFreeMarkerCloseSize * m_scale;
|
||||
const float gap = 4.0f * m_scale;
|
||||
const float markerButtonHeight = kEdgePanelButtonHeight * m_scale;
|
||||
const float sliderRow = lane.freeMarkerCursor ? (kFreeMarkerSliderRowHeight * m_scale) : 0.0f;
|
||||
const float scrollW = kFreeMarkerScrollWidth * m_scale;
|
||||
|
||||
// 四列(① 取值 ② 增量 ③ 时刻 ④ 间隔)从左往右**固定列宽**排布:
|
||||
// 每一行的同类信息都从同一横坐标开始 ⇒ 纵向上严格对齐(用户明确要求)。
|
||||
float columnX = left + pad;
|
||||
for (int column = 0; column < 4; ++column)
|
||||
// 底部固定区:切换按钮(整行宽度、与面板下边界留一个 pad)。
|
||||
layout.button = gd::RectF(left + pad, top + height - pad - markerButtonHeight,
|
||||
std::max(0.0f, width - pad * 2.0f), markerButtonHeight);
|
||||
if (sliderRow > 0.0f)
|
||||
{
|
||||
layout.markerColumns[column].left = columnX;
|
||||
layout.markerColumns[column].width = kFreeMarkerColumnWidths[column] * m_scale;
|
||||
columnX += layout.markerColumns[column].width + kFreeMarkerColumnGap * m_scale;
|
||||
// 「标记透明度」行:与信号样式区的「透明度」行同一套排版
|
||||
//(左侧标签 + 中间轨道 + 右侧百分比),只是位置固定在按钮上方。
|
||||
const float rowTop = layout.button.Y - sliderRow;
|
||||
const float valueWidth =
|
||||
std::max(36.0f * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, L"100%") + 6.0f * m_scale);
|
||||
const float labelWidth =
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kFreeMarkerOpacityLabel) + 6.0f * m_scale;
|
||||
const float trackLeft = left + pad + labelWidth;
|
||||
const float trackRight = left + width - pad - valueWidth - 6.0f * m_scale;
|
||||
const float trackHeight = kWeakPeakTrackHeight * m_scale;
|
||||
layout.opacityTrack =
|
||||
gd::RectF(trackLeft, rowTop + (sliderRow - trackHeight) * 0.5f,
|
||||
std::max(20.0f * m_scale, trackRight - trackLeft), trackHeight);
|
||||
layout.opacityValue =
|
||||
gd::RectF(trackRight + 6.0f * m_scale, rowTop, valueWidth, sliderRow);
|
||||
}
|
||||
|
||||
float rowY = top + titleHeight + pad;
|
||||
// 内容区高度(面板高度算式里的 `view` 部分)与"内容总高"决定是否需要滚动条。
|
||||
const float viewHeight = std::max(
|
||||
0.0f, height - titleHeight - pad * 3.0f - markerButtonHeight - sliderRow);
|
||||
float totalContent = 0.0f;
|
||||
std::vector<std::size_t> order;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
@@ -5716,35 +5773,124 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 该信号没有任何固定点 ⇒ 名称条目与数据行都**不出现**(用户明确要求)。
|
||||
SortedFreeMarkerIndices(lane, ref, order);
|
||||
if (order.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
layout.rowRefs.push_back(ref);
|
||||
layout.rowNameRects.push_back(gd::RectF(left + pad, rowY,
|
||||
std::max(0.0f, width - pad * 2.0f - closeSize - gap),
|
||||
nameRow));
|
||||
layout.rowNameCloses.push_back(
|
||||
gd::RectF(left + width - pad - closeSize, rowY + (nameRow - closeSize) * 0.5f,
|
||||
closeSize, closeSize));
|
||||
rowY += nameRow;
|
||||
totalContent += groupRow;
|
||||
if (!lane.FreeMarkerGroupCollapsed(ref))
|
||||
{
|
||||
totalContent += static_cast<float>(order.size()) * itemRow;
|
||||
}
|
||||
}
|
||||
if (!markerAny)
|
||||
{
|
||||
totalContent += groupRow;
|
||||
}
|
||||
const bool scrollable = totalContent > viewHeight + 0.5f;
|
||||
const float maxScroll = std::max(0.0f, totalContent - viewHeight);
|
||||
const float scroll = std::max(0.0f, std::min(lane.freeMarkerScroll, maxScroll));
|
||||
const float innerWidth =
|
||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||
layout.content =
|
||||
gd::RectF(left + pad, top + titleHeight + pad, innerWidth, viewHeight);
|
||||
layout.markerContentHeight = totalContent;
|
||||
if (scrollable)
|
||||
{
|
||||
layout.scrollTrack =
|
||||
gd::RectF(left + pad + innerWidth + 4.0f * m_scale, layout.content.Y, scrollW,
|
||||
viewHeight);
|
||||
const float thumbHeight =
|
||||
std::max(18.0f * m_scale, viewHeight * (viewHeight / totalContent));
|
||||
const float thumbTop =
|
||||
layout.content.Y +
|
||||
(viewHeight - thumbHeight) * ((maxScroll > 0.0f) ? (scroll / maxScroll) : 0.0f);
|
||||
layout.scrollThumb = gd::RectF(layout.scrollTrack.X, thumbTop, scrollW, thumbHeight);
|
||||
}
|
||||
|
||||
// 五列(① 取值 ② 枚举 ③ 增量 ④ 时刻 ⑤ 间隔)从左往右**固定列宽**排布:
|
||||
// 每一行的同类信息都从同一横坐标开始 ⇒ 纵向上严格对齐(用户明确要求)。
|
||||
// ⚠️ 整个面板里没有任何枚举文本时,② 枚举列**整列缺省**(列宽记 0,后面的列左移)。
|
||||
// 这里只查"该信号是否带枚举表"(O(1)),**不**逐条采样(布局函数被高频调用)。
|
||||
bool hasEnum = false;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data != nullptr && resolved.visible && !resolved.data->enumText.Empty() &&
|
||||
lane.HasFreeMarkers(ref))
|
||||
{
|
||||
hasEnum = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
layout.markerHasEnumColumn = hasEnum;
|
||||
float columnX = left + pad;
|
||||
for (int column = 0; column < 5; ++column)
|
||||
{
|
||||
const bool omitted = (column == EdgePanelLayout::MARKER_COL_ENUM) && !hasEnum;
|
||||
const float columnWidth =
|
||||
omitted ? 0.0f : (kFreeMarkerColumnWidths[column] * m_scale);
|
||||
layout.markerColumns[column].left = columnX;
|
||||
layout.markerColumns[column].width = columnWidth;
|
||||
columnX += columnWidth + (omitted ? 0.0f : kFreeMarkerColumnGap * m_scale);
|
||||
}
|
||||
|
||||
const float contentBottom = layout.content.Y + layout.content.Height;
|
||||
float rowY = layout.content.Y - scroll;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 该信号没有任何固定点 ⇒ 分组栏与数据行都**不出现**(用户明确要求)。
|
||||
SortedFreeMarkerIndices(lane, ref, order);
|
||||
if (order.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
EdgePanelLayout::MarkerGroup group;
|
||||
group.ref = ref;
|
||||
group.collapsed = lane.FreeMarkerGroupCollapsed(ref);
|
||||
group.header = gd::RectF(layout.content.X, rowY, innerWidth, groupRow);
|
||||
group.triangle = gd::RectF(layout.content.X + 3.0f * m_scale,
|
||||
rowY + (groupRow - 12.0f * m_scale) * 0.5f,
|
||||
12.0f * m_scale, 12.0f * m_scale);
|
||||
group.close = gd::RectF(layout.content.X + innerWidth - closeSize - 2.0f * m_scale,
|
||||
rowY + (groupRow - closeSize) * 0.5f, closeSize, closeSize);
|
||||
if (group.header.Y + group.header.Height > layout.content.Y &&
|
||||
group.header.Y < contentBottom)
|
||||
{
|
||||
// 只登记"在可视内容区里"的分组栏与条目(滚出视界的条目不得抢走
|
||||
// 底部按钮 / 滑块的点击)。
|
||||
layout.markerGroups.push_back(group);
|
||||
layout.rowRefs.push_back(ref);
|
||||
layout.rowNameRects.push_back(group.header);
|
||||
layout.rowNameCloses.push_back(group.close);
|
||||
}
|
||||
rowY += groupRow;
|
||||
if (group.collapsed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const std::size_t itemIndex : order)
|
||||
{
|
||||
EdgePanelLayout::MarkerRow markerRow;
|
||||
markerRow.itemIndex = itemIndex;
|
||||
markerRow.rect = gd::RectF(left + pad, rowY, width - pad * 2.0f, itemRow);
|
||||
markerRow.close = gd::RectF(left + width - pad - closeSize,
|
||||
rowY + (itemRow - closeSize) * 0.5f, closeSize, closeSize);
|
||||
layout.markerRows.push_back(markerRow);
|
||||
markerRow.rect = gd::RectF(layout.content.X, rowY, innerWidth, itemRow);
|
||||
markerRow.close = gd::RectF(layout.content.X + innerWidth - closeSize,
|
||||
rowY + (itemRow - closeSize) * 0.5f, closeSize,
|
||||
closeSize);
|
||||
if (markerRow.rect.Y + markerRow.rect.Height > layout.content.Y &&
|
||||
markerRow.rect.Y < contentBottom)
|
||||
{
|
||||
layout.markerRows.push_back(markerRow);
|
||||
}
|
||||
rowY += itemRow;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部切换按钮:整行宽度、与面板下边界留一个 pad。
|
||||
layout.button = gd::RectF(left + pad, top + height - pad - markerButtonHeight,
|
||||
std::max(0.0f, width - pad * 2.0f), markerButtonHeight);
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
@@ -6207,9 +6353,17 @@ namespace jdi
|
||||
mixRect(layout.panel);
|
||||
mixRect(layout.title);
|
||||
mixRect(layout.close);
|
||||
mixRect(layout.disclosure);
|
||||
mixRect(layout.badge);
|
||||
mixRect(layout.edit);
|
||||
mixRect(layout.button);
|
||||
mixRect(layout.content);
|
||||
mixRect(layout.scrollTrack);
|
||||
mixRect(layout.scrollThumb);
|
||||
mixRect(layout.opacityTrack);
|
||||
mixRect(layout.opacityValue);
|
||||
mix(layout.collapsed ? 1ull : 0ull);
|
||||
mix(layout.markerHasEnumColumn ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(layout.rows));
|
||||
for (const gd::RectF& rect : layout.rowChecks)
|
||||
{
|
||||
@@ -6247,12 +6401,25 @@ namespace jdi
|
||||
mixFloat(column.left);
|
||||
mixFloat(column.width);
|
||||
}
|
||||
// 「自由标记」的分组栏(托底矩形 / 三角 / ✕)与各自的收起状态都在面板像素里。
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
mixRect(group.header);
|
||||
mixRect(group.triangle);
|
||||
mixRect(group.close);
|
||||
mix(group.collapsed ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(group.ref.groupIndex + 2));
|
||||
mix(static_cast<std::uint64_t>(group.ref.signalIndex + 2));
|
||||
}
|
||||
|
||||
const Lane& lane = Tab().Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (kind == EdgePanelKind::MARK)
|
||||
{
|
||||
// 底部切换按钮的文案取决于光标模式 ⇒ 必须进键,否则开关后仍贴回旧像素。
|
||||
// 底部切换按钮的激活态、透明度滑块与内容滚动偏移直接改变面板像素 ⇒ 必须进键,
|
||||
// 否则开关 / 拖滑块 / 滚动之后仍会贴回旧像素。
|
||||
mix(lane.freeMarkerCursor ? 1ull : 0ull);
|
||||
mixFloat(lane.freeMarkerOpacity);
|
||||
mixFloat(lane.freeMarkerScroll);
|
||||
// 固定点的**内容**(哪个信号、什么时刻)直接决定面板文字与行数,必须进键;
|
||||
// 否则新增/删除一个固定点后可能贴回旧像素。
|
||||
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
|
||||
@@ -6499,10 +6666,11 @@ namespace jdi
|
||||
: nullptr;
|
||||
text = FreeMarkerValueText(value, hasPrev, delta, enumText);
|
||||
|
||||
const float closeSize = kFreeMarkerCloseSize * m_scale;
|
||||
const float gap = kFreeMarkerCloseGap * m_scale;
|
||||
// ✕ 按钮 = **与取值胶囊同高的正方形**(用户明确要求:长宽都等于胶囊的上下高度)。
|
||||
const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale;
|
||||
const float textHeight = 17.0f * m_scale;
|
||||
const float closeSize = textHeight;
|
||||
const float gap = kFreeMarkerCloseGap * m_scale;
|
||||
const float totalWidth = textWidth + gap + closeSize;
|
||||
|
||||
float left = rawX + 6.0f * m_scale;
|
||||
@@ -6516,28 +6684,93 @@ namespace jdi
|
||||
top = std::max(clipTop + 1.0f * m_scale, std::min(top, clipBottom - textHeight - 1.0f * m_scale));
|
||||
|
||||
label = gd::RectF(left, top, textWidth, textHeight);
|
||||
close = gd::RectF(left + textWidth + gap, top + (textHeight - closeSize) * 0.5f, closeSize,
|
||||
closeSize);
|
||||
close = gd::RectF(left + textWidth + gap, top, closeSize, closeSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 用 **GDI+** 绘制一段可带 alpha 的文字(仅用于「标记透明度」低于 100% 的胶囊)。
|
||||
*
|
||||
* ⚠️ 界面其余文字一律走 `UiText`(GDI `DrawTextW`)。GDI 文本**忽略 alpha**
|
||||
*(`SetTextColor` 只吃 RGB)⇒ 直接把 alpha 乘进颜色不会让文字淡出。
|
||||
* 因此只有在"固定点被调成半透明"这个特殊状态下才改用 GDI+ `DrawString`:
|
||||
* 那样才能让文字与胶囊一起真正淡出;不透明度为 100% 时仍走 `UiText`,像素零变化。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] text 文本。
|
||||
* @param [IN] font 字体(与 `UiText` 同一 HFONT)。
|
||||
* @param [IN] rect 文本矩形(居中)。
|
||||
* @param [IN] argb 颜色(含目标 alpha)。
|
||||
*/
|
||||
void DrawFreeMarkerAlphaText(gd::Graphics& g, const std::wstring& text, HFONT font,
|
||||
const gd::RectF& rect, std::uint32_t argb)
|
||||
{
|
||||
if (text.empty() || font == nullptr || rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
LOGFONTW lf{};
|
||||
if (GetObjectW(font, static_cast<int>(sizeof(lf)), &lf) != static_cast<int>(sizeof(lf)))
|
||||
{
|
||||
UiText(g, text, font, rect, argb, TextAlign::CENTER);
|
||||
return;
|
||||
}
|
||||
HDC hdc = g.GetHDC();
|
||||
if (hdc == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
gd::Font textFont(hdc, &lf);
|
||||
g.ReleaseHDC(hdc);
|
||||
if (textFont.GetLastStatus() != gd::Ok)
|
||||
{
|
||||
UiText(g, text, font, rect, argb, TextAlign::CENTER);
|
||||
return;
|
||||
}
|
||||
gd::SolidBrush brush(ToGdiColor(argb));
|
||||
gd::StringFormat format;
|
||||
format.SetAlignment(gd::StringAlignmentCenter);
|
||||
format.SetLineAlignment(gd::StringAlignmentCenter);
|
||||
format.SetTrimming(gd::StringTrimmingEllipsisCharacter);
|
||||
format.SetFormatFlags(gd::StringFormatFlagsNoWrap);
|
||||
g.DrawString(text.c_str(), static_cast<INT>(text.size()), &textFont, rect, &format, &brush);
|
||||
}
|
||||
|
||||
void PlotView::PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label,
|
||||
const std::wstring& text, std::uint32_t color,
|
||||
const gd::RectF& close, bool closeHot)
|
||||
const gd::RectF& close, bool closeHot, float alphaScale)
|
||||
{
|
||||
const float scale = std::max(0.0f, std::min(1.0f, alphaScale));
|
||||
const auto scaled = [scale](std::uint32_t argb, float base) -> std::uint32_t {
|
||||
const int alpha = static_cast<int>(std::lround(base * scale));
|
||||
return WithAlpha(argb, (alpha <= 0) ? 0 : (alpha >= 255 ? 255 : alpha));
|
||||
};
|
||||
const UiInk ink = UiContrastInk(color);
|
||||
UiFillRoundRect(g, label, 3.0f * m_scale, WithAlpha(color, 0xF0));
|
||||
DrawLabelOutline(g, label, 3.0f * m_scale, ink.outline);
|
||||
UiText(g, text, m_fonts->monoSmall, label, ink.text, TextAlign::CENTER);
|
||||
if (scale <= 0.02f)
|
||||
{
|
||||
return; // 完全透明:连描边与文字都不画(否则会留下"看不见的"像素)
|
||||
}
|
||||
UiFillRoundRect(g, label, 3.0f * m_scale, scaled(color, 0xF0));
|
||||
DrawLabelOutline(g, label, 3.0f * m_scale, scaled(ink.outline, 255.0f));
|
||||
if (scale >= 0.999f)
|
||||
{
|
||||
UiText(g, text, m_fonts->monoSmall, label, ink.text, TextAlign::CENTER);
|
||||
}
|
||||
else
|
||||
{
|
||||
// GDI 文本忽略 alpha ⇒ 半透明时必须改用 GDI+ 才能真的淡出(见上面说明)。
|
||||
DrawFreeMarkerAlphaText(g, text, m_fonts->monoSmall, label, scaled(ink.text, 255.0f));
|
||||
}
|
||||
if (close.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ✕ 按钮:悬停时用 danger 底色 + 叉(与面板里的 ✕ 同一套语义)。
|
||||
UiFillRoundRect(g, close, 3.0f * m_scale,
|
||||
closeHot ? WithAlpha(m_theme->danger, 0x66) : WithAlpha(color, 0xC0));
|
||||
const float crossPad = 3.5f * m_scale;
|
||||
const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text;
|
||||
closeHot ? WithAlpha(m_theme->danger, 0x66) : scaled(color, 0xC0));
|
||||
const float crossPad = 4.0f * m_scale;
|
||||
const std::uint32_t crossColor =
|
||||
closeHot ? m_theme->danger : scaled(ink.text, 255.0f);
|
||||
UiDrawLine(g, close.X + crossPad, close.Y + crossPad, close.X + close.Width - crossPad,
|
||||
close.Y + close.Height - crossPad, crossColor, 1.4f * m_scale);
|
||||
UiDrawLine(g, close.X + close.Width - crossPad, close.Y + crossPad, close.X + crossPad,
|
||||
@@ -6560,6 +6793,8 @@ namespace jdi
|
||||
const std::size_t count = std::min(lanes.size(), tabLanes.size());
|
||||
|
||||
// ---- (1) 已固定的数据点(含各自的 ✕)----
|
||||
// ⚠️ 固定点**不再画纵向细线**(用户 2026-10-21 明确要求);
|
||||
// 取值胶囊彼此重叠时做**纵向避让**(依次尝试下方 / 上方错开),保证都能读清。
|
||||
for (std::size_t laneIndex = 0; laneIndex < count; ++laneIndex)
|
||||
{
|
||||
const Lane& lane = tabLanes[laneIndex];
|
||||
@@ -6567,6 +6802,8 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float markerAlpha = std::max(0.0f, std::min(1.0f, lane.freeMarkerOpacity));
|
||||
std::vector<gd::RectF> placed;
|
||||
for (std::size_t itemIndex = 0; itemIndex < lane.freeMarkerItems.size(); ++itemIndex)
|
||||
{
|
||||
gd::RectF label;
|
||||
@@ -6592,27 +6829,71 @@ namespace jdi
|
||||
double enumValue = 0.0;
|
||||
const bool valueOk = FreeMarkerValue(lane, item, value, enumValue);
|
||||
|
||||
// 纵向细线(仅限本网格,低透明度):让用户看得出固定点在哪一刻。
|
||||
const float x = TimeToX(item.time);
|
||||
UiDrawLine(g, x, clipTop, x, clipBottom, WithAlpha(signalColor, 0x70),
|
||||
1.0f * m_scale);
|
||||
// ---- 纵向避让:与本次已放置的胶囊重叠时,向下 / 向上交替错开 ----
|
||||
const auto occupied = [&placed](const gd::RectF& rect) {
|
||||
for (const gd::RectF& other : placed)
|
||||
{
|
||||
if (rect.X < other.X + other.Width && other.X < rect.X + rect.Width &&
|
||||
rect.Y < other.Y + other.Height && other.Y < rect.Y + rect.Height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto totalRect = [](const gd::RectF& labelRect, const gd::RectF& closeRect) {
|
||||
const float right = std::max(labelRect.X + labelRect.Width,
|
||||
closeRect.X + closeRect.Width);
|
||||
return gd::RectF(labelRect.X, labelRect.Y, right - labelRect.X,
|
||||
labelRect.Height);
|
||||
};
|
||||
const float step = label.Height + 2.0f * m_scale;
|
||||
const float minTop = clipTop + 1.0f * m_scale;
|
||||
const float maxTop = std::max(minTop, clipBottom - label.Height - 1.0f * m_scale);
|
||||
const float baseTop = label.Y;
|
||||
float chosenTop = baseTop;
|
||||
for (int attempt = 0; attempt <= 8; ++attempt)
|
||||
{
|
||||
const float magnitude = static_cast<float>((attempt + 1) / 2) * step;
|
||||
const float offset =
|
||||
(attempt == 0) ? 0.0f : ((attempt % 2 == 1) ? magnitude : -magnitude);
|
||||
const float top = std::max(minTop, std::min(baseTop + offset, maxTop));
|
||||
gd::RectF candidateLabel = label;
|
||||
gd::RectF candidateClose = close;
|
||||
candidateLabel.Y = top;
|
||||
candidateClose.Y = top;
|
||||
if (!occupied(totalRect(candidateLabel, candidateClose)))
|
||||
{
|
||||
chosenTop = top;
|
||||
break;
|
||||
}
|
||||
}
|
||||
label.Y = chosenTop;
|
||||
close.Y = chosenTop;
|
||||
placed.push_back(totalRect(label, close));
|
||||
|
||||
// 数据点:与光标的数据点同一画法(圆点)。
|
||||
// 数据点:与光标的数据点同一画法(圆点),同样受「标记透明度」控制。
|
||||
const float x = TimeToX(item.time);
|
||||
const float radius = 3.4f * m_scale;
|
||||
gd::SolidBrush dot(ToGdiColor(signalColor));
|
||||
const int dotAlpha = static_cast<int>(std::lround(255.0f * markerAlpha));
|
||||
gd::SolidBrush dot(ToGdiColor(WithAlpha(signalColor, dotAlpha)));
|
||||
const float dotY = valueOk ? ValueToY(value, visual) : (clipTop + clipBottom) * 0.5f;
|
||||
g.FillEllipse(&dot, x - radius, dotY - radius, radius * 2.0f, radius * 2.0f);
|
||||
|
||||
// ✕ 按钮的悬停态决定它的配色,因此先算再画胶囊。
|
||||
const bool hot = (m_freeMarkerHoverLane == static_cast<int>(laneIndex) &&
|
||||
m_freeMarkerHoverItem == itemIndex);
|
||||
PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot);
|
||||
PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot, markerAlpha);
|
||||
|
||||
FreeMarkerHit hit;
|
||||
hit.laneIndex = static_cast<int>(laneIndex);
|
||||
hit.itemIndex = itemIndex;
|
||||
hit.close = close;
|
||||
m_freeMarkerHits.push_back(hit);
|
||||
if (markerAlpha > 0.02f)
|
||||
{
|
||||
// 完全透明时不可见 ⇒ 也不该被点中(否则会“隔着看不见的按钮”移除固定点)。
|
||||
m_freeMarkerHits.push_back(hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7079,6 +7360,33 @@ namespace jdi
|
||||
UiFillRoundRect(g, gd::RectF(panel.X, panel.Y, panel.Width, titleHeight), 5.0f * m_scale,
|
||||
WithAlpha(m_theme->accent, 0x2A));
|
||||
|
||||
// 标题栏左侧的「展开 / 折起」指示器:三角(朝下 = 已展开、朝右 = 已折起)
|
||||
// + 三角后方的一条短横线(经典展开/折起标识),位于 GX 徽标左边。
|
||||
// 单击它切换折起状态;折起后面板只剩标题栏,这里是唯一的“重新展开”按钮。
|
||||
{
|
||||
const gd::RectF& disclosureRect = layout.disclosure;
|
||||
if (disclosureRect.Width > 0.0f)
|
||||
{
|
||||
const bool disclosureHot =
|
||||
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
|
||||
UiPointInRect(disclosureRect, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY));
|
||||
const std::uint32_t disclosureColor =
|
||||
disclosureHot ? m_theme->accent : m_theme->text;
|
||||
const float centerX = disclosureRect.X + 5.5f * m_scale;
|
||||
const float centerY = disclosureRect.Y + disclosureRect.Height * 0.5f;
|
||||
UiDrawTriangleIcon(
|
||||
g,
|
||||
gd::RectF(centerX - 4.5f * m_scale, centerY - 4.5f * m_scale,
|
||||
9.0f * m_scale, 9.0f * m_scale),
|
||||
disclosureColor,
|
||||
layout.collapsed ? IconDirection::RIGHT : IconDirection::DOWN);
|
||||
UiDrawLine(g, centerX + 6.0f * m_scale, centerY,
|
||||
disclosureRect.X + disclosureRect.Width, centerY, disclosureColor,
|
||||
1.4f * m_scale);
|
||||
}
|
||||
}
|
||||
|
||||
// 标题:与左侧信号面板一致的 “GX” 徽标。
|
||||
// ⚠️ 几何必须取 `layout.badge`(与悬停提示 / 手型光标 / 点击联动同源);
|
||||
// 悬停时给一圈强调色描边,作为“可点击”的视觉提示(用户 2026-10-19 要求)。
|
||||
@@ -7125,6 +7433,9 @@ namespace jdi
|
||||
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor,
|
||||
1.6f * m_scale);
|
||||
|
||||
// 折起的面板只保留标题栏:内容区(条目 / 分组栏 / 按钮 / 滚动条)全部省略。
|
||||
if (!layout.collapsed)
|
||||
{
|
||||
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板都采用「逐条目」排布:
|
||||
// 第一排 = 名称 + 复选框,第二排 = 两个参数输入框(仅勾选的条目,
|
||||
// 弱峰调整没有参数输入框),中心轴再多第三排 = 对应的统计结果。
|
||||
@@ -7345,68 +7656,101 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 「自由标记」面板:逐信号一行名称(带清除全部固定点的 ✕),
|
||||
// 下面按时间升序列出该信号的固定点(`值(±增量) = 枚举` + 右对齐的 `t:… Δt:…` + 行尾 ✕)。
|
||||
// 「自由标记」面板:按信号分组的**分组栏**(信号色托底 + 三角 + 名称 +
|
||||
// 「N 标记点」+ ✕),下属固定点条目五列对齐(取值 / 枚举 / 增量 / 时刻 / 间隔),
|
||||
// 行尾各带一个 ✕。内容区有滚动偏移时按内容区裁剪。
|
||||
if (freeMarker)
|
||||
{
|
||||
const float closeSize = kFreeMarkerCloseSize * m_scale;
|
||||
const float gap = 4.0f * m_scale;
|
||||
const float groupRowHeight = kFreeMarkerNameRowHeight * m_scale;
|
||||
bool markerAny = false;
|
||||
for (const Lane::FreeMarkerItem& item : tabLane.freeMarkerItems)
|
||||
{
|
||||
markerAny = markerAny || item.ref.IsValid();
|
||||
}
|
||||
|
||||
const int contentState = g.Save();
|
||||
g.SetClip(layout.content, gd::CombineModeIntersect);
|
||||
|
||||
if (!markerAny)
|
||||
{
|
||||
UiText(g, L"在网格内单击可固定数据点",
|
||||
m_fonts->uiSmall,
|
||||
gd::RectF(panel.X + pad, panel.Y + titleHeight + pad,
|
||||
panel.Width - pad * 2.0f, kFreeMarkerNameRowHeight * m_scale),
|
||||
UiText(g, L"在网格内单击可固定数据点", m_fonts->uiSmall,
|
||||
gd::RectF(layout.content.X, layout.content.Y, layout.content.Width,
|
||||
groupRowHeight),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
|
||||
// ---- 分组栏 ----
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
const ResolvedSignal resolved = ResolveSignal(group.ref);
|
||||
if (resolved.data == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
// 名称行矩形直接取自布局(与 ✕ 同一套几何,绘制侧不反推)。
|
||||
const gd::RectF nameRect =
|
||||
(rowIndex < layout.rowNameRects.size())
|
||||
? layout.rowNameRects[rowIndex]
|
||||
: gd::RectF(panel.X + pad, panel.Y + titleHeight + pad,
|
||||
panel.Width - pad * 2.0f - (closeSize + gap),
|
||||
kFreeMarkerNameRowHeight * m_scale);
|
||||
UiText(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameRect.Width),
|
||||
m_fonts->uiSmall, nameRect, signalColor, TextAlign::LEFT);
|
||||
const std::uint32_t signalColor =
|
||||
(resolved.style != nullptr) ? StyleColor(*resolved.style) : m_theme->border;
|
||||
// 托底 = 信号色;文字一律用**自适应对比色**(与信号面板的组名徽标同规则),
|
||||
// 因此无论信号是亮黄还是深蓝都能看清分组栏上的所有内容。
|
||||
const UiInk ink = UiContrastInk(signalColor);
|
||||
UiFillRoundRect(g, group.header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
|
||||
|
||||
// 信号名右侧的 ✕:清除该信号的**全部**固定点。
|
||||
if (rowIndex < layout.rowNameCloses.size() &&
|
||||
layout.rowNameCloses[rowIndex].Width > 0.0f)
|
||||
// 左侧三角(与信号选择区域组名同款):朝下 = 展开、朝右 = 收起。
|
||||
const bool triangleHot =
|
||||
(m_freeMarkerTriangleHoverLane == static_cast<int>(index) &&
|
||||
m_freeMarkerTriangleHoverSignal == group.ref);
|
||||
UiDrawTriangleIcon(g, group.triangle,
|
||||
triangleHot ? m_theme->accentSoft : ink.text,
|
||||
group.collapsed ? IconDirection::RIGHT
|
||||
: IconDirection::DOWN);
|
||||
|
||||
// 右侧:`N 标记点` + ✕(清除该信号全部固定点)。
|
||||
std::size_t markerCount = 0;
|
||||
for (const Lane::FreeMarkerItem& entry : tabLane.freeMarkerItems)
|
||||
{
|
||||
const gd::RectF& closeRect = layout.rowNameCloses[rowIndex];
|
||||
const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
|
||||
m_freeMarkerPanelHoverSignal == ref &&
|
||||
m_freeMarkerPanelHoverItem == static_cast<std::size_t>(-1));
|
||||
UiFillRoundRect(g, closeRect, 3.0f * m_scale,
|
||||
hot ? WithAlpha(m_theme->danger, 0x66) : WithAlpha(signalColor, 0xC0));
|
||||
const float crossPad = 3.5f * m_scale;
|
||||
const std::uint32_t crossColor = hot ? m_theme->danger : m_theme->textOnAccent;
|
||||
UiDrawLine(g, closeRect.X + crossPad, closeRect.Y + crossPad,
|
||||
closeRect.X + closeRect.Width - crossPad,
|
||||
closeRect.Y + closeRect.Height - crossPad, crossColor,
|
||||
1.4f * m_scale);
|
||||
UiDrawLine(g, closeRect.X + closeRect.Width - crossPad,
|
||||
closeRect.Y + crossPad, closeRect.X + crossPad,
|
||||
closeRect.Y + closeRect.Height - crossPad, crossColor,
|
||||
1.4f * m_scale);
|
||||
if (entry.ref == group.ref)
|
||||
{
|
||||
++markerCount;
|
||||
}
|
||||
}
|
||||
const std::wstring countText =
|
||||
std::to_wstring(markerCount) + L" 标记点";
|
||||
const float countWidth =
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, countText) + 2.0f * m_scale;
|
||||
const float countRight = group.close.X - 5.0f * m_scale;
|
||||
UiText(g, countText, m_fonts->monoSmall,
|
||||
gd::RectF(countRight - countWidth, group.header.Y, countWidth,
|
||||
group.header.Height),
|
||||
ink.text, TextAlign::RIGHT);
|
||||
|
||||
const float nameLeft = group.triangle.X + group.triangle.Width + 6.0f * m_scale;
|
||||
const float nameWidth =
|
||||
std::max(10.0f, countRight - countWidth - 6.0f * m_scale - nameLeft);
|
||||
UiText(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth),
|
||||
m_fonts->uiSmall,
|
||||
gd::RectF(nameLeft, group.header.Y, nameWidth, group.header.Height),
|
||||
ink.text, TextAlign::LEFT);
|
||||
|
||||
const bool closeHot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
|
||||
m_freeMarkerPanelHoverSignal == group.ref &&
|
||||
m_freeMarkerPanelHoverItem == static_cast<std::size_t>(-1));
|
||||
if (closeHot)
|
||||
{
|
||||
UiFillRoundRect(g, group.close, 3.0f * m_scale,
|
||||
WithAlpha(m_theme->danger, 0x77));
|
||||
}
|
||||
const float crossPad = 3.5f * m_scale;
|
||||
const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text;
|
||||
UiDrawLine(g, group.close.X + crossPad, group.close.Y + crossPad,
|
||||
group.close.X + group.close.Width - crossPad,
|
||||
group.close.Y + group.close.Height - crossPad, crossColor,
|
||||
1.5f * m_scale);
|
||||
UiDrawLine(g, group.close.X + group.close.Width - crossPad,
|
||||
group.close.Y + crossPad, group.close.X + crossPad,
|
||||
group.close.Y + group.close.Height - crossPad, crossColor,
|
||||
1.5f * m_scale);
|
||||
}
|
||||
|
||||
// 固定点行(已按信号分组 + 组内时间升序展开)。
|
||||
// ---- 固定点条目(五列,列起点由布局给定 ⇒ 纵向严格对齐)----
|
||||
for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows)
|
||||
{
|
||||
const Lane::FreeMarkerItem& item = tabLane.freeMarkerItems[markerRow.itemIndex];
|
||||
@@ -7426,48 +7770,43 @@ namespace jdi
|
||||
FreeMarkerDelta(tabLane, markerRow.itemIndex, value, hasPrev, delta,
|
||||
timeDelta);
|
||||
}
|
||||
const std::wstring* enumText =
|
||||
resolved.data->enumText.Find(enumValue);
|
||||
const std::wstring* enumText = resolved.data->enumText.Find(enumValue);
|
||||
const std::wstring valueText =
|
||||
FreeMarkerValueText(valueOk ? value : 0.0, hasPrev, delta, enumText);
|
||||
valueOk ? FormatValue(value) : std::wstring(kFreeMarkerNotAvailable);
|
||||
const std::wstring deltaText =
|
||||
(valueOk && hasPrev) ? FormatSignedValue(delta)
|
||||
: std::wstring(kFreeMarkerNotAvailable);
|
||||
const std::wstring timeText = FreeMarkerTimeText(item.time);
|
||||
const std::wstring deltaText = FreeMarkerDeltaText(hasPrev, timeDelta);
|
||||
const std::wstring timeDeltaText =
|
||||
FreeMarkerDeltaText(valueOk && hasPrev, timeDelta);
|
||||
|
||||
// 四列(① 取值 ② 增量 ③ 时刻 ④ 间隔)各列**左对齐在固定横坐标**上,
|
||||
// 于是同一信息在所有条目上纵向上严格对齐(用户明确要求);
|
||||
// 列宽固定,超长文本用 `UiEllipsize` 截断(布局期不做文本测量)。
|
||||
const auto columnRect = [&](EdgePanelLayout::MarkerColumnIndex column) {
|
||||
return gd::RectF(layout.markerColumns[column].left, markerRow.rect.Y,
|
||||
layout.markerColumns[column].width,
|
||||
markerRow.rect.Height);
|
||||
};
|
||||
UiText(g, UiEllipsize(valueText, m_fonts->monoSmall,
|
||||
layout.markerColumns[EdgePanelLayout::MARKER_COL_VALUE].width),
|
||||
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_VALUE),
|
||||
m_theme->text, TextAlign::LEFT);
|
||||
if (hasPrev)
|
||||
const auto paintCell = [&](EdgePanelLayout::MarkerColumnIndex column,
|
||||
const std::wstring& text, std::uint32_t color) {
|
||||
if (layout.markerColumns[column].width <= 0.0f || text.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const gd::RectF rect = columnRect(column);
|
||||
UiText(g, UiEllipsize(text, m_fonts->monoSmall, rect.Width),
|
||||
m_fonts->monoSmall, rect, color, TextAlign::LEFT);
|
||||
};
|
||||
paintCell(EdgePanelLayout::MARKER_COL_VALUE, valueText, m_theme->textStrong);
|
||||
if (layout.markerHasEnumColumn)
|
||||
{
|
||||
UiText(g,
|
||||
UiEllipsize(FormatSignedValue(delta), m_fonts->monoSmall,
|
||||
layout.markerColumns[EdgePanelLayout::MARKER_COL_DELTA].width),
|
||||
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_DELTA),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
UiText(g,
|
||||
UiEllipsize(timeText, m_fonts->monoSmall,
|
||||
layout.markerColumns[EdgePanelLayout::MARKER_COL_TIME].width),
|
||||
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_TIME),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
if (!deltaText.empty())
|
||||
{
|
||||
UiText(g,
|
||||
UiEllipsize(deltaText, m_fonts->monoSmall,
|
||||
layout.markerColumns[EdgePanelLayout::MARKER_COL_DT].width),
|
||||
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_DT),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_ENUM,
|
||||
(enumText != nullptr) ? *enumText : std::wstring(),
|
||||
m_theme->text);
|
||||
}
|
||||
paintCell(EdgePanelLayout::MARKER_COL_DELTA, deltaText, m_theme->textMuted);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_TIME, timeText, m_theme->textMuted);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_DT, timeDeltaText, m_theme->textMuted);
|
||||
|
||||
// 行尾 ✕:移除该固定点。
|
||||
// 行尾 ✕:移除该固定点(与之前一致)。
|
||||
const gd::RectF& closeRect = markerRow.close;
|
||||
const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
|
||||
m_freeMarkerPanelHoverItem == markerRow.itemIndex);
|
||||
@@ -7484,6 +7823,39 @@ namespace jdi
|
||||
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad,
|
||||
crossColor, 1.2f * m_scale);
|
||||
}
|
||||
|
||||
g.Restore(contentState);
|
||||
|
||||
// ---- 内容滚动条(只在内容超高时出现;按钮与滑块不参与滚动)----
|
||||
if (layout.scrollTrack.Width > 0.0f)
|
||||
{
|
||||
UiFillRoundRect(g, layout.scrollTrack, layout.scrollTrack.Width * 0.5f,
|
||||
WithAlpha(m_theme->border, 0x88));
|
||||
UiFillRoundRect(g, layout.scrollThumb, layout.scrollThumb.Width * 0.5f,
|
||||
m_theme->borderStrong);
|
||||
}
|
||||
|
||||
// ---- 「标记透明度」滑动条(仅「光标选取模式」激活时出现,固定在按钮上方)----
|
||||
if (layout.opacityTrack.Width > 0.0f)
|
||||
{
|
||||
UiText(g, kFreeMarkerOpacityLabel, m_fonts->uiSmall,
|
||||
gd::RectF(panel.X + pad, layout.opacityValue.Y,
|
||||
layout.opacityTrack.X - panel.X - pad, layout.opacityValue.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
const bool sliderActive =
|
||||
(m_dragMode == PlotDragMode::MARKER_OPACITY &&
|
||||
m_markerOpacityDragLane == static_cast<int>(index)) ||
|
||||
UiPointInRect(layout.opacityTrack, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY));
|
||||
// 外观与信号样式区的「透明度」滑动条完全相同(同一 `UiDrawSlider` 口径)。
|
||||
UiDrawSlider(g, layout.opacityTrack,
|
||||
static_cast<double>(tabLane.freeMarkerOpacity), m_theme->accent,
|
||||
sliderActive, m_scale);
|
||||
const int percent =
|
||||
static_cast<int>(tabLane.freeMarkerOpacity * 100.0f + 0.5f);
|
||||
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall,
|
||||
layout.opacityValue, m_theme->textStrong, TextAlign::RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
float rowY = panel.Y + titleHeight + pad;
|
||||
@@ -7520,7 +7892,8 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮);
|
||||
// 「自由标记」面板的按钮是「启用光标模式 / 关闭光标模式」的切换按钮。
|
||||
// 「自由标记」面板的按钮是「光标选取模式」的**切换类按钮**
|
||||
//(交互外观与工具栏的切换按钮一致:激活时浅强调色底 + 强调描边 + 强文本色)。
|
||||
if (hasButton || freeMarker)
|
||||
{
|
||||
const gd::RectF& buttonRect = layout.button;
|
||||
@@ -7532,16 +7905,21 @@ namespace jdi
|
||||
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
|
||||
UiPointInRect(inner, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY));
|
||||
UiFillRoundRect(g, inner, 4.0f * m_scale,
|
||||
hot ? WithAlpha(m_theme->accent, 0x3A) : m_theme->cardBg);
|
||||
UiDrawRoundRect(g, inner, 4.0f * m_scale, m_theme->accent, 1.2f);
|
||||
const std::wstring label =
|
||||
freeMarker ? (tabLane.freeMarkerCursor ? std::wstring(kFreeMarkerCursorOffLabel)
|
||||
: std::wstring(kFreeMarkerCursorOnLabel))
|
||||
: std::wstring(kBuildSignalLabel);
|
||||
UiText(g, label, m_fonts->uiSmall, inner, m_theme->accent, TextAlign::CENTER);
|
||||
const bool checked = freeMarker && tabLane.freeMarkerCursor;
|
||||
const std::uint32_t background =
|
||||
checked ? (hot ? MixColor(m_theme->accentSoft, m_theme->accent, 0.16)
|
||||
: m_theme->accentSoft)
|
||||
: (hot ? m_theme->hover : m_theme->cardBg);
|
||||
UiFillRoundRect(g, inner, 4.0f * m_scale, background);
|
||||
UiDrawRoundRect(g, inner, 4.0f * m_scale,
|
||||
checked ? m_theme->accent : m_theme->border, 1.2f);
|
||||
const std::wstring label = freeMarker ? std::wstring(kFreeMarkerCursorModeLabel)
|
||||
: std::wstring(kBuildSignalLabel);
|
||||
UiText(g, label, m_fonts->uiSmall, inner,
|
||||
checked ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||
}
|
||||
}
|
||||
} // if (!layout.collapsed)
|
||||
|
||||
// 面板画完了:把它刚画进后备缓冲区的像素(连同缓存键)存进面板图层,
|
||||
// 下一帧只要键不变就直接贴图。矩形外的 4 逻辑像素边距用于容纳投影。
|
||||
|
||||
Reference in new issue
Block a user