界面增强:网格图例 GX 编号、工具栏状态修复、增量括号、实验性边沿计数
- 每个网格图例左侧新增当前网格的 “GX” 编号徽标,与左侧信号列表行末的 G1/G2 徽标
使用完全一致的样式(同宽高、同圆角、同配色、同字体),并纳入图例底板范围。
- 修复工具栏 toggle 状态被重置的缺陷:Toolbar::Layout() 会重建全部按钮,
原先会把 checked 全部清零。因此当「光标」按钮因为模式变化(光标x2 → 光标)
触发文本变更与 Relayout 时,同帧刚设置好的「Y 轴自适应」「选区放大」勾选态被清掉,
表现为切换光标模式时前面两个开关被一并取消。现在 Layout() 会按 action 继承既有
checked 状态,重建后状态保持不变。
- 双光标右侧光标的增量显示去掉逗号,改为包裹在圆括号中:`4.9425 (+0.0008)`。
- 网格右键菜单在分隔横线上方新增「实验性功能」(带二级子菜单指示箭头):
鼠标悬停即向右展开二级菜单,其中包含「边沿计数」切换项(带 √)。
为此为 PopupMenu 增加了完整的二级子菜单支持:子菜单排版(贴主菜单右缘、
右侧空间不足时翻到左侧、限制在宿主窗口内)、悬停展开/移开收起、
点击父项不下发命令、子菜单内按下/抬起配对提交、键盘(← 收起 / → 展开 /
↑↓ 在子菜单内移动 / Enter 提交 / Esc 先收起子菜单再关菜单)、
独立的投影与绘制、ContainsPoint 覆盖子菜单区域、Relayout 后子菜单位置重建。
- 「边沿计数」开启后(按网格独立,状态随 RebuildLanes 按信号继承):
* 在该网格绘制一条横向光标,风格与纵向时间光标一致(同色同线宽),
左右两端各带一个尖端朝内的三角把手。
* 左端显示当前光标的 Y 轴值,读数胶囊纵向居中于横线、靠网格左侧。
* 横向光标不绘制任何数据点取值标注(与纵向光标的行为不同)。
* 横向光标可上下拖动改变幅值,拖动时显示上下箭头光标。
* 生成带 ✕ 关闭按钮的浮动面板:标题栏显示 “GX” 徽标 + “边沿计数”,
下方按信号列出名称(信号色)与「上升沿 N / 下降沿 M」。
面板可在整个绘图区内拖动(限制在绘图区内),✕ 仅收起面板、保留横向光标,
重新勾选菜单项可再次展开。
* 边沿判定阈值为信号幅值的 (min+max)/2 中点穿越,NaN 视为断点;
统计结果按 (文件, 信号) 缓存,数据层变化时随折线缓存一并失效。
验证(TestData/VERY_BIG_DATA.CSV,编码器A/B相密集方波,125%、1725x996):
- 图例编号:G1 / G2 徽标与左侧列表徽标样式一致。
- 工具栏:开启「Y 轴自适应」+「选区放大」后,光标由 光标x2 切回 光标,
两个开关仍保持勾选(截图核对)。
- 增量:右光标显示 `4.9425 (+0.0008)`。
- 二级菜单:悬停「实验性功能」展开「边沿计数」,箭头与子菜单位置正确。
- 边沿计数:横向光标 + 左端读数 `2.54415` + 面板
`G1 边沿计数 / 编码器A相 / 上升沿 1333422 下降沿 1333421`;
上下拖动后横线上移到 4.13602(与 Y 轴刻度一致);面板拖到绘图区左下并保持在内;
点击 ✕ 后面板收起而横向光标保留;
光标形态探测:横线悬停 SIZENS、面板悬停 SIZEALL。
- 合并网格后面板同时列出网格内两条信号(编码器A相 / 编码器B相)。
- 回归:[1].CSV 双光标正常(光标x2),Δt:151ms 200us;
make clean && make all 无警告无错误。
- 修复过程中发现:静态场景缓存命中路径(贴图快路径)与整幅重绘路径是两处独立的
叠加层绘制入口,新增的随鼠标变化内容必须同时加在两边。
This commit is contained in:
1 parent
a566d98bab
commit
cd6e0ed058
8 files changed
+1137
-25
No files matched your search
@@ -1441,6 +1441,19 @@ namespace jdi
|
|||||||
fitLaneTime.id = PLOT_CMD_LANE_FIT_TIME;
|
fitLaneTime.id = PLOT_CMD_LANE_FIT_TIME;
|
||||||
items.push_back(fitLaneTime);
|
items.push_back(fitLaneTime);
|
||||||
|
|
||||||
|
// 实验性功能(二级菜单):横线上方,鼠标悬停即向右展开。
|
||||||
|
MenuItem experimental;
|
||||||
|
experimental.kind = MenuItemKind::SUBMENU;
|
||||||
|
experimental.label = L"实验性功能";
|
||||||
|
{
|
||||||
|
MenuItem edgeCount;
|
||||||
|
edgeCount.label = L"边沿计数";
|
||||||
|
edgeCount.id = PLOT_CMD_LANE_EDGE_COUNT;
|
||||||
|
edgeCount.checked = m_plot.LaneEdgeCount(laneIndex);
|
||||||
|
experimental.submenu.push_back(edgeCount);
|
||||||
|
}
|
||||||
|
items.push_back(experimental);
|
||||||
|
|
||||||
MenuItem separator;
|
MenuItem separator;
|
||||||
separator.kind = MenuItemKind::SEPARATOR;
|
separator.kind = MenuItemKind::SEPARATOR;
|
||||||
items.push_back(separator);
|
items.push_back(separator);
|
||||||
@@ -1540,6 +1553,10 @@ namespace jdi
|
|||||||
// 「按网格时间轴展开」:展开时间轴后同时把该网格的 Y 轴自适应一次。
|
// 「按网格时间轴展开」:展开时间轴后同时把该网格的 Y 轴自适应一次。
|
||||||
m_plot.FitTimeToLane(laneIndex);
|
m_plot.FitTimeToLane(laneIndex);
|
||||||
break;
|
break;
|
||||||
|
case PLOT_CMD_LANE_EDGE_COUNT:
|
||||||
|
// 「实验性功能 → 边沿计数」:切换本网格的横向光标与统计面板。
|
||||||
|
m_plot.ToggleLaneEdgeCount(laneIndex);
|
||||||
|
break;
|
||||||
case PLOT_CMD_FIT_ALL:
|
case PLOT_CMD_FIT_ALL:
|
||||||
m_plot.FitAll();
|
m_plot.FitAll();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ namespace jdi
|
|||||||
SCROLL_LANES, /**< 拖动网格滚动条。 */
|
SCROLL_LANES, /**< 拖动网格滚动条。 */
|
||||||
CURSOR_MOVE, /**< 拖拽时间光标。 */
|
CURSOR_MOVE, /**< 拖拽时间光标。 */
|
||||||
CURSOR_SPAN_MOVE, /**< 拖拽双光标差值标签:两条光标整体平移,间隔不变。 */
|
CURSOR_SPAN_MOVE, /**< 拖拽双光标差值标签:两条光标整体平移,间隔不变。 */
|
||||||
MOVE_SIGNAL /**< 拖拽信号曲线到其它网格。 */
|
MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */
|
||||||
|
EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */
|
||||||
|
EDGE_PANEL_MOVE /**< 拖动边沿计数浮动面板。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +65,8 @@ namespace jdi
|
|||||||
PLOT_CMD_LANE_Y_MIN = 3, /**< 输入该网格 Y 轴最小值。 */
|
PLOT_CMD_LANE_Y_MIN = 3, /**< 输入该网格 Y 轴最小值。 */
|
||||||
PLOT_CMD_FIT_ALL = 4, /**< 全部网格与时间轴自适应。 */
|
PLOT_CMD_FIT_ALL = 4, /**< 全部网格与时间轴自适应。 */
|
||||||
PLOT_CMD_LANE_FIT_TIME = 5, /**< 时间轴自适应到本网格信号范围(按网格时间轴展开)。 */
|
PLOT_CMD_LANE_FIT_TIME = 5, /**< 时间轴自适应到本网格信号范围(按网格时间轴展开)。 */
|
||||||
PLOT_CMD_LANE_AUTO_VIEW = 6 /**< 切换本网格的「选区 Y 轴自适应」开关。 */
|
PLOT_CMD_LANE_AUTO_VIEW = 6, /**< 切换本网格的「选区 Y 轴自适应」开关。 */
|
||||||
|
PLOT_CMD_LANE_EDGE_COUNT = 7 /**< 切换本网格的「实验性功能 → 边沿计数」开关。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -382,6 +385,28 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
bool SetAllLanesAutoFitToSelection(bool enabled, bool applyNow = true);
|
bool SetAllLanesAutoFitToSelection(bool enabled, bool applyNow = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断某网格是否开启了「实验性功能 → 边沿计数」(菜单勾选标记)。
|
||||||
|
* @param [IN] laneIndex 网格下标。
|
||||||
|
* @return 开启返回 true。
|
||||||
|
*/
|
||||||
|
bool LaneEdgeCount(int laneIndex) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置某网格的「边沿计数」开关。
|
||||||
|
* @param [IN] laneIndex 网格下标。
|
||||||
|
* @param [IN] enabled 目标状态。
|
||||||
|
* @return 状态发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool SetLaneEdgeCount(int laneIndex, bool enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换某网格的「边沿计数」开关(开启时自动初始化横向光标位置与面板)。
|
||||||
|
* @param [IN] laneIndex 网格下标。
|
||||||
|
* @return 切换后的状态。
|
||||||
|
*/
|
||||||
|
bool ToggleLaneEdgeCount(int laneIndex);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 进入某网格 Y 轴上限/下限的内联输入状态。
|
* @brief 进入某网格 Y 轴上限/下限的内联输入状态。
|
||||||
* @param [IN] laneIndex 网格下标。
|
* @param [IN] laneIndex 网格下标。
|
||||||
@@ -814,6 +839,46 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void PaintCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes);
|
void PaintCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制「边沿计数」的横向光标(含两端把手、左侧幅值读数)与统计浮动面板。
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
* @param [IN] lanes 网格绘制信息集合。
|
||||||
|
*/
|
||||||
|
void PaintEdgeCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断坐标是否命中某网格的「边沿计数」横向光标线。
|
||||||
|
* @param [IN] x 客户区横坐标。
|
||||||
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
* @param [OUT] laneIndex 命中的网格下标。
|
||||||
|
* @return 命中返回 true。
|
||||||
|
*/
|
||||||
|
bool IsOnEdgeCursorLine(int x, int y, int& laneIndex) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取边沿统计面板的矩形(未展开时返回空矩形)。
|
||||||
|
* @param [IN] laneIndex 网格下标。
|
||||||
|
* @return 面板矩形。
|
||||||
|
*/
|
||||||
|
gd::RectF EdgePanelRect(int laneIndex) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取边沿统计面板上关闭按钮的矩形。
|
||||||
|
* @param [IN] laneIndex 网格下标。
|
||||||
|
* @return 关闭按钮矩形。
|
||||||
|
*/
|
||||||
|
gd::RectF EdgePanelCloseRect(int laneIndex) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 统计某信号的上/下边沿数量(以信号幅值中点为判定阈值),结果带缓存。
|
||||||
|
* @param [IN] file 信号所属文件。
|
||||||
|
* @param [IN] signal 目标信号。
|
||||||
|
* @param [OUT] rising 上升沿数量。
|
||||||
|
* @param [OUT] falling 下降沿数量。
|
||||||
|
*/
|
||||||
|
void EdgeCountsFor(const DataFile& file, const SignalData& signal, std::size_t& rising,
|
||||||
|
std::size_t& falling);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制拖动中的缩放选区。
|
* @brief 绘制拖动中的缩放选区。
|
||||||
* @param [IN,OUT] g 绘图上下文。
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
@@ -934,6 +999,21 @@ namespace jdi
|
|||||||
std::vector<gd::RectF> m_cursorLabelRects; /**< 本次绘制已占用的取值标注矩形(用于避让重叠)。 */
|
std::vector<gd::RectF> m_cursorLabelRects; /**< 本次绘制已占用的取值标注矩形(用于避让重叠)。 */
|
||||||
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
|
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
|
||||||
|
|
||||||
|
/** 边沿统计结果缓存项(按「文件指针 + 信号指针」标识,数据变化时整体失效)。 */
|
||||||
|
struct EdgeCountEntry
|
||||||
|
{
|
||||||
|
const DataFile* file = nullptr;
|
||||||
|
const SignalData* signal = nullptr;
|
||||||
|
std::size_t rising = 0;
|
||||||
|
std::size_t falling = 0;
|
||||||
|
};
|
||||||
|
std::vector<EdgeCountEntry> m_edgeCountCache; /**< 边沿统计缓存。 */
|
||||||
|
|
||||||
|
// 「边沿计数」横向光标 / 面板的拖拽状态。
|
||||||
|
int m_edgeDragLane = -1; /**< 正在拖动的横向光标所属网格。 */
|
||||||
|
int m_edgePanelDragLane = -1; /**< 正在拖动的统计面板所属网格。 */
|
||||||
|
gd::PointF m_edgePanelDragOffset; /**< 抓取面板时的相对偏移(保持不跳变)。 */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 图例条目的命中信息(用于点选与双击改名)。
|
* @brief 图例条目的命中信息(用于点选与双击改名)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ namespace jdi
|
|||||||
const float kLegendSwatchSize = 8.0f; /**< 图例色块尺寸。 */
|
const float kLegendSwatchSize = 8.0f; /**< 图例色块尺寸。 */
|
||||||
const float kLegendItemGap = 14.0f; /**< 图例条目之间的间距。 */
|
const float kLegendItemGap = 14.0f; /**< 图例条目之间的间距。 */
|
||||||
const float kLegendMaxTextWidth = 200.0f; /**< 图例条目的文本宽度上限(超出则省略)。 */
|
const float kLegendMaxTextWidth = 200.0f; /**< 图例条目的文本宽度上限(超出则省略)。 */
|
||||||
|
const float kLaneBadgeWidth = 30.0f; /**< 网格编号徽标宽度(与信号面板保持一致)。 */
|
||||||
|
const float kLaneBadgeHeight = 17.0f; /**< 网格编号徽标高度。 */
|
||||||
|
const float kLaneBadgeGap = 8.0f; /**< 编号徽标与图例条目之间的间距。 */
|
||||||
|
const float kEdgePanelWidth = 230.0f; /**< 边沿统计浮动面板宽度。 */
|
||||||
|
const float kEdgePanelTitleHeight = 26.0f; /**< 面板标题栏高度。 */
|
||||||
|
const float kEdgePanelRowHeight = 18.0f; /**< 面板内单行文本高度。 */
|
||||||
|
const float kEdgePanelPad = 7.0f; /**< 面板内边距。 */
|
||||||
const std::uint64_t kHintHoldMs = 1300; /**< 提示浮层完全显示的停留时长(毫秒)。 */
|
const std::uint64_t kHintHoldMs = 1300; /**< 提示浮层完全显示的停留时长(毫秒)。 */
|
||||||
const std::uint64_t kHintFadeMs = 500; /**< 提示浮层的淡出时长(毫秒)。 */
|
const std::uint64_t kHintFadeMs = 500; /**< 提示浮层的淡出时长(毫秒)。 */
|
||||||
|
|
||||||
@@ -899,6 +906,8 @@ namespace jdi
|
|||||||
void PlotView::InvalidatePolylineCache()
|
void PlotView::InvalidatePolylineCache()
|
||||||
{
|
{
|
||||||
m_polylineCache.clear();
|
m_polylineCache.clear();
|
||||||
|
// 数据层结构变化后边沿统计结果也会失效。
|
||||||
|
m_edgeCountCache.clear();
|
||||||
InvalidateSceneCache();
|
InvalidateSceneCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1291,6 +1300,106 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PlotView::LaneEdgeCount(int laneIndex) const
|
||||||
|
{
|
||||||
|
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].edgeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlotView::SetLaneEdgeCount(int laneIndex, bool enabled)
|
||||||
|
{
|
||||||
|
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)];
|
||||||
|
if (lane.edgeCount == enabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lane.edgeCount = enabled;
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
// 首次开启:把横向光标放到可见幅值范围的中点,并让面板重新展开。
|
||||||
|
if (!lane.edgeValueValid)
|
||||||
|
{
|
||||||
|
double yMin = 0.0;
|
||||||
|
double yMax = 1.0;
|
||||||
|
LaneRange(laneIndex, yMin, yMax);
|
||||||
|
lane.edgeValue = (yMin + yMax) * 0.5;
|
||||||
|
lane.edgeValueValid = true;
|
||||||
|
}
|
||||||
|
lane.edgePanelOpen = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lane.edgePanelOpen = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlotView::ToggleLaneEdgeCount(int laneIndex)
|
||||||
|
{
|
||||||
|
const bool enabled = !LaneEdgeCount(laneIndex);
|
||||||
|
SetLaneEdgeCount(laneIndex, enabled);
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotView::EdgeCountsFor(const DataFile& file, const SignalData& signal, std::size_t& rising,
|
||||||
|
std::size_t& falling)
|
||||||
|
{
|
||||||
|
rising = 0;
|
||||||
|
falling = 0;
|
||||||
|
|
||||||
|
for (const EdgeCountEntry& entry : m_edgeCountCache)
|
||||||
|
{
|
||||||
|
if (entry.file == &file && entry.signal == &signal)
|
||||||
|
{
|
||||||
|
rising = entry.rising;
|
||||||
|
falling = entry.falling;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判定阈值取信号幅值中点:对数字/方波类信号等价于电平翻转,对模拟信号则是穿越中值。
|
||||||
|
const double mid = (signal.minValue + signal.maxValue) * 0.5;
|
||||||
|
bool hasPrevious = false;
|
||||||
|
bool previousHigh = false;
|
||||||
|
for (double value : signal.values)
|
||||||
|
{
|
||||||
|
if (!std::isfinite(value))
|
||||||
|
{
|
||||||
|
// NaN 视为断点:不参与边沿判定,也不与前一点相连。
|
||||||
|
hasPrevious = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool high = (value >= mid);
|
||||||
|
if (hasPrevious && high != previousHigh)
|
||||||
|
{
|
||||||
|
if (high)
|
||||||
|
{
|
||||||
|
++rising;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
++falling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousHigh = high;
|
||||||
|
hasPrevious = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EdgeCountEntry entry;
|
||||||
|
entry.file = &file;
|
||||||
|
entry.signal = &signal;
|
||||||
|
entry.rising = rising;
|
||||||
|
entry.falling = falling;
|
||||||
|
m_edgeCountCache.push_back(entry);
|
||||||
|
}
|
||||||
|
|
||||||
bool PlotView::ToggleLaneAutoFitToSelection(int laneIndex)
|
bool PlotView::ToggleLaneAutoFitToSelection(int laneIndex)
|
||||||
{
|
{
|
||||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||||
@@ -2179,6 +2288,12 @@ namespace jdi
|
|||||||
case PlotDragMode::MOVE_SIGNAL:
|
case PlotDragMode::MOVE_SIGNAL:
|
||||||
// 拖动信号(参考 Photoshop 平移元素的抓握光标)。
|
// 拖动信号(参考 Photoshop 平移元素的抓握光标)。
|
||||||
return LoadCursorW(nullptr, IDC_SIZEALL);
|
return LoadCursorW(nullptr, IDC_SIZEALL);
|
||||||
|
case PlotDragMode::EDGE_MOVE:
|
||||||
|
// 「边沿计数」横向光标:上下移动幅值。
|
||||||
|
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||||
|
case PlotDragMode::EDGE_PANEL_MOVE:
|
||||||
|
// 边沿统计浮动面板:可自由拖动。
|
||||||
|
return LoadCursorW(nullptr, IDC_SIZEALL);
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -2189,6 +2304,25 @@ namespace jdi
|
|||||||
return LoadCursorW(nullptr, IDC_SIZENS);
|
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 边沿统计面板与横向光标优先于信号悬停提示判定(面板会盖住波形)。
|
||||||
|
if (m_tab != nullptr)
|
||||||
|
{
|
||||||
|
for (int index = 0; index < static_cast<int>(m_lanes.size()); ++index)
|
||||||
|
{
|
||||||
|
const gd::RectF panel = EdgePanelRect(index);
|
||||||
|
if (panel.Width > 0.0f &&
|
||||||
|
UiPointInRect(panel, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
return LoadCursorW(nullptr, IDC_SIZEALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int edgeLane = -1;
|
||||||
|
if (IsOnEdgeCursorLine(x, y, edgeLane))
|
||||||
|
{
|
||||||
|
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 光标时间读数标签与双光标差值标签:**悬停(未按下)时不改变鼠标形状**。
|
// 光标时间读数标签与双光标差值标签:**悬停(未按下)时不改变鼠标形状**。
|
||||||
// 两个标签都叠在光标线的正上方,因此必须先于"光标线"判定返回 nullptr,
|
// 两个标签都叠在光标线的正上方,因此必须先于"光标线"判定返回 nullptr,
|
||||||
// 由调用方回退成箭头;只有真正拖拽时才切成左右箭头(见 CURSOR_MOVE / CURSOR_SPAN_MOVE)。
|
// 由调用方回退成箭头;只有真正拖拽时才切成左右箭头(见 CURSOR_MOVE / CURSOR_SPAN_MOVE)。
|
||||||
@@ -2379,6 +2513,51 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case PlotDragMode::EDGE_MOVE:
|
||||||
|
{
|
||||||
|
// 横向光标:上下移动幅值(夹在该网格的可见幅值范围内)。
|
||||||
|
if (m_tab == nullptr || m_edgeDragLane < 0 ||
|
||||||
|
m_edgeDragLane >= static_cast<int>(m_lanes.size()) ||
|
||||||
|
m_edgeDragLane >= static_cast<int>(m_tab->Lanes().size()))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(m_edgeDragLane)];
|
||||||
|
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_edgeDragLane)];
|
||||||
|
double value = YToValue(static_cast<float>(y), visual);
|
||||||
|
const double low = std::min(visual.yMin, visual.yMax);
|
||||||
|
const double high = std::max(visual.yMin, visual.yMax);
|
||||||
|
value = std::max(low, std::min(value, high));
|
||||||
|
lane.edgeValue = value;
|
||||||
|
lane.edgeValueValid = true;
|
||||||
|
m_dragMoved = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case PlotDragMode::EDGE_PANEL_MOVE:
|
||||||
|
{
|
||||||
|
// 浮动面板:自由拖动,但整体限制在绘图区内。
|
||||||
|
if (m_tab == nullptr || m_edgePanelDragLane < 0 ||
|
||||||
|
m_edgePanelDragLane >= static_cast<int>(m_tab->Lanes().size()))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const gd::RectF panel = EdgePanelRect(m_edgePanelDragLane);
|
||||||
|
if (panel.Width <= 0.0f)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
float left = static_cast<float>(x) - m_edgePanelDragOffset.X;
|
||||||
|
float top = static_cast<float>(y) - m_edgePanelDragOffset.Y;
|
||||||
|
left = std::max(m_plotRect.X + 2.0f * m_scale,
|
||||||
|
std::min(left, m_plotRect.X + m_plotRect.Width - panel.Width - 2.0f * m_scale));
|
||||||
|
top = std::max(m_plotRect.Y + 2.0f * m_scale,
|
||||||
|
std::min(top, m_plotRect.Y + m_plotRect.Height - panel.Height - 2.0f * m_scale));
|
||||||
|
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_edgePanelDragLane)];
|
||||||
|
lane.edgePanelX = left;
|
||||||
|
lane.edgePanelY = top;
|
||||||
|
m_dragMoved = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
case PlotDragMode::NONE:
|
case PlotDragMode::NONE:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -2386,6 +2565,19 @@ namespace jdi
|
|||||||
|
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
|
// 鼠标压在边沿统计面板上时不再触发信号悬停提示(面板本来就盖住波形)。
|
||||||
|
bool overEdgePanel = false;
|
||||||
|
for (int index = 0; index < static_cast<int>(m_lanes.size()); ++index)
|
||||||
|
{
|
||||||
|
const gd::RectF panel = EdgePanelRect(index);
|
||||||
|
if (panel.Width > 0.0f &&
|
||||||
|
UiPointInRect(panel, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
overEdgePanel = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const int lane = LaneAtY(y);
|
const int lane = LaneAtY(y);
|
||||||
if (lane != m_hoverLane)
|
if (lane != m_hoverLane)
|
||||||
{
|
{
|
||||||
@@ -2410,7 +2602,7 @@ namespace jdi
|
|||||||
// 常规模式下精确检测鼠标是否贴近某条信号曲线,用于给出可拖拽提示。
|
// 常规模式下精确检测鼠标是否贴近某条信号曲线,用于给出可拖拽提示。
|
||||||
// 图例优先于曲线:图例条目本身就是信号的可点选目标(与左侧信号列表一致)。
|
// 图例优先于曲线:图例条目本身就是信号的可点选目标(与左侧信号列表一致)。
|
||||||
SignalRef hoverSignal;
|
SignalRef hoverSignal;
|
||||||
if (Tab().ZoomMode() == PlotZoomMode::NONE && !m_cursorHover)
|
if (Tab().ZoomMode() == PlotZoomMode::NONE && !m_cursorHover && !overEdgePanel)
|
||||||
{
|
{
|
||||||
hoverSignal = LegendAtPoint(x, y);
|
hoverSignal = LegendAtPoint(x, y);
|
||||||
if (!hoverSignal.IsValid())
|
if (!hoverSignal.IsValid())
|
||||||
@@ -2517,6 +2709,42 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 边沿统计浮动面板:关闭按钮 / 面板拖动(面板压在波形之上,必须先于信号拖拽判定)。
|
||||||
|
for (int index = 0; index < static_cast<int>(m_lanes.size()); ++index)
|
||||||
|
{
|
||||||
|
const gd::RectF panel = EdgePanelRect(index);
|
||||||
|
if (panel.Width <= 0.0f || !UiPointInRect(panel, fx, fy))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (UiPointInRect(EdgePanelCloseRect(index), fx, fy))
|
||||||
|
{
|
||||||
|
// ✕ 只收起面板;横向光标仍保留(重新勾选菜单项可再次展开)。
|
||||||
|
if (m_tab != nullptr)
|
||||||
|
{
|
||||||
|
m_tab->Lanes()[static_cast<std::size_t>(index)].edgePanelOpen = false;
|
||||||
|
}
|
||||||
|
m_dragMode = PlotDragMode::NONE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
m_dragMode = PlotDragMode::EDGE_PANEL_MOVE;
|
||||||
|
m_edgePanelDragLane = index;
|
||||||
|
m_edgePanelDragOffset.X = fx - panel.X;
|
||||||
|
m_edgePanelDragOffset.Y = fy - panel.Y;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 「边沿计数」横向光标:上下拖动改变幅值。
|
||||||
|
{
|
||||||
|
int edgeLane = -1;
|
||||||
|
if (IsOnEdgeCursorLine(x, y, edgeLane))
|
||||||
|
{
|
||||||
|
m_dragMode = PlotDragMode::EDGE_MOVE;
|
||||||
|
m_edgeDragLane = edgeLane;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!UiPointInRect(m_plotRect, fx, fy))
|
if (!UiPointInRect(m_plotRect, fx, fy))
|
||||||
{
|
{
|
||||||
m_dragMode = PlotDragMode::NONE;
|
m_dragMode = PlotDragMode::NONE;
|
||||||
@@ -2620,6 +2848,8 @@ namespace jdi
|
|||||||
m_dragMoved = false;
|
m_dragMoved = false;
|
||||||
m_rubberRect = gd::RectF();
|
m_rubberRect = gd::RectF();
|
||||||
m_cursorGrabOffset = 0;
|
m_cursorGrabOffset = 0;
|
||||||
|
m_edgeDragLane = -1;
|
||||||
|
m_edgePanelDragLane = -1;
|
||||||
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。
|
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。
|
||||||
ReleaseHint();
|
ReleaseHint();
|
||||||
(void)x;
|
(void)x;
|
||||||
@@ -3108,6 +3338,7 @@ namespace jdi
|
|||||||
PaintHoverHighlight(g);
|
PaintHoverHighlight(g);
|
||||||
PaintRubberBand(g);
|
PaintRubberBand(g);
|
||||||
PaintCursor(g, m_lanes);
|
PaintCursor(g, m_lanes);
|
||||||
|
PaintEdgeCursor(g, m_lanes);
|
||||||
PaintSignalInteraction(g);
|
PaintSignalInteraction(g);
|
||||||
PaintHint(g);
|
PaintHint(g);
|
||||||
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
||||||
@@ -3153,6 +3384,7 @@ namespace jdi
|
|||||||
PaintHoverHighlight(g);
|
PaintHoverHighlight(g);
|
||||||
PaintRubberBand(g);
|
PaintRubberBand(g);
|
||||||
PaintCursor(g, m_lanes);
|
PaintCursor(g, m_lanes);
|
||||||
|
PaintEdgeCursor(g, m_lanes);
|
||||||
PaintSignalInteraction(g);
|
PaintSignalInteraction(g);
|
||||||
PaintHint(g);
|
PaintHint(g);
|
||||||
|
|
||||||
@@ -3883,6 +4115,12 @@ namespace jdi
|
|||||||
float cursorX = legendBand.X;
|
float cursorX = legendBand.X;
|
||||||
const float maxX = lane.rect.X + lane.rect.Width - kLanePadding * m_scale;
|
const float maxX = lane.rect.X + lane.rect.Width - kLanePadding * m_scale;
|
||||||
|
|
||||||
|
// 网格编号徽标(图例左侧):与左侧信号面板每行末尾的 “GX” 徽标样式完全一致。
|
||||||
|
const float badgeWidth = kLaneBadgeWidth * m_scale;
|
||||||
|
const float badgeHeight = kLaneBadgeHeight * m_scale;
|
||||||
|
const gd::RectF badgeRect(cursorX, top + (height - badgeHeight) * 0.5f, badgeWidth, badgeHeight);
|
||||||
|
cursorX = badgeRect.X + badgeRect.Width + kLaneBadgeGap * m_scale;
|
||||||
|
|
||||||
bool anyVisible = false;
|
bool anyVisible = false;
|
||||||
int visibleCount = 0;
|
int visibleCount = 0;
|
||||||
int drawnCount = 0;
|
int drawnCount = 0;
|
||||||
@@ -3912,13 +4150,20 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
legendWidth += itemWidth;
|
legendWidth += itemWidth;
|
||||||
}
|
}
|
||||||
if (legendWidth > 0.0f)
|
|
||||||
{
|
{
|
||||||
UiFillRoundRect(g, gd::RectF(cursorX - 4.0f * m_scale, top - 2.0f * m_scale,
|
// 底板从编号徽标开始一直盖到最后一个图例条目(编号徽标一并被掠开底下的网格线)。
|
||||||
legendWidth + 6.0f * m_scale, height + 4.0f * m_scale),
|
const float plateRight = cursorX + legendWidth + (legendWidth > 0.0f ? 2.0f * m_scale : 0.0f);
|
||||||
|
UiFillRoundRect(g, gd::RectF(legendBand.X - 4.0f * m_scale, top - 2.0f * m_scale,
|
||||||
|
plateRight - legendBand.X + 4.0f * m_scale, height + 4.0f * m_scale),
|
||||||
3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xE8));
|
3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xE8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 编号徽标绘制在底板之上(与信号面板的 GX 徽标使用同一套配色与圆角)。
|
||||||
|
UiFillRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->cardBg);
|
||||||
|
UiDrawRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->border, 1.0f);
|
||||||
|
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
|
||||||
|
m_theme->text, TextAlign::CENTER);
|
||||||
|
|
||||||
// 记录本次绘制命中的条目矩形,供点击选中与双击改名使用。
|
// 记录本次绘制命中的条目矩形,供点击选中与双击改名使用。
|
||||||
for (auto iterator = m_legendHits.begin(); iterator != m_legendHits.end();)
|
for (auto iterator = m_legendHits.begin(); iterator != m_legendHits.end();)
|
||||||
{
|
{
|
||||||
@@ -4296,10 +4541,11 @@ namespace jdi
|
|||||||
if (SampleFileAt(*resolved.file, *resolved.data, otherTime, interpolate, otherValue))
|
if (SampleFileAt(*resolved.file, *resolved.data, otherTime, interpolate, otherValue))
|
||||||
{
|
{
|
||||||
const double delta = value - otherValue;
|
const double delta = value - otherValue;
|
||||||
// 增量 = 右侧光标取值 − 左侧光标取值,按差值显式给出正负号。
|
// 增量 = 右侧光标取值 − 左侧光标取值,包在圆括号里并按差值显式给出正负号。
|
||||||
text += L" , ";
|
text += L" (";
|
||||||
text += (delta < 0.0) ? L"-" : L"+";
|
text += (delta < 0.0) ? L"-" : L"+";
|
||||||
text += FormatValue(std::fabs(delta));
|
text += FormatValue(std::fabs(delta));
|
||||||
|
text += L")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4440,6 +4686,274 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gd::RectF PlotView::EdgePanelRect(int laneIndex) const
|
||||||
|
{
|
||||||
|
if (m_tab == nullptr || laneIndex < 0 ||
|
||||||
|
laneIndex >= static_cast<int>(m_tab->Lanes().size()) ||
|
||||||
|
laneIndex >= static_cast<int>(m_lanes.size()))
|
||||||
|
{
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
|
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||||
|
if (!lane.edgeCount || !lane.edgePanelOpen)
|
||||||
|
{
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
|
|
||||||
|
int rowCount = 0;
|
||||||
|
for (const SignalRef& ref : lane.signals)
|
||||||
|
{
|
||||||
|
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||||
|
if (resolved.data != nullptr && resolved.visible)
|
||||||
|
{
|
||||||
|
++rowCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rowCount <= 0)
|
||||||
|
{
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float width = kEdgePanelWidth * m_scale;
|
||||||
|
const float titleHeight = kEdgePanelTitleHeight * m_scale;
|
||||||
|
const float rowHeight = kEdgePanelRowHeight * m_scale;
|
||||||
|
const float pad = kEdgePanelPad * m_scale;
|
||||||
|
const float height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * 2.0f + pad;
|
||||||
|
|
||||||
|
// 首次使用(位置为负)时自动放到绘图区右侧,并各网格依次错开,避免完全重叠。
|
||||||
|
float left = (lane.edgePanelX >= 0.0)
|
||||||
|
? static_cast<float>(lane.edgePanelX)
|
||||||
|
: (m_plotRect.X + m_plotRect.Width - width - 16.0f * m_scale);
|
||||||
|
float top = (lane.edgePanelY >= 0.0)
|
||||||
|
? static_cast<float>(lane.edgePanelY)
|
||||||
|
: (m_plotRect.Y + 16.0f * m_scale +
|
||||||
|
static_cast<float>(laneIndex) * 28.0f * m_scale);
|
||||||
|
|
||||||
|
// 面板始终限制在绘图区内(用户明确要求"可以在右侧的整个绘制区域拖动")。
|
||||||
|
left = std::max(m_plotRect.X + 2.0f * m_scale,
|
||||||
|
std::min(left, m_plotRect.X + m_plotRect.Width - width - 2.0f * m_scale));
|
||||||
|
top = std::max(m_plotRect.Y + 2.0f * m_scale,
|
||||||
|
std::min(top, m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale));
|
||||||
|
|
||||||
|
return gd::RectF(left, top, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::RectF PlotView::EdgePanelCloseRect(int laneIndex) const
|
||||||
|
{
|
||||||
|
const gd::RectF rect = EdgePanelRect(laneIndex);
|
||||||
|
if (rect.Width <= 0.0f)
|
||||||
|
{
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
|
const float size = kEdgePanelTitleHeight * m_scale;
|
||||||
|
return gd::RectF(rect.X + rect.Width - size, rect.Y, size, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlotView::IsOnEdgeCursorLine(int x, int y, int& laneIndex) const
|
||||||
|
{
|
||||||
|
laneIndex = -1;
|
||||||
|
if (m_tab == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const std::vector<Lane>& tabLanes = m_tab->Lanes();
|
||||||
|
const float fx = static_cast<float>(x);
|
||||||
|
const float fy = static_cast<float>(y);
|
||||||
|
const float tolerance = 5.0f * m_scale;
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < m_lanes.size() && index < tabLanes.size(); ++index)
|
||||||
|
{
|
||||||
|
if (!tabLanes[index].edgeCount)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const LaneVisual& visual = m_lanes[index];
|
||||||
|
if (fx < visual.rect.X || fx > visual.rect.X + visual.rect.Width)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const float clipTop = std::max(visual.rect.Y, m_plotRect.Y);
|
||||||
|
const float clipBottom =
|
||||||
|
std::min(visual.rect.Y + visual.rect.Height, m_plotRect.Y + m_plotRect.Height);
|
||||||
|
if (clipBottom <= clipTop)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const float lineY = std::max(
|
||||||
|
clipTop, std::min(ValueToY(tabLanes[index].edgeValue, visual), clipBottom));
|
||||||
|
if (std::fabs(fy - lineY) <= tolerance)
|
||||||
|
{
|
||||||
|
laneIndex = static_cast<int>(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotView::PaintEdgeCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes)
|
||||||
|
{
|
||||||
|
if (m_tab == nullptr || m_dataSet == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Lane>& tabLanes = m_tab->Lanes();
|
||||||
|
const std::size_t count = std::min(lanes.size(), tabLanes.size());
|
||||||
|
const std::uint32_t color = CursorColor(1);
|
||||||
|
const bool dark = IsDarkColor(color);
|
||||||
|
|
||||||
|
// ---- 横向光标(线与两端把手 + 左侧幅值读数)----
|
||||||
|
for (std::size_t index = 0; index < count; ++index)
|
||||||
|
{
|
||||||
|
Lane& tabLane = tabLanes[index];
|
||||||
|
if (!tabLane.edgeCount)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const LaneVisual& visual = lanes[index];
|
||||||
|
const float clipTop = std::max(visual.rect.Y, m_plotRect.Y);
|
||||||
|
const float clipBottom =
|
||||||
|
std::min(visual.rect.Y + visual.rect.Height, m_plotRect.Y + m_plotRect.Height);
|
||||||
|
if (clipBottom <= clipTop)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首次绘制时把横向光标落在可见幅值范围的中点。
|
||||||
|
if (!tabLane.edgeValueValid)
|
||||||
|
{
|
||||||
|
tabLane.edgeValue = (visual.yMin + visual.yMax) * 0.5;
|
||||||
|
tabLane.edgeValueValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float lineY =
|
||||||
|
std::max(clipTop, std::min(ValueToY(tabLane.edgeValue, visual), clipBottom));
|
||||||
|
const float left = visual.rect.X;
|
||||||
|
const float right = visual.rect.X + visual.rect.Width;
|
||||||
|
|
||||||
|
// 横线与纵向光标同色同宽;两端各画一个尖端朝内的三角把手。
|
||||||
|
UiDrawLine(g, left, lineY, right, lineY, color, 1.4f * m_scale);
|
||||||
|
const float arm = 5.0f * m_scale;
|
||||||
|
gd::SolidBrush handle(ToGdiColor(color));
|
||||||
|
gd::PointF leftHandle[3];
|
||||||
|
leftHandle[0] = gd::PointF(left, lineY - arm);
|
||||||
|
leftHandle[1] = gd::PointF(left, lineY + arm);
|
||||||
|
leftHandle[2] = gd::PointF(left + arm * 1.4f, lineY);
|
||||||
|
g.FillPolygon(&handle, leftHandle, 3);
|
||||||
|
gd::PointF rightHandle[3];
|
||||||
|
rightHandle[0] = gd::PointF(right, lineY - arm);
|
||||||
|
rightHandle[1] = gd::PointF(right, lineY + arm);
|
||||||
|
rightHandle[2] = gd::PointF(right - arm * 1.4f, lineY);
|
||||||
|
g.FillPolygon(&handle, rightHandle, 3);
|
||||||
|
|
||||||
|
// 幅值读数:靠网格左侧、纵向居中于横线(与纵向光标的时间读数同款胶囊)。
|
||||||
|
const std::wstring text = FormatValue(tabLane.edgeValue);
|
||||||
|
const float textWidth = std::max(
|
||||||
|
56.0f * m_scale, UiMeasureTextWidth(m_fonts->monoSmall, text) + 16.0f * m_scale);
|
||||||
|
const float textHeight = 18.0f * m_scale;
|
||||||
|
const float labelLeft = left + 3.0f * m_scale;
|
||||||
|
const float labelTop =
|
||||||
|
std::max(clipTop + 1.0f * m_scale, std::min(lineY - textHeight * 0.5f,
|
||||||
|
clipBottom - textHeight - 1.0f * m_scale));
|
||||||
|
const gd::RectF labelRect(labelLeft, labelTop, textWidth, textHeight);
|
||||||
|
UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(color, 0xE0));
|
||||||
|
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
|
||||||
|
UiText(g, text, m_fonts->monoSmall, labelRect, dark ? 0xFFFDF6E3u : 0xFF073642u,
|
||||||
|
TextAlign::CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 统计浮动面板(最后绘制,覆盖在波形之上)----
|
||||||
|
for (std::size_t index = 0; index < count; ++index)
|
||||||
|
{
|
||||||
|
const Lane& tabLane = tabLanes[index];
|
||||||
|
if (!tabLane.edgeCount || !tabLane.edgePanelOpen)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const gd::RectF panel = EdgePanelRect(static_cast<int>(index));
|
||||||
|
if (panel.Width <= 0.0f || panel.Height <= 0.0f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float titleHeight = kEdgePanelTitleHeight * m_scale;
|
||||||
|
const float rowHeight = kEdgePanelRowHeight * m_scale;
|
||||||
|
const float pad = kEdgePanelPad * m_scale;
|
||||||
|
|
||||||
|
// 面板主体 + 标题栏。
|
||||||
|
UiFillRoundRect(g, gd::RectF(panel.X + 1.0f * m_scale, panel.Y + 2.0f * m_scale,
|
||||||
|
panel.Width, panel.Height),
|
||||||
|
5.0f * m_scale, 0x14000000u);
|
||||||
|
UiFillRoundRect(g, panel, 5.0f * m_scale, WithAlpha(m_theme->cardBg, 0xF4));
|
||||||
|
UiDrawRoundRect(g, panel, 5.0f * m_scale, m_theme->borderStrong, 1.0f);
|
||||||
|
UiFillRoundRect(g, gd::RectF(panel.X, panel.Y, panel.Width, titleHeight), 5.0f * m_scale,
|
||||||
|
WithAlpha(m_theme->accent, 0x2A));
|
||||||
|
|
||||||
|
// 标题:与左侧信号面板一致的 “GX” 徽标。
|
||||||
|
const float badgeWidth = kLaneBadgeWidth * m_scale;
|
||||||
|
const float badgeHeight = kLaneBadgeHeight * m_scale;
|
||||||
|
const gd::RectF badgeRect(panel.X + pad, panel.Y + (titleHeight - badgeHeight) * 0.5f,
|
||||||
|
badgeWidth, badgeHeight);
|
||||||
|
UiFillRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->cardBg);
|
||||||
|
UiDrawRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->border, 1.0f);
|
||||||
|
UiText(g, L"G" + std::to_wstring(index + 1), m_fonts->monoSmall, badgeRect, m_theme->text,
|
||||||
|
TextAlign::CENTER);
|
||||||
|
UiText(g, L"边沿计数", m_fonts->uiSmall,
|
||||||
|
gd::RectF(badgeRect.X + badgeRect.Width + 8.0f * m_scale, panel.Y,
|
||||||
|
panel.Width - badgeRect.Width - 8.0f * m_scale - titleHeight,
|
||||||
|
titleHeight),
|
||||||
|
m_theme->textStrong, TextAlign::LEFT);
|
||||||
|
|
||||||
|
// 关闭按钮 ✕。
|
||||||
|
const gd::RectF closeRect = EdgePanelCloseRect(static_cast<int>(index));
|
||||||
|
const bool closeHot =
|
||||||
|
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
|
||||||
|
UiPointInRect(closeRect, static_cast<float>(m_mouseX), static_cast<float>(m_mouseY));
|
||||||
|
if (closeHot)
|
||||||
|
{
|
||||||
|
UiFillRoundRect(g, gd::RectF(closeRect.X + 3.0f * m_scale, closeRect.Y + 3.0f * m_scale,
|
||||||
|
closeRect.Width - 6.0f * m_scale,
|
||||||
|
closeRect.Height - 6.0f * m_scale),
|
||||||
|
3.0f * m_scale, WithAlpha(m_theme->danger, 0x33));
|
||||||
|
}
|
||||||
|
const float crossPad = 8.0f * m_scale;
|
||||||
|
const std::uint32_t crossColor = closeHot ? m_theme->danger : m_theme->textMuted;
|
||||||
|
UiDrawLine(g, closeRect.X + crossPad, closeRect.Y + crossPad,
|
||||||
|
closeRect.X + closeRect.Width - crossPad, closeRect.Y + closeRect.Height - crossPad,
|
||||||
|
crossColor, 1.6f * m_scale);
|
||||||
|
UiDrawLine(g, closeRect.X + closeRect.Width - crossPad, closeRect.Y + crossPad,
|
||||||
|
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor,
|
||||||
|
1.6f * m_scale);
|
||||||
|
|
||||||
|
// 每个信号两行:名称(信号色)+ 上升沿/下降沿数量。
|
||||||
|
float rowY = panel.Y + titleHeight + pad;
|
||||||
|
for (const SignalRef& ref : tabLane.signals)
|
||||||
|
{
|
||||||
|
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||||
|
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||||
|
const float textWidth = panel.Width - pad * 2.0f;
|
||||||
|
UiText(g, UiEllipsize(g, resolved.data->Title(), m_fonts->uiSmall, textWidth),
|
||||||
|
m_fonts->uiSmall,
|
||||||
|
gd::RectF(panel.X + pad, rowY, textWidth, rowHeight), signalColor,
|
||||||
|
TextAlign::LEFT);
|
||||||
|
|
||||||
|
std::size_t rising = 0;
|
||||||
|
std::size_t falling = 0;
|
||||||
|
EdgeCountsFor(*resolved.file, *resolved.data, rising, falling);
|
||||||
|
const std::wstring counts = L"上升沿 " + std::to_wstring(rising) + L" 下降沿 " +
|
||||||
|
std::to_wstring(falling);
|
||||||
|
UiText(g, counts, m_fonts->monoSmall,
|
||||||
|
gd::RectF(panel.X + pad, rowY + rowHeight, textWidth, rowHeight),
|
||||||
|
m_theme->textMuted, TextAlign::LEFT);
|
||||||
|
rowY += rowHeight * 2.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PlotView::PaintEditField(gd::Graphics& g)
|
void PlotView::PaintEditField(gd::Graphics& g)
|
||||||
{
|
{
|
||||||
const gd::RectF fieldRect = EditFieldRect();
|
const gd::RectF fieldRect = EditFieldRect();
|
||||||
|
|||||||
@@ -49,6 +49,26 @@ namespace jdi
|
|||||||
void Toolbar::Layout(const gd::RectF& area)
|
void Toolbar::Layout(const gd::RectF& area)
|
||||||
{
|
{
|
||||||
m_area = area;
|
m_area = area;
|
||||||
|
|
||||||
|
// 重新排布会重建全部按钮:先记下当前各 toggle 的勾选状态,
|
||||||
|
// 否则每次因文本变化(如「光标x2」)触发的 Layout 会把按钮选中态全部清掉。
|
||||||
|
std::vector<std::pair<ToolbarAction, bool>> previousChecked;
|
||||||
|
for (const Button& button : m_buttons)
|
||||||
|
{
|
||||||
|
previousChecked.emplace_back(button.action, button.checked);
|
||||||
|
}
|
||||||
|
const auto inheritedChecked = [&previousChecked](ToolbarAction action)
|
||||||
|
{
|
||||||
|
for (const std::pair<ToolbarAction, bool>& entry : previousChecked)
|
||||||
|
{
|
||||||
|
if (entry.first == action)
|
||||||
|
{
|
||||||
|
return entry.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
m_buttons.clear();
|
m_buttons.clear();
|
||||||
|
|
||||||
if (m_fonts == nullptr || m_fonts->ui == nullptr)
|
if (m_fonts == nullptr || m_fonts->ui == nullptr)
|
||||||
@@ -94,6 +114,7 @@ namespace jdi
|
|||||||
button.primary = spec.primary;
|
button.primary = spec.primary;
|
||||||
button.needsDocument = spec.needsDocument;
|
button.needsDocument = spec.needsDocument;
|
||||||
button.toggle = spec.toggle;
|
button.toggle = spec.toggle;
|
||||||
|
button.checked = inheritedChecked(spec.action);
|
||||||
|
|
||||||
// 动态文本覆盖(例如「光标」按当前模式显示 光标 / 光标x1 / 光标x2)。
|
// 动态文本覆盖(例如「光标」按当前模式显示 光标 / 光标x1 / 光标x2)。
|
||||||
for (const std::pair<ToolbarAction, std::wstring>& entry : m_labelOverrides)
|
for (const std::pair<ToolbarAction, std::wstring>& entry : m_labelOverrides)
|
||||||
|
|||||||
@@ -347,6 +347,29 @@ namespace jdi
|
|||||||
* 用户手动调整幅值(拖拽平移、Y 轴选区放大、直接输入上下限)后会自动关闭。
|
* 用户手动调整幅值(拖拽平移、Y 轴选区放大、直接输入上下限)后会自动关闭。
|
||||||
*/
|
*/
|
||||||
bool fitYToSelection = false;
|
bool fitYToSelection = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 「实验性功能 → 边沿计数」开关。
|
||||||
|
*
|
||||||
|
* 开启后本网格显示一条横向光标(可上下拖动),并在浮动面板中列出该网格各信号的
|
||||||
|
* 上升沿 / 下降沿数量。与 `fitYToSelection` 一样属于网格自身状态,重建网格时按信号继承。
|
||||||
|
*/
|
||||||
|
bool edgeCount = false;
|
||||||
|
|
||||||
|
/** 横向光标所在的幅值(数据单位)。 */
|
||||||
|
double edgeValue = 0.0;
|
||||||
|
|
||||||
|
/** `edgeValue` 是否已经初始化(首次开启时取可见幅值范围的中点)。 */
|
||||||
|
bool edgeValueValid = false;
|
||||||
|
|
||||||
|
/** 边沿统计浮动面板是否展开(点击面板上的 ✕ 后置 false)。 */
|
||||||
|
bool edgePanelOpen = true;
|
||||||
|
|
||||||
|
/** 统计面板左上角位置(客户区像素;<0 表示自动定位到绘图区右侧)。 */
|
||||||
|
double edgePanelX = -1.0;
|
||||||
|
|
||||||
|
/** 统计面板左上角纵坐标(<0 表示自动定位)。 */
|
||||||
|
double edgePanelY = -1.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ namespace jdi
|
|||||||
SEPARATOR = 1, /**< 分隔线。 */
|
SEPARATOR = 1, /**< 分隔线。 */
|
||||||
HEADER = 2, /**< 分组标题(不可点击)。 */
|
HEADER = 2, /**< 分组标题(不可点击)。 */
|
||||||
SWATCHES = 3, /**< 颜色色块行。 */
|
SWATCHES = 3, /**< 颜色色块行。 */
|
||||||
EDIT = 4 /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
|
EDIT = 4, /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
|
||||||
|
SUBMENU = 5 /**< 带二级子菜单的项:鼠标悬停时向右展开。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,6 +55,7 @@ namespace jdi
|
|||||||
int checkedSwatch = -1; /**< 被选中的色块下标。 */
|
int checkedSwatch = -1; /**< 被选中的色块下标。 */
|
||||||
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
|
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
|
||||||
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
|
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
|
||||||
|
std::vector<MenuItem> submenu; /**< 二级子菜单项(kind == SUBMENU 时有效)。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -319,6 +321,31 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void CloseEditState();
|
void CloseEditState();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 展开指定项的二级子菜单(项不存在或非 SUBMENU 时等价于关闭)。
|
||||||
|
* @param [IN] parentIndex 父项下标。
|
||||||
|
*/
|
||||||
|
void BuildSubLayout(int parentIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 关闭二级子菜单并清空其状态。
|
||||||
|
*/
|
||||||
|
void CloseSubmenu();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 查询坐标命中的子菜单项下标。
|
||||||
|
* @param [IN] x 客户区横坐标。
|
||||||
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
* @return 子菜单项下标,未命中返回 -1。
|
||||||
|
*/
|
||||||
|
int SubItemAtPoint(int x, int y) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制二级子菜单(含投影与全部条目)。
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
*/
|
||||||
|
void PaintSubmenu(gd::Graphics& g);
|
||||||
|
|
||||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||||
@@ -343,6 +370,14 @@ namespace jdi
|
|||||||
gd::RectF m_buildBounds; /**< 最近一次排布使用的宿主矩形。 */
|
gd::RectF m_buildBounds; /**< 最近一次排布使用的宿主矩形。 */
|
||||||
int m_buildX = 0; /**< 最近一次排布使用的锚点横坐标。 */
|
int m_buildX = 0; /**< 最近一次排布使用的锚点横坐标。 */
|
||||||
int m_buildY = 0; /**< 最近一次排布使用的锚点纵坐标。 */
|
int m_buildY = 0; /**< 最近一次排布使用的锚点纵坐标。 */
|
||||||
|
|
||||||
|
bool m_subOpen = false; /**< 二级子菜单是否展开。 */
|
||||||
|
int m_subParentIndex = -1; /**< 展开子菜单的父项下标。 */
|
||||||
|
gd::RectF m_subRect; /**< 子菜单整体矩形。 */
|
||||||
|
std::vector<MenuItem> m_subItems; /**< 子菜单项集合。 */
|
||||||
|
std::vector<gd::RectF> m_subItemRects; /**< 子菜单各项矩形。 */
|
||||||
|
int m_subHoverIndex = -1; /**< 子菜单当前悬停项。 */
|
||||||
|
int m_subPressedIndex = -1; /**< 子菜单按下时的项下标。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
|
|||||||
@@ -111,26 +111,57 @@ namespace jdi
|
|||||||
void RebuildLanes(const std::vector<std::vector<bool>>& visible, std::vector<Lane>& lanes,
|
void RebuildLanes(const std::vector<std::vector<bool>>& visible, std::vector<Lane>& lanes,
|
||||||
LaneMode mode)
|
LaneMode mode)
|
||||||
{
|
{
|
||||||
// 先记下每个信号原来所在网格的「选区 Y 轴自适应」开关:
|
// 随网格携带的“网格自身状态”(按信号继承):重建网格时不能丢。
|
||||||
|
struct LaneCarry
|
||||||
|
{
|
||||||
|
bool fitYToSelection = false;
|
||||||
|
bool edgeCount = false;
|
||||||
|
double edgeValue = 0.0;
|
||||||
|
bool edgeValueValid = false;
|
||||||
|
bool edgePanelOpen = true;
|
||||||
|
double edgePanelX = -1.0;
|
||||||
|
double edgePanelY = -1.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 先记下每个信号原来所在网格的网格自身状态:
|
||||||
// 勾选可见性会重建网格,若不携带,用户刚打开的开关会被悄悄清掉。
|
// 勾选可见性会重建网格,若不携带,用户刚打开的开关会被悄悄清掉。
|
||||||
std::vector<std::pair<SignalRef, bool>> previousFit;
|
std::vector<std::pair<SignalRef, LaneCarry>> previousCarry;
|
||||||
for (const Lane& lane : lanes)
|
for (const Lane& lane : lanes)
|
||||||
{
|
{
|
||||||
|
LaneCarry carry;
|
||||||
|
carry.fitYToSelection = lane.fitYToSelection;
|
||||||
|
carry.edgeCount = lane.edgeCount;
|
||||||
|
carry.edgeValue = lane.edgeValue;
|
||||||
|
carry.edgeValueValid = lane.edgeValueValid;
|
||||||
|
carry.edgePanelOpen = lane.edgePanelOpen;
|
||||||
|
carry.edgePanelX = lane.edgePanelX;
|
||||||
|
carry.edgePanelY = lane.edgePanelY;
|
||||||
for (const SignalRef& ref : lane.signals)
|
for (const SignalRef& ref : lane.signals)
|
||||||
{
|
{
|
||||||
previousFit.emplace_back(ref, lane.fitYToSelection);
|
previousCarry.emplace_back(ref, carry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const auto inheritedFit = [&previousFit](const SignalRef& ref)
|
const auto inheritedCarry = [&previousCarry](const SignalRef& ref)
|
||||||
{
|
{
|
||||||
for (const std::pair<SignalRef, bool>& entry : previousFit)
|
for (const std::pair<SignalRef, LaneCarry>& entry : previousCarry)
|
||||||
{
|
{
|
||||||
if (entry.first == ref)
|
if (entry.first == ref)
|
||||||
{
|
{
|
||||||
return entry.second;
|
return entry.second;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return LaneCarry();
|
||||||
|
};
|
||||||
|
const auto applyCarry = [&inheritedCarry](Lane& lane, const SignalRef& ref)
|
||||||
|
{
|
||||||
|
const LaneCarry carry = inheritedCarry(ref);
|
||||||
|
lane.fitYToSelection = carry.fitYToSelection;
|
||||||
|
lane.edgeCount = carry.edgeCount;
|
||||||
|
lane.edgeValue = carry.edgeValue;
|
||||||
|
lane.edgeValueValid = carry.edgeValueValid;
|
||||||
|
lane.edgePanelOpen = carry.edgePanelOpen;
|
||||||
|
lane.edgePanelX = carry.edgePanelX;
|
||||||
|
lane.edgePanelY = carry.edgePanelY;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 收集全部可见信号引用。
|
// 收集全部可见信号引用。
|
||||||
@@ -173,7 +204,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
Lane lane;
|
Lane lane;
|
||||||
lane.autoRange = true;
|
lane.autoRange = true;
|
||||||
lane.fitYToSelection = inheritedFit(ref);
|
applyCarry(lane, ref);
|
||||||
lane.signals.push_back(ref);
|
lane.signals.push_back(ref);
|
||||||
lanes.push_back(std::move(lane));
|
lanes.push_back(std::move(lane));
|
||||||
}
|
}
|
||||||
@@ -198,7 +229,7 @@ namespace jdi
|
|||||||
lane.autoRange = previousAutoRange;
|
lane.autoRange = previousAutoRange;
|
||||||
lane.yMin = previousYMin;
|
lane.yMin = previousYMin;
|
||||||
lane.yMax = previousYMax;
|
lane.yMax = previousYMax;
|
||||||
lane.fitYToSelection = inheritedFit(visibleRefs.front());
|
applyCarry(lane, visibleRefs.front());
|
||||||
lane.signals = visibleRefs;
|
lane.signals = visibleRefs;
|
||||||
lanes.push_back(std::move(lane));
|
lanes.push_back(std::move(lane));
|
||||||
}
|
}
|
||||||
@@ -240,7 +271,7 @@ namespace jdi
|
|||||||
|
|
||||||
Lane lane;
|
Lane lane;
|
||||||
lane.autoRange = true;
|
lane.autoRange = true;
|
||||||
lane.fitYToSelection = inheritedFit(ref);
|
applyCarry(lane, ref);
|
||||||
lane.signals.push_back(ref);
|
lane.signals.push_back(ref);
|
||||||
lanes.push_back(std::move(lane));
|
lanes.push_back(std::move(lane));
|
||||||
}
|
}
|
||||||
|
|||||||
+398
-7
@@ -25,6 +25,8 @@ namespace jdi
|
|||||||
const float kEditFieldWidth = 88.0f; /**< EDIT 项输入框宽度。 */
|
const float kEditFieldWidth = 88.0f; /**< EDIT 项输入框宽度。 */
|
||||||
const float kEditFieldGap = 6.0f; /**< 说明文字与输入框之间的最小间隔。 */
|
const float kEditFieldGap = 6.0f; /**< 说明文字与输入框之间的最小间隔。 */
|
||||||
const float kEditFieldPadV = 5.0f; /**< 输入框上下内缩。 */
|
const float kEditFieldPadV = 5.0f; /**< 输入框上下内缩。 */
|
||||||
|
const float kSubArrowSize = 4.5f; /**< 子菜单指示箭头尺寸。 */
|
||||||
|
const float kSubOverlap = 2.0f; /**< 子菜单与主菜单重叠的宽度(避免中间露出缝隙)。 */
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupMenu::PopupMenu() = default;
|
PopupMenu::PopupMenu() = default;
|
||||||
@@ -165,6 +167,131 @@ namespace jdi
|
|||||||
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
|
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PopupMenu::CloseSubmenu()
|
||||||
|
{
|
||||||
|
m_subOpen = false;
|
||||||
|
m_subParentIndex = -1;
|
||||||
|
m_subRect = gd::RectF();
|
||||||
|
m_subItems.clear();
|
||||||
|
m_subItemRects.clear();
|
||||||
|
m_subHoverIndex = -1;
|
||||||
|
m_subPressedIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PopupMenu::BuildSubLayout(int parentIndex)
|
||||||
|
{
|
||||||
|
CloseSubmenu();
|
||||||
|
if (parentIndex < 0 || parentIndex >= static_cast<int>(m_items.size()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const MenuItem& parent = m_items[static_cast<std::size_t>(parentIndex)];
|
||||||
|
if (parent.kind != MenuItemKind::SUBMENU || parent.submenu.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_subItems = parent.submenu;
|
||||||
|
|
||||||
|
// 与主菜单一致:宽度取条目中的最大者并夹在上下限之间。
|
||||||
|
float contentWidth = kMinWidth * m_scale;
|
||||||
|
float totalHeight = kPadV * 2.0f * m_scale;
|
||||||
|
for (const MenuItem& item : m_subItems)
|
||||||
|
{
|
||||||
|
totalHeight += ItemHeight(item);
|
||||||
|
if (item.kind == MenuItemKind::SWATCHES)
|
||||||
|
{
|
||||||
|
const int count = std::max(0, std::min(item.swatchCount, 16));
|
||||||
|
const float width = (kPadH + kItemPad) * 2.0f * m_scale +
|
||||||
|
static_cast<float>(count) * kSwatchSize * m_scale +
|
||||||
|
static_cast<float>(std::max(0, count - 1)) * kSwatchGap * m_scale;
|
||||||
|
contentWidth = std::max(contentWidth, width);
|
||||||
|
}
|
||||||
|
else if (item.kind == MenuItemKind::HEADER)
|
||||||
|
{
|
||||||
|
const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale +
|
||||||
|
kMarkerGap * m_scale +
|
||||||
|
UiMeasureTextWidth(m_fonts->uiSmallBold, item.label);
|
||||||
|
contentWidth = std::max(contentWidth, width);
|
||||||
|
}
|
||||||
|
else if (item.kind == MenuItemKind::EDIT)
|
||||||
|
{
|
||||||
|
const float fieldWidth = NaturalEditFieldWidth(item);
|
||||||
|
const float width = (kPadH + kItemPad) * 2.0f * m_scale +
|
||||||
|
UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale +
|
||||||
|
fieldWidth;
|
||||||
|
contentWidth = std::max(contentWidth, width);
|
||||||
|
}
|
||||||
|
else if (item.kind == MenuItemKind::NORMAL || item.kind == MenuItemKind::SUBMENU)
|
||||||
|
{
|
||||||
|
const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale +
|
||||||
|
kMarkerGap * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, item.label);
|
||||||
|
contentWidth = std::max(contentWidth, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentWidth = std::min(contentWidth, kMaxWidth * m_scale);
|
||||||
|
|
||||||
|
const float width = contentWidth;
|
||||||
|
const float height = totalHeight;
|
||||||
|
const gd::RectF& parentRect = m_itemRects[static_cast<std::size_t>(parentIndex)];
|
||||||
|
const gd::RectF& bounds = m_buildBounds;
|
||||||
|
|
||||||
|
// 默认贴主菜单右缘并向上与父项顶端对齐;右侧空间不足时翻到左侧。
|
||||||
|
float left = m_rect.X + m_rect.Width - kSubOverlap * m_scale;
|
||||||
|
if (left + width > bounds.X + bounds.Width - 2.0f)
|
||||||
|
{
|
||||||
|
left = m_rect.X - width + kSubOverlap * m_scale;
|
||||||
|
}
|
||||||
|
left = std::max(bounds.X + 2.0f, std::min(left, bounds.X + bounds.Width - width - 2.0f));
|
||||||
|
|
||||||
|
float top = parentRect.Y - kPadV * m_scale;
|
||||||
|
top = std::max(bounds.Y + 2.0f, std::min(top, bounds.Y + bounds.Height - height - 2.0f));
|
||||||
|
|
||||||
|
m_subRect = gd::RectF(left, top, width, height);
|
||||||
|
m_subItemRects.assign(m_subItems.size(), gd::RectF());
|
||||||
|
|
||||||
|
float cursorY = top + kPadV * m_scale;
|
||||||
|
for (std::size_t index = 0; index < m_subItems.size(); ++index)
|
||||||
|
{
|
||||||
|
const float itemHeight = ItemHeight(m_subItems[index]);
|
||||||
|
m_subItemRects[index] = gd::RectF(left + kPadH * m_scale, cursorY,
|
||||||
|
std::max(0.0f, width - kPadH * 2.0f * m_scale), itemHeight);
|
||||||
|
cursorY += itemHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_subParentIndex = parentIndex;
|
||||||
|
m_subOpen = true;
|
||||||
|
m_subHoverIndex = -1;
|
||||||
|
m_subPressedIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PopupMenu::SubItemAtPoint(int x, int y) const
|
||||||
|
{
|
||||||
|
if (!m_subOpen)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const float fx = static_cast<float>(x);
|
||||||
|
const float fy = static_cast<float>(y);
|
||||||
|
for (std::size_t index = 0; index < m_subItems.size(); ++index)
|
||||||
|
{
|
||||||
|
const MenuItem& item = m_subItems[index];
|
||||||
|
if (item.kind == MenuItemKind::SEPARATOR || item.kind == MenuItemKind::HEADER)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!item.enabled)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (UiPointInRect(m_subItemRects[index], fx, fy))
|
||||||
|
{
|
||||||
|
return static_cast<int>(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
gd::RectF PopupMenu::EditFieldRect(int index) const
|
gd::RectF PopupMenu::EditFieldRect(int index) const
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= static_cast<int>(m_items.size()))
|
if (index < 0 || index >= static_cast<int>(m_items.size()))
|
||||||
@@ -264,7 +391,13 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const int subParent = m_subParentIndex;
|
||||||
BuildLayout(m_buildBounds, m_buildX, m_buildY);
|
BuildLayout(m_buildBounds, m_buildX, m_buildY);
|
||||||
|
// 主菜单重新排布后子菜单位置会失效:若之前是展开的则按新几何重建。
|
||||||
|
if (subParent >= 0)
|
||||||
|
{
|
||||||
|
BuildSubLayout(subParent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PopupMenu::UpdateItemValue(int id, const std::wstring& value)
|
bool PopupMenu::UpdateItemValue(int id, const std::wstring& value)
|
||||||
@@ -392,6 +525,7 @@ namespace jdi
|
|||||||
m_buildX = x;
|
m_buildX = x;
|
||||||
m_buildY = y;
|
m_buildY = y;
|
||||||
|
|
||||||
|
CloseSubmenu();
|
||||||
CloseEditState();
|
CloseEditState();
|
||||||
BuildLayout(bounds, x, y);
|
BuildLayout(bounds, x, y);
|
||||||
}
|
}
|
||||||
@@ -407,6 +541,7 @@ namespace jdi
|
|||||||
m_hoverSwatch = -1;
|
m_hoverSwatch = -1;
|
||||||
m_pressedIndex = -1;
|
m_pressedIndex = -1;
|
||||||
m_pressedSwatch = -1;
|
m_pressedSwatch = -1;
|
||||||
|
CloseSubmenu();
|
||||||
CloseEditState();
|
CloseEditState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,7 +560,14 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return UiPointInRect(m_rect, static_cast<float>(x), static_cast<float>(y));
|
const float fx = static_cast<float>(x);
|
||||||
|
const float fy = static_cast<float>(y);
|
||||||
|
if (UiPointInRect(m_rect, fx, fy))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 展开的二级子菜单同样属于菜单范围(否则在子菜单内点击会被当成“点菜单外”)。
|
||||||
|
return m_subOpen && UiPointInRect(m_subRect, fx, fy);
|
||||||
}
|
}
|
||||||
|
|
||||||
int PopupMenu::ItemAtPoint(int x, int y, int& swatchIndex) const
|
int PopupMenu::ItemAtPoint(int x, int y, int& swatchIndex) const
|
||||||
@@ -545,16 +687,47 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
// 鼠标在展开的子菜单内:只更新子菜单的悬停项(主菜单父项保持高亮)。
|
||||||
|
if (m_subOpen && UiPointInRect(m_subRect, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
const int sub = SubItemAtPoint(x, y);
|
||||||
|
if (sub != m_subHoverIndex)
|
||||||
|
{
|
||||||
|
m_subHoverIndex = sub;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
int swatchIndex = -1;
|
int swatchIndex = -1;
|
||||||
const int index = ItemAtPoint(x, y, swatchIndex);
|
const int index = ItemAtPoint(x, y, swatchIndex);
|
||||||
|
|
||||||
if (index == m_hoverIndex && swatchIndex == m_hoverSwatch)
|
if (index != m_hoverIndex || swatchIndex != m_hoverSwatch)
|
||||||
{
|
{
|
||||||
return false;
|
|
||||||
}
|
|
||||||
m_hoverIndex = index;
|
m_hoverIndex = index;
|
||||||
m_hoverSwatch = swatchIndex;
|
m_hoverSwatch = swatchIndex;
|
||||||
return true;
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 悬停到带子菜单的项 → 向右展开;移开到其它条目 → 收起。
|
||||||
|
const bool onSubmenuParent =
|
||||||
|
(index >= 0) && (m_items[static_cast<std::size_t>(index)].kind == MenuItemKind::SUBMENU);
|
||||||
|
if (onSubmenuParent)
|
||||||
|
{
|
||||||
|
if (!m_subOpen || m_subParentIndex != index)
|
||||||
|
{
|
||||||
|
BuildSubLayout(index);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_subOpen)
|
||||||
|
{
|
||||||
|
CloseSubmenu();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PopupMenu::OnLButtonDown(int x, int y, MenuResult& result)
|
bool PopupMenu::OnLButtonDown(int x, int y, MenuResult& result)
|
||||||
@@ -573,6 +746,15 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 子菜单内的按下:记录待确认项,待抬起时提交。
|
||||||
|
if (m_subOpen && UiPointInRect(m_subRect, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
m_subPressedIndex = SubItemAtPoint(x, y);
|
||||||
|
m_pressedIndex = -1;
|
||||||
|
m_pressedSwatch = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 已经处于编辑状态时:点击当前输入框 → 重新定位插入符;
|
// 已经处于编辑状态时:点击当前输入框 → 重新定位插入符;
|
||||||
// 点击其它位置 → 先提交本次编辑(菜单不关闭),再按新位置继续处理。
|
// 点击其它位置 → 先提交本次编辑(菜单不关闭),再按新位置继续处理。
|
||||||
if (m_editIndex >= 0)
|
if (m_editIndex >= 0)
|
||||||
@@ -602,6 +784,28 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 子菜单内的抬起:按下与抬起命中同一项才提交(菜单随之关闭)。
|
||||||
|
if (m_subOpen && m_subPressedIndex >= 0)
|
||||||
|
{
|
||||||
|
const int subPressed = m_subPressedIndex;
|
||||||
|
m_subPressedIndex = -1;
|
||||||
|
if (UiPointInRect(m_subRect, static_cast<float>(x), static_cast<float>(y)) &&
|
||||||
|
SubItemAtPoint(x, y) == subPressed)
|
||||||
|
{
|
||||||
|
const MenuItem& item = m_subItems[static_cast<std::size_t>(subPressed)];
|
||||||
|
if (item.kind == MenuItemKind::NORMAL || item.kind == MenuItemKind::SWATCHES)
|
||||||
|
{
|
||||||
|
result.valid = true;
|
||||||
|
result.scope = m_scope;
|
||||||
|
result.id = item.id;
|
||||||
|
result.swatchIndex = -1;
|
||||||
|
result.itemRect = m_subItemRects[static_cast<std::size_t>(subPressed)];
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 编辑态下松开:先结束拖拽框选,不当作普通项点击。
|
// 编辑态下松开:先结束拖拽框选,不当作普通项点击。
|
||||||
if (m_editIndex >= 0)
|
if (m_editIndex >= 0)
|
||||||
{
|
{
|
||||||
@@ -633,6 +837,16 @@ namespace jdi
|
|||||||
|
|
||||||
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
||||||
|
|
||||||
|
// 子菜单父项:单击不下发命令,只保证子菜单处于展开状态。
|
||||||
|
if (item.kind == MenuItemKind::SUBMENU)
|
||||||
|
{
|
||||||
|
if (!m_subOpen || m_subParentIndex != index)
|
||||||
|
{
|
||||||
|
BuildSubLayout(index);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 编辑项:单击进入菜单内联编辑,菜单保持打开。
|
// 编辑项:单击进入菜单内联编辑,菜单保持打开。
|
||||||
if (item.kind == MenuItemKind::EDIT)
|
if (item.kind == MenuItemKind::EDIT)
|
||||||
{
|
{
|
||||||
@@ -670,8 +884,7 @@ namespace jdi
|
|||||||
|
|
||||||
// 编辑态下按键全部交给输入框(回车提交、Esc 取消本次编辑但菜单保持打开)。
|
// 编辑态下按键全部交给输入框(回车提交、Esc 取消本次编辑但菜单保持打开)。
|
||||||
if (m_editIndex >= 0)
|
if (m_editIndex >= 0)
|
||||||
{
|
{ const bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
|
||||||
const bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
|
|
||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
case VK_ESCAPE:
|
case VK_ESCAPE:
|
||||||
@@ -707,16 +920,67 @@ namespace jdi
|
|||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
case VK_ESCAPE:
|
case VK_ESCAPE:
|
||||||
|
// Esc 先收起展开的二级子菜单,再次按下才关闭整个菜单。
|
||||||
|
if (m_subOpen)
|
||||||
|
{
|
||||||
|
CloseSubmenu();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
Close();
|
Close();
|
||||||
return true;
|
return true;
|
||||||
|
case VK_LEFT:
|
||||||
|
if (m_subOpen)
|
||||||
|
{
|
||||||
|
CloseSubmenu();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VK_RIGHT:
|
||||||
|
{
|
||||||
|
if (m_hoverIndex >= 0 &&
|
||||||
|
m_items[static_cast<std::size_t>(m_hoverIndex)].kind == MenuItemKind::SUBMENU)
|
||||||
|
{
|
||||||
|
BuildSubLayout(m_hoverIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case VK_DOWN:
|
case VK_DOWN:
|
||||||
|
if (m_subOpen && m_subItems.size() > 0)
|
||||||
|
{
|
||||||
|
const int count = static_cast<int>(m_subItems.size());
|
||||||
|
m_subHoverIndex = (m_subHoverIndex + 1 + count) % count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
MoveHover(1);
|
MoveHover(1);
|
||||||
return true;
|
return true;
|
||||||
case VK_UP:
|
case VK_UP:
|
||||||
|
if (m_subOpen && m_subItems.size() > 0)
|
||||||
|
{
|
||||||
|
const int count = static_cast<int>(m_subItems.size());
|
||||||
|
m_subHoverIndex = (m_subHoverIndex - 1 + count) % count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
MoveHover(-1);
|
MoveHover(-1);
|
||||||
return true;
|
return true;
|
||||||
case VK_RETURN:
|
case VK_RETURN:
|
||||||
{
|
{
|
||||||
|
// 子菜单展开且已有悬停项时,回车提交该项。
|
||||||
|
if (m_subOpen && m_subHoverIndex >= 0 &&
|
||||||
|
m_subHoverIndex < static_cast<int>(m_subItems.size()))
|
||||||
|
{
|
||||||
|
const MenuItem& item = m_subItems[static_cast<std::size_t>(m_subHoverIndex)];
|
||||||
|
if (item.kind == MenuItemKind::NORMAL && item.enabled)
|
||||||
|
{
|
||||||
|
result.valid = true;
|
||||||
|
result.scope = m_scope;
|
||||||
|
result.id = item.id;
|
||||||
|
result.swatchIndex = -1;
|
||||||
|
result.itemRect = m_subItemRects[static_cast<std::size_t>(m_subHoverIndex)];
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (CommitHover(result))
|
if (CommitHover(result))
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
@@ -864,6 +1128,133 @@ namespace jdi
|
|||||||
cy - 4.0f * m_scale, markerColor, 1.8f * m_scale);
|
cy - 4.0f * m_scale, markerColor, 1.8f * m_scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 带子菜单的项:在右侧标记列画一个指向右的三角,提示可以继续展开。
|
||||||
|
if (item.kind == MenuItemKind::SUBMENU)
|
||||||
|
{
|
||||||
|
const float markerRight = rect.X + rect.Width - kItemPad * m_scale;
|
||||||
|
const float cx = markerRight - kMarkerColumn * m_scale * 0.5f;
|
||||||
|
const float cy = rect.Y + rect.Height * 0.5f;
|
||||||
|
const float size = kSubArrowSize * m_scale;
|
||||||
|
gd::PointF arrow[3];
|
||||||
|
arrow[0] = gd::PointF(cx - size * 0.5f, cy - size);
|
||||||
|
arrow[1] = gd::PointF(cx - size * 0.5f, cy + size);
|
||||||
|
arrow[2] = gd::PointF(cx + size * 0.7f, cy);
|
||||||
|
gd::SolidBrush brush(ToGdiColor(textColor));
|
||||||
|
g.FillPolygon(&brush, arrow, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float textLeft = rect.X + kItemPad * m_scale;
|
||||||
|
const float textWidth =
|
||||||
|
rect.Width - 2.0f * kItemPad * m_scale - kMarkerColumn * m_scale - kMarkerGap * m_scale;
|
||||||
|
UiText(g, UiEllipsize(g, item.label, m_fonts->uiSmall, textWidth), m_fonts->uiSmall,
|
||||||
|
gd::RectF(textLeft, rect.Y, std::max(8.0f, textWidth), rect.Height), textColor,
|
||||||
|
TextAlign::LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二级子菜单最后绘制,保证覆盖在主菜单之上。
|
||||||
|
PaintSubmenu(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PopupMenu::PaintSubmenu(gd::Graphics& g)
|
||||||
|
{
|
||||||
|
if (!m_subOpen || m_subRect.Width <= 0.0f || m_subRect.Height <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UiFillRoundRect(g, gd::RectF(m_subRect.X + 1.0f * m_scale, m_subRect.Y + 2.0f * m_scale,
|
||||||
|
m_subRect.Width, m_subRect.Height),
|
||||||
|
kCorner * m_scale, 0x14000000u);
|
||||||
|
UiFillRoundRect(g, gd::RectF(m_subRect.X + 2.0f * m_scale, m_subRect.Y + 4.0f * m_scale,
|
||||||
|
m_subRect.Width, m_subRect.Height),
|
||||||
|
kCorner * m_scale, 0x10000000u);
|
||||||
|
|
||||||
|
UiFillRoundRect(g, m_subRect, kCorner * m_scale, m_theme->cardBg);
|
||||||
|
UiDrawRoundRect(g, m_subRect, kCorner * m_scale, m_theme->borderStrong, 1.0f);
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < m_subItems.size(); ++index)
|
||||||
|
{
|
||||||
|
const MenuItem& item = m_subItems[index];
|
||||||
|
const gd::RectF& rect = m_subItemRects[index];
|
||||||
|
|
||||||
|
if (item.kind == MenuItemKind::SEPARATOR)
|
||||||
|
{
|
||||||
|
const float lineY = rect.Y + rect.Height * 0.5f;
|
||||||
|
UiDrawLine(g, rect.X + kItemPad * m_scale, lineY,
|
||||||
|
rect.X + rect.Width - kItemPad * m_scale, lineY, m_theme->border, 1.0f);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind == MenuItemKind::HEADER)
|
||||||
|
{
|
||||||
|
UiText(g, item.label, m_fonts->uiSmallBold,
|
||||||
|
gd::RectF(rect.X + kItemPad * m_scale, rect.Y, rect.Width, rect.Height),
|
||||||
|
m_theme->textMuted, TextAlign::LEFT);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool hovered = (static_cast<int>(index) == m_subHoverIndex) && item.enabled;
|
||||||
|
|
||||||
|
if (item.kind == MenuItemKind::EDIT)
|
||||||
|
{
|
||||||
|
// 子菜单中的编辑项:与主菜单一样就地编辑。
|
||||||
|
if (hovered && static_cast<int>(index) != m_editIndex)
|
||||||
|
{
|
||||||
|
UiFillRoundRect(g, rect, 4.0f * m_scale, WithAlpha(m_theme->accent, 0x22));
|
||||||
|
}
|
||||||
|
const std::uint32_t labelColor = item.enabled ? m_theme->text : m_theme->textMuted;
|
||||||
|
const float labelLeft = rect.X + kItemPad * m_scale;
|
||||||
|
const gd::RectF fieldRect = EditFieldRect(static_cast<int>(index));
|
||||||
|
const float labelWidth = std::max(8.0f, fieldRect.X - labelLeft - kEditFieldGap * m_scale);
|
||||||
|
UiText(g, UiEllipsize(g, item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
|
||||||
|
gd::RectF(labelLeft, rect.Y, labelWidth, rect.Height), labelColor, TextAlign::LEFT);
|
||||||
|
UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->pressed);
|
||||||
|
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->border, 1.0f);
|
||||||
|
const std::wstring text =
|
||||||
|
UiEllipsize(g, item.value, m_fonts->uiSmall, fieldRect.Width - 12.0f * m_scale);
|
||||||
|
UiText(g, text, m_fonts->uiSmall,
|
||||||
|
gd::RectF(fieldRect.X + 6.0f * m_scale, fieldRect.Y,
|
||||||
|
std::max(8.0f, fieldRect.Width - 12.0f * m_scale), fieldRect.Height),
|
||||||
|
item.enabled ? m_theme->textStrong : m_theme->textMuted, TextAlign::LEFT);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind == MenuItemKind::SWATCHES)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t background = 0;
|
||||||
|
if (hovered)
|
||||||
|
{
|
||||||
|
background = m_theme->accent;
|
||||||
|
}
|
||||||
|
else if (static_cast<int>(index) == m_subPressedIndex)
|
||||||
|
{
|
||||||
|
background = m_theme->pressed;
|
||||||
|
}
|
||||||
|
if (background != 0)
|
||||||
|
{
|
||||||
|
UiFillRoundRect(g, rect, 4.0f * m_scale, background);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::uint32_t textColor = !item.enabled ? m_theme->textMuted
|
||||||
|
: (hovered ? m_theme->textOnAccent
|
||||||
|
: m_theme->text);
|
||||||
|
|
||||||
|
// 选中标记(对勾):与主菜单一致画在右侧标记列内。
|
||||||
|
if (item.checked)
|
||||||
|
{
|
||||||
|
const float markerRight = rect.X + rect.Width - kItemPad * m_scale;
|
||||||
|
const float cx = markerRight - kMarkerColumn * m_scale * 0.5f;
|
||||||
|
const float cy = rect.Y + rect.Height * 0.5f;
|
||||||
|
const std::uint32_t markerColor = hovered ? m_theme->textOnAccent : m_theme->accent;
|
||||||
|
UiDrawLine(g, cx - 4.0f * m_scale, cy, cx - 1.0f * m_scale, cy + 3.0f * m_scale,
|
||||||
|
markerColor, 1.8f * m_scale);
|
||||||
|
UiDrawLine(g, cx - 1.0f * m_scale, cy + 3.0f * m_scale, cx + 4.5f * m_scale,
|
||||||
|
cy - 4.0f * m_scale, markerColor, 1.8f * m_scale);
|
||||||
|
}
|
||||||
|
|
||||||
const float textLeft = rect.X + kItemPad * m_scale;
|
const float textLeft = rect.X + kItemPad * m_scale;
|
||||||
const float textWidth =
|
const float textWidth =
|
||||||
rect.Width - 2.0f * kItemPad * m_scale - kMarkerColumn * m_scale - kMarkerGap * m_scale;
|
rect.Width - 2.0f * kItemPad * m_scale - kMarkerColumn * m_scale - kMarkerGap * m_scale;
|
||||||
|
|||||||
Reference in new issue
Block a user