1) 工具栏文案:"时间轴缩放"/"Y 轴缩放" -> "时间轴选区"/"Y 轴选区" - Toolbar.cpp 按钮标签、Toolbar.hpp 枚举注释、Application 状态提示语同步更新 2) 网格右键菜单:对勾从左侧移到右侧,并修正菜单内各元素边距 - 文本列统一为 kPadH + kItemPad(5 + 8),三类条目(HEADER/NORMAL/EDIT)共用; 原先左侧预留 22px 标记列,导致"网格 N"、"Y 轴最大值"、"选区 Y 轴自适应"等 所有文本的左边距被整体推右(用户报告的"文本左边距过大") - 对勾改画在条目右侧的 kMarkerColumn(18) 列内,文本宽度相应预留,不会与文字重叠 - EDIT 行输入框改为右对齐到条目内容区右边界,宽度由 NaturalEditFieldWidth() 统一计算, 空间不足时为左侧说明文字保留自然宽度(避免被菜单宽度上限挤成省略号) - 分隔线、色块行同样改用 kItemPad 内缩;kRightPad 已删除 3) "选区 Y 轴自适应"增加与"Y 轴自适应"一致的勾选标记 - Lane 新增 viewAutoRange / viewAutoMin / viewAutoMax,记录应用该操作时的视图时间区间 - 新增 PlotView::LaneRangeIsAutoToView():只有未被任何手动幅值改动且时间选区未变化时才为 true - 所有手动改动路径(ApplyValueSelection/ScaleLaneRange/ShiftLaneRange/ApplyLaneValueText/ EndEdit/ResetLaneRange/MoveSignalToLane)都会清除该状态; 时间选区变化后 viewAutoMin/Max 与当前视图不符,勾选自动消失 - RefreshPlotMenuValues() 同步刷新该项状态 4) 修复大文件(VERY_BIG_DATA 1.77GB / 5 信号)下悬停与选区缩放的严重卡顿 - 根因:任何悬停态变化(左侧信号列表、网格内命中曲线提示)或选区拖拽都会触发整幅重绘, 其中波形绘制约 80ms/帧,因此交互帧率只有 ~10fps - 静态场景缓存从"仅拖拽信号/光标"推广为"只要场景键一致就复用",缓存区域由整个客户区 收敛为仅绘图区:工具栏/标签栏/面板各自带悬停态必须每帧绘制(约 10.5ms), 而真正昂贵的波形改为直接贴图(约 1ms) - 场景键新增 contentHash(FNV-1a 折叠网格几何/幅值、每条信号的引用/可见性/ 颜色/透明度/线宽/样式/显示名与所属文件名):改样式、重命名、增删信号都会自动失效 - 随鼠标变化的内容全部移出缓存:新增 PaintHoverHighlight() 在贴图之后绘制 悬停网格底色(半透明叠加)与悬停信号光晕(PaintSignalHalo()); PaintLane 内只绘制拖拽中的信号光晕(key 中记录 haloFile/haloSignal) - EnsureSceneCache 改为要求位图尺寸与当前区域严格相等(原 >= 复用会在窗口缩小后 把旧像素画到绘图区之外) - Application::Render 移除 reuseScene 分支,统一每帧绘制界面框架 实测(VERY_BIG_DATA 5 信号,Release): - 整幅重绘 ~80ms -> 命中缓存贴图 ~1ms - 悬停左侧信号列表:~95ms -> ~14ms/帧 - 悬停网格内信号(含光晕高亮):~95ms -> ~28~32ms/帧 - 时间轴 / Y 轴选区拖拽:~95~105ms -> ~19~22ms/帧 - 非拖拽静止帧 ~118ms 维持不变;空闲 CPU 0.00% 构建: make clean; make all 无警告无错误
312 lines
8.0 KiB
C++
312 lines
8.0 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;
|
|
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::ZOOM_TIME, L"时间轴选区", false, true, true},
|
|
{ToolbarAction::ZOOM_VALUE, L"Y 轴选区", 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.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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|