- 每个网格图例左侧新增当前网格的 “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 无警告无错误。
- 修复过程中发现:静态场景缓存命中路径(贴图快路径)与整幅重绘路径是两处独立的
叠加层绘制入口,新增的随鼠标变化内容必须同时加在两边。
389 lines
9.9 KiB
C++
389 lines
9.9 KiB
C++
#include "Toolbar.hpp"
|
|
|
|
#include <cmath>
|
|
|
|
#include "Utf8Util.hpp"
|
|
|
|
namespace jdi
|
|
{
|
|
|
|
namespace
|
|
{
|
|
const float kButtonHeight = 30.0f; /**< 按钮高度(逻辑像素)。 */
|
|
const float kButtonGap = 6.0f; /**< 按钮间距。 */
|
|
const float kButtonPadding = 14.0f; /**< 按钮左右内边距。 */
|
|
const float kGroupGap = 18.0f; /**< 分组间距。 */
|
|
const float kButtonRadius = 5.0f; /**< 按钮圆角半径。 */
|
|
const float kSidePadding = 12.0f; /**< 工具栏左右留白。 */
|
|
const float kTopPadding = 8.0f; /**< 工具栏上下留白。 */
|
|
}
|
|
|
|
Toolbar::Toolbar() = default;
|
|
|
|
void Toolbar::Setup(const UiFonts* fonts, const Theme* theme)
|
|
{
|
|
m_fonts = fonts;
|
|
m_theme = theme;
|
|
}
|
|
|
|
void Toolbar::SetScale(float scale)
|
|
{
|
|
m_scale = (scale > 0.1f) ? scale : 1.0f;
|
|
}
|
|
|
|
void Toolbar::SetDocumentLoaded(bool hasDocument)
|
|
{
|
|
m_documentLoaded = hasDocument;
|
|
}
|
|
|
|
float Toolbar::DesiredHeight() const
|
|
{
|
|
return (kButtonHeight + kTopPadding * 2.0f) * m_scale;
|
|
}
|
|
|
|
bool Toolbar::IsEnabled(const Button& button) const
|
|
{
|
|
return button.needsDocument ? m_documentLoaded : true;
|
|
}
|
|
|
|
void Toolbar::Layout(const gd::RectF& 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();
|
|
|
|
if (m_fonts == nullptr || m_fonts->ui == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
struct Spec
|
|
{
|
|
ToolbarAction action;
|
|
const wchar_t* label;
|
|
bool primary;
|
|
bool needsDocument;
|
|
bool toggle;
|
|
};
|
|
|
|
const Spec specs[] = {
|
|
{ToolbarAction::OPEN_FILE, L"导入文件", true, false, false},
|
|
{ToolbarAction::FIT_ALL, L"适应视图", false, true, false},
|
|
{ToolbarAction::AUTO_FIT_Y, L"Y 轴自适应", false, true, true},
|
|
{ToolbarAction::ZOOM_SELECT, L"选区放大", false, true, true},
|
|
{ToolbarAction::CURSOR, L"光标", false, true, true},
|
|
{ToolbarAction::MERGE_LANES, L"合并网格", false, true, false},
|
|
{ToolbarAction::SPLIT_LANES, L"拆分网格", false, true, false},
|
|
{ToolbarAction::CLEAR_ALL, L"清空网格", false, true, false},
|
|
};
|
|
const std::size_t specCount = sizeof(specs) / sizeof(specs[0]);
|
|
|
|
// 按钮宽度按实际文本像素宽 + 固定内边距计算,避免中文标签被裁切。
|
|
const float top = area.Y + kTopPadding * m_scale;
|
|
const float height = kButtonHeight * m_scale;
|
|
const float horizontalPadding = kButtonPadding * 2.0f * m_scale;
|
|
|
|
// 先建立全部按钮并测量宽度,再决定“导入文件”固定在左、其余按钮整体靠右。
|
|
m_buttons.clear();
|
|
m_buttons.reserve(specCount);
|
|
|
|
for (const Spec& spec : specs)
|
|
{
|
|
Button button;
|
|
button.action = spec.action;
|
|
button.label = spec.label;
|
|
button.primary = spec.primary;
|
|
button.needsDocument = spec.needsDocument;
|
|
button.toggle = spec.toggle;
|
|
button.checked = inheritedChecked(spec.action);
|
|
|
|
// 动态文本覆盖(例如「光标」按当前模式显示 光标 / 光标x1 / 光标x2)。
|
|
for (const std::pair<ToolbarAction, std::wstring>& entry : m_labelOverrides)
|
|
{
|
|
if (entry.first == spec.action)
|
|
{
|
|
button.label = entry.second;
|
|
break;
|
|
}
|
|
}
|
|
|
|
button.width = horizontalPadding + UiMeasureTextWidth(m_fonts->ui, button.label);
|
|
m_buttons.push_back(std::move(button));
|
|
}
|
|
|
|
// 首按钮(导入文件)靠左;其余按钮作为整体靠右,并留出一组额外间距。
|
|
const float leftX = area.X + kSidePadding * m_scale;
|
|
float groupWidth = 0.0f;
|
|
for (std::size_t index = 1; index < m_buttons.size(); ++index)
|
|
{
|
|
groupWidth += m_buttons[index].width;
|
|
if (index + 1 < m_buttons.size())
|
|
{
|
|
groupWidth += kButtonGap * m_scale;
|
|
}
|
|
}
|
|
|
|
float cursorX = leftX;
|
|
m_buttons[0].rect = gd::RectF(cursorX, top, m_buttons[0].width, height);
|
|
|
|
const float rightEdge = area.X + area.Width - kSidePadding * m_scale;
|
|
cursorX = std::max(leftX + m_buttons[0].width + kGroupGap * m_scale, rightEdge - groupWidth);
|
|
for (std::size_t index = 1; index < m_buttons.size(); ++index)
|
|
{
|
|
m_buttons[index].rect = gd::RectF(cursorX, top, m_buttons[index].width, height);
|
|
cursorX += m_buttons[index].width + kButtonGap * m_scale;
|
|
}
|
|
}
|
|
|
|
void Toolbar::SetToggleState(ToolbarAction action, bool checked)
|
|
{
|
|
for (Button& button : m_buttons)
|
|
{
|
|
if (button.action == action && button.toggle)
|
|
{
|
|
button.checked = checked;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Toolbar::SetActionLabel(ToolbarAction action, const std::wstring& label)
|
|
{
|
|
for (std::pair<ToolbarAction, std::wstring>& entry : m_labelOverrides)
|
|
{
|
|
if (entry.first == action)
|
|
{
|
|
if (entry.second == label)
|
|
{
|
|
return false;
|
|
}
|
|
entry.second = label;
|
|
// 已排布过时同步更新按钮文本,宿主随后会调用 Layout() 重新计算宽度。
|
|
for (Button& button : m_buttons)
|
|
{
|
|
if (button.action == action)
|
|
{
|
|
button.label = label;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
m_labelOverrides.emplace_back(action, label);
|
|
for (Button& button : m_buttons)
|
|
{
|
|
if (button.action == action)
|
|
{
|
|
button.label = label;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::wstring Toolbar::ActionLabel(ToolbarAction action) const
|
|
{
|
|
for (const Button& button : m_buttons)
|
|
{
|
|
if (button.action == action)
|
|
{
|
|
return button.label;
|
|
}
|
|
}
|
|
return std::wstring();
|
|
}
|
|
|
|
void Toolbar::Paint(gd::Graphics& g)
|
|
{
|
|
if (m_theme == nullptr || m_fonts == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UiFillRect(g, m_area, m_theme->toolbarBg);
|
|
|
|
for (const Button& button : m_buttons)
|
|
{
|
|
const bool enabled = IsEnabled(button);
|
|
std::uint32_t background = 0;
|
|
std::uint32_t border = 0;
|
|
std::uint32_t textColor = 0;
|
|
|
|
if (button.primary)
|
|
{
|
|
background = enabled ? m_theme->accent : MixColor(m_theme->accent, m_theme->toolbarBg, 0.6);
|
|
if (button.pressed)
|
|
{
|
|
background = MixColor(m_theme->accent, 0xFF000000u, 0.12);
|
|
}
|
|
else if (enabled && button.hover)
|
|
{
|
|
background = MixColor(m_theme->accent, 0xFFFFFFFFu, 0.12);
|
|
}
|
|
border = background;
|
|
textColor = m_theme->textOnAccent;
|
|
}
|
|
else
|
|
{
|
|
// 可切换按钮在选中时用强调色浅底 + 强调描边,与普通按钮区分。
|
|
background = (button.toggle && button.checked) ? m_theme->accentSoft : m_theme->toolbarBg;
|
|
if (button.pressed)
|
|
{
|
|
background = m_theme->pressed;
|
|
}
|
|
else if (enabled && button.hover)
|
|
{
|
|
background = (button.toggle && button.checked) ? MixColor(m_theme->accentSoft, m_theme->accent,
|
|
0.16)
|
|
: m_theme->hover;
|
|
}
|
|
if (!enabled)
|
|
{
|
|
background = m_theme->toolbarBg;
|
|
}
|
|
|
|
if (button.toggle && button.checked && enabled)
|
|
{
|
|
border = m_theme->accent;
|
|
}
|
|
else
|
|
{
|
|
border = MixColor(m_theme->border, m_theme->toolbarBg, enabled ? 0.0 : 0.5);
|
|
}
|
|
textColor = enabled ? ((button.toggle && button.checked) ? m_theme->textStrong : m_theme->text)
|
|
: m_theme->textMuted;
|
|
}
|
|
|
|
if (background != m_theme->toolbarBg)
|
|
{
|
|
UiFillRoundRect(g, button.rect, kButtonRadius * m_scale, background);
|
|
}
|
|
if (!button.primary)
|
|
{
|
|
UiDrawRoundRect(g, button.rect, kButtonRadius * m_scale, border, 1.0f);
|
|
}
|
|
|
|
UiText(g, button.label, m_fonts->ui, button.rect, textColor, TextAlign::CENTER);
|
|
}
|
|
|
|
// 底部分隔线。
|
|
UiDrawLine(g, m_area.X, m_area.Y + m_area.Height - 1.0f, m_area.X + m_area.Width,
|
|
m_area.Y + m_area.Height - 1.0f, m_theme->border, 1.0f);
|
|
}
|
|
|
|
bool Toolbar::HitTest(int x, int y) const
|
|
{
|
|
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
|
|
}
|
|
|
|
bool Toolbar::OnMouseMove(int x, int y)
|
|
{
|
|
int hoverIndex = -1;
|
|
for (std::size_t index = 0; index < m_buttons.size(); ++index)
|
|
{
|
|
if (UiPointInRect(m_buttons[index].rect, static_cast<float>(x), static_cast<float>(y)))
|
|
{
|
|
hoverIndex = static_cast<int>(index);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hoverIndex == m_hoverIndex)
|
|
{
|
|
return false;
|
|
}
|
|
m_hoverIndex = hoverIndex;
|
|
return true;
|
|
}
|
|
|
|
bool Toolbar::OnLButtonDown(int x, int y, ToolbarAction& action)
|
|
{
|
|
action = ToolbarAction::NONE;
|
|
|
|
for (std::size_t index = 0; index < m_buttons.size(); ++index)
|
|
{
|
|
Button& button = m_buttons[index];
|
|
if (!UiPointInRect(button.rect, static_cast<float>(x), static_cast<float>(y)))
|
|
{
|
|
continue;
|
|
}
|
|
if (!IsEnabled(button))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
m_pressedIndex = static_cast<int>(index);
|
|
button.pressed = true;
|
|
action = button.action;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Toolbar::OnLButtonUp(int x, int y, ToolbarAction& action)
|
|
{
|
|
action = ToolbarAction::NONE;
|
|
if (m_pressedIndex < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const int index = m_pressedIndex;
|
|
m_pressedIndex = -1;
|
|
if (index >= static_cast<int>(m_buttons.size()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Button& button = m_buttons[static_cast<std::size_t>(index)];
|
|
button.pressed = false;
|
|
|
|
const bool inside = UiPointInRect(button.rect, static_cast<float>(x), static_cast<float>(y));
|
|
m_hoverIndex = inside ? index : -1;
|
|
if (inside && IsEnabled(button))
|
|
{
|
|
action = button.action;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Toolbar::OnMouseLeave()
|
|
{
|
|
bool changed = false;
|
|
if (m_hoverIndex >= 0)
|
|
{
|
|
m_hoverIndex = -1;
|
|
changed = true;
|
|
}
|
|
if (m_pressedIndex >= 0)
|
|
{
|
|
if (m_pressedIndex < static_cast<int>(m_buttons.size()))
|
|
{
|
|
m_buttons[static_cast<std::size_t>(m_pressedIndex)].pressed = false;
|
|
}
|
|
m_pressedIndex = -1;
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
} // namespace jdi
|