Files
Just-DataInspector/Components/Toolbar/Src/Toolbar.cpp
T
DeepSeek V4.1 Flash ed13aa7d49 交互细节修复:悬停提示锚定、选区放大提示生命周期、光标样式统一与差值标签
- 信号悬停提示改为"锚定在首次出现位置":新增 m_hoverAnchorX/Y,
  PaintSignalInteraction 用锚点而非当前鼠标位置;同时补回 OnMouseMove 里
  漏写的 m_hoverSignal 赋值(该行缺失会导致悬停提示完全不显示)。

- 选区放大(RUBBER_SELECT)的提示文字按阶段切换:
  开启开关 -> 「横向或纵向拖动以放大选择区域,ESC退出选区模式」;
  左键按下不显示提示;开始拖动后维持「右键取消选区,ESC退出选区模式」;
  松开左键后淡出。新增 SetHintSticky/ReleaseHint(m_hintSticky 期间阻塞淡出),
  HintNeedsAnimation() 驱动宿主的淡出定时器。

- 两个时间光标统一为同一颜色与样式(去掉 theme.cursorAlt,仅保留 theme.cursor);
  光标把手统一为同一形状。工具栏按钮文案动态化:
  未选中「光标」/ 单光标「光标x1」/ 双光标「光标x2」
  (Toolbar::SetActionLabel 返回是否变化,变化时 Application::Relayout() 以重算按钮宽度)。

- 取值胶囊避让重叠:PaintCursor 先把两条时间读数标签与 Δt 标签的矩形预填进
  m_cursorLabelRects,取值标签逐条检测重叠并向下/向上让位,
  多信号数据点重合或过近时读数不再互相覆盖。
  两光标靠近时,光标 2 的时间读数标签自动上移一层叠放,避免互相覆盖。

- 双光标时间读数与 Δt 差值标签支持拖拽,并区分悬停与拖拽:
  悬停显示手形(IDC_HAND),拖拽统一使用抓握图标(IDC_SIZEALL,
  用 m_dragOnLabel 与"直接在光标线上抓取"的左右箭头区分);
  Δt 标签拖拽时两条光标整体平移,差值保持不变。

- Δt 差值显示样式与光标时间读数一致,文案为「Δt <时间差值>」,单位复合化:
  >= 60s 一路整合到 h/min/s(3662s -> 「1h 1min 2s」);1~60s 显示整秒,
  非整数时补 ms(5.06s -> 「5s 60ms」,5s -> 「5s」);< 1s 逐级降到
  ms/us/ns/ps 且只降一级,顶级单位为 ps 时不再降级并保留小数(「5.123 ps」)。

- 双击 Δt 进入编辑时数值量级始终为秒(例如显示 1min 时编辑框为 60.0),
  修正进入高级单位后(如 1.5min)编辑框只显示 1.5 的问题。

验证(使用 TestData/VERY_BIG_DATA.CSV,1.77GB / 5 信号):
- 悬停提示停留在首次出现位置,网格内继续移动鼠标不再跟随。
- 选区放大提示:开启/按下/拖动/松开四个阶段的文案与淡出均符合预期。
- 工具栏文案 光标 / 光标x1 / 光标x2 正常;两光标样式与颜色完全一致。
- 5 条信号合并网格时取值标签全部完整可见,无覆盖。
- 光标形态探测:时间读数标签与 Δt 标签悬停 HAND、拖拽 SIZEALL;
  直接在光标线上拖拽仍为 SIZEWE。
- Δt 分别输入 3662 / 5.06 / 5.123e-12 秒得到「Δt 1h 1min 2s」/
  「Δt 5s 60ms」/「Δt 5.11591 ps」;整数秒显示「Δt 5s」。
- 双击 Δt(显示 15s 457.214ms)编辑框内容为 15.4572(秒)。
- Δt 标签拖拽 (-320px):光标 64.4051/79.8623 -> 30.743/46.2002,差值保持 15.4572。
- 两光标靠近时时间读数标签上下叠放、Δt 标签落位正确,取值标签不重叠。
- [1].CSV 回归正常;make clean && make all 无警告无错误。
2026-10-08 21:05:02 +08:00

368 lines
9.2 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::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;
// 动态文本覆盖(例如「光标」按当前模式显示 光标 / 光标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