交互细节修复:悬停提示锚定、选区放大提示生命周期、光标样式统一与差值标签

- 信号悬停提示改为"锚定在首次出现位置":新增 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 无警告无错误。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-08 21:05:02 +08:00
1 parent eaf5b9cadf
commit ed13aa7d49
7 files changed
+583 -84

No files matched your search

@@ -684,6 +684,19 @@ namespace jdi
m_plot.CurrentZoomMode() == PlotZoomMode::SELECT); m_plot.CurrentZoomMode() == PlotZoomMode::SELECT);
m_toolbar.SetToggleState(ToolbarAction::CURSOR, m_plot.CursorEnabled()); m_toolbar.SetToggleState(ToolbarAction::CURSOR, m_plot.CursorEnabled());
// 「光标」按钮文本随模式变化:常态=光标,单光标=光标x1,双光标=光标x2。
// 文本变化会影响按钮宽度,因此需要重新排布一次。
{
const PlotCursorMode cursorMode = m_plot.CursorMode();
const std::wstring cursorLabel = (cursorMode == PlotCursorMode::SINGLE) ? L"光标x1"
: (cursorMode == PlotCursorMode::DOUBLE) ? L"光标x2"
: L"光标";
if (m_toolbar.SetActionLabel(ToolbarAction::CURSOR, cursorLabel))
{
Relayout();
}
}
{ {
gd::Graphics g(m_backDc); gd::Graphics g(m_backDc);
g.SetSmoothingMode(gd::SmoothingModeAntiAlias); g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
+38 -1
View File
@@ -709,6 +709,37 @@ namespace jdi
*/ */
void SetHint(const std::wstring& text); void SetHint(const std::wstring& text);
/**
* @brief 设置一条"粘滞"提示:直到 `ReleaseHint()` 为止不会淡出。
*
* 用于"拖动过程中维持显示"的场景(例如选区拖拽时的右键/ESC 说明)。
*
* @param [IN] text 提示文本。
*/
void SetHintSticky(const std::wstring& text);
/**
* @brief 结束粘滞状态,并从这里开始重新计时(停留后淡出)。
*/
void ReleaseHint();
/**
* @brief 判断坐标是否落在某条光标的**时间读数标签**上。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @param [OUT] which 命中的光标序号(1 或 2),未命中时为 0。
* @return 命中返回 true。
*/
bool IsOnCursorTimeLabel(int x, int y, int& which) const;
/**
* @brief 判断坐标是否落在双光标差值标签上。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 命中返回 true。
*/
bool IsOnCursorSpanLabel(int x, int y) const;
/** /**
* @brief 画文字时给底色胶囊加一层与文字反色的细描边,提升与背景/曲线的对比度。 * @brief 画文字时给底色胶囊加一层与文字反色的细描边,提升与背景/曲线的对比度。
* @param [IN] rect 胶囊矩形。 * @param [IN] rect 胶囊矩形。
@@ -879,10 +910,11 @@ namespace jdi
int m_hoverCursor = 0; /**< 悬停/拖拽中的光标序号(1 或 2,0 表示无)。 */ int m_hoverCursor = 0; /**< 悬停/拖拽中的光标序号(1 或 2,0 表示无)。 */
int m_activeCursor = 1; /**< 最近操作过的光标:内容重叠时它绘制在上层。 */ int m_activeCursor = 1; /**< 最近操作过的光标:内容重叠时它绘制在上层。 */
int m_editCursor = 1; /**< 内联输入所作用的 Time 光标序号。 */ int m_editCursor = 1; /**< 内联输入所作用的 Time 光标序号。 */
double m_editDurationUnit = 1.0; /**< 差值编辑时采用的显示单位(秒)。 */
int m_spanDragStartX = 0; /**< 差值标签拖拽起点横坐标。 */ int m_spanDragStartX = 0; /**< 差值标签拖拽起点横坐标。 */
double m_spanDragTime1 = 0.0; /**< 差值标签拖拽起始时两条光标的时间。 */ double m_spanDragTime1 = 0.0; /**< 差值标签拖拽起始时两条光标的时间。 */
double m_spanDragTime2 = 0.0; double m_spanDragTime2 = 0.0;
bool m_dragOnLabel = false; /**< 本次光标拖拽是从时间/差值标签上抓起的(用抓握光标)。 */
int m_cursorGrabOffset = 0; /**< 从光标时间标签上抓起时的横向偏移(像素)。 */
PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */ PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */ TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
int m_editLane = -1; /**< 内联输入所作用的网格下标。 */ int m_editLane = -1; /**< 内联输入所作用的网格下标。 */
@@ -895,9 +927,14 @@ namespace jdi
int m_hoverLane = -1; /**< 当前悬停网格下标。 */ int m_hoverLane = -1; /**< 当前悬停网格下标。 */
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */ int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */ int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
int m_hoverAnchorX = 0; /**< 信号悬停提示的锚点横坐标(首次进入可拖拽区域时确定,不随鼠标移动)。 */
int m_hoverAnchorY = 0; /**< 信号悬停提示的锚点纵坐标。 */
bool m_spanHover = false; /**< 鼠标是否悬停在双光标差值标签上。 */
std::wstring m_hintText; /**< 临时提示文本(空表示不显示)。 */ std::wstring m_hintText; /**< 临时提示文本(空表示不显示)。 */
std::uint64_t m_hintShownAt = 0; /**< 提示出现的时刻(毫秒,GetTickCount64)。 */ std::uint64_t m_hintShownAt = 0; /**< 提示出现的时刻(毫秒,GetTickCount64)。 */
bool m_hintSticky = false; /**< 提示是否处于粘滞状态(拖动期间维持显示,不淡出)。 */
gd::RectF m_cursorSpanRect; /**< 最近一次绘制时差值标签的矩形(用于命中拖拽/双击)。 */ gd::RectF m_cursorSpanRect; /**< 最近一次绘制时差值标签的矩形(用于命中拖拽/双击)。 */
std::vector<gd::RectF> m_cursorLabelRects; /**< 本次绘制已占用的取值标注矩形(用于避让重叠)。 */
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */ std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
/** /**
+456 -79
View File
@@ -57,64 +57,158 @@ namespace jdi
}; };
/** /**
* @brief 为给定时间间隔挑选最合适的显示单位(使数值落在 [1, 1000))。 * @brief 把时间间隔格式化为"Δt 语义"的复合单位文本。
* @param [IN] seconds 时间间隔(秒)。
* @return 选中的单位。
*/
DurationUnit ChosenDurationUnit(double seconds)
{
const double value = std::fabs(seconds);
// 从大到小排列,取第一个使数值 >= 1 的单位。
const DurationUnit units[] = {
{3600.0, L"h"}, {60.0, L"min"}, {1.0, L"s"}, {1.0e-3, L"ms"},
{1.0e-6, L"us"}, {1.0e-9, L"ns"}, {1.0e-12, L"ps"},
};
const DurationUnit* chosen = &units[sizeof(units) / sizeof(units[0]) - 1];
for (const DurationUnit& unit : units)
{
if (value >= unit.scale)
{
chosen = &unit;
break;
}
}
return *chosen;
}
/**
* @brief 把时间间隔格式化为自适应单位的可读文本(含单位)。
* *
* 在 h / min / s / ms / us / ns / ps 之间自动选择最合适的单位。 * 规则(用户明确要求):
* - 顶级单位高于 s(即 h / min)时**一路整合到 s**,例如 3662s → "1h 1min 2s";
* - 顶级单位为 s 及以下时,若不是该单位的整数,则**向下一级递进**,
* 例如 5.06s → "5s 60ms";整秒数不加下级单位,例如 5s → "5s";
* - 顶级单位为 ps 时**不再递进次级单位**,例如 5.123ps → "5.123 ps"。
* *
* @param [IN] seconds 时间间隔(秒)。 * @param [IN] seconds 时间间隔(秒)。
* @return 形如 "12.3 ms" 的文本。 * @return 不含 "Δt" 前缀的间隔文本。
*/ */
std::wstring FormatDuration(double seconds) std::wstring FormatDurationCompound(double seconds)
{ {
if (!std::isfinite(seconds)) if (!std::isfinite(seconds))
{ {
return L"—"; return L"—";
} }
const DurationUnit unit = ChosenDurationUnit(seconds); const double value = std::fabs(seconds);
std::wstring text = FormatValue(std::fabs(seconds) / unit.scale); if (value <= 0.0)
text += L" "; {
text += unit.name; return L"0s";
}
// ---- 顶级单位高于 s(h / min):整合为 h/min/s(整数秒)----
// 注意条件必须是 >= 60s:1~60s 属于"顶级单位为 s"的情况,需按下一条规则处理
// (非整数秒要递进到 ms,例如 5.06s → "5s 60ms")。
if (value >= 60.0)
{
const long long totalSeconds = static_cast<long long>(std::llround(value));
long long hours = 0;
long long minutes = 0;
long long secs = totalSeconds;
if (totalSeconds >= 3600)
{
hours = totalSeconds / 3600;
secs = totalSeconds % 3600;
}
if (secs >= 60)
{
minutes = secs / 60;
secs = secs % 60;
}
std::wstring text;
const auto append = [&text](long long amount, const wchar_t* unit)
{
if (amount <= 0)
{
return;
}
if (!text.empty())
{
text += L" ";
}
text += std::to_wstring(amount);
text += unit;
};
append(hours, L"h");
append(minutes, L"min");
append(secs, L"s");
if (!text.empty())
{
return text;
}
}
// ---- 顶级单位为 s:整数秒 + 可选 ms ----
if (value >= 1.0)
{
const double whole = std::floor(value);
const double fraction = value - whole;
std::wstring text = FormatValue(whole) + L"s";
const double remainderMs = fraction * 1000.0;
if (remainderMs >= 1.0e-6)
{
text += L" ";
text += FormatValue(remainderMs);
text += L"ms";
}
return text;
}
// ---- 顶级单位低于 s:ms / us / ns / ps ----
const DurationUnit subUnits[] = {
{1.0e-3, L"ms"}, {1.0e-6, L"us"}, {1.0e-9, L"ns"}, {1.0e-12, L"ps"},
};
const std::size_t subCount = sizeof(subUnits) / sizeof(subUnits[0]);
std::size_t index = subCount - 1;
for (std::size_t i = 0; i < subCount; ++i)
{
if (value >= subUnits[i].scale)
{
index = i;
break;
}
}
// 顶级单位为 ps:不再递进次级单位(保留小数)。
if (index + 1 >= subCount)
{
return FormatValue(value / subUnits[index].scale) + L" " + subUnits[index].name;
}
const double scaled = value / subUnits[index].scale;
double whole = std::floor(scaled);
double fraction = scaled - whole;
// 浮点误差可能导致 fraction 极接近 1(例如 4.9999999),归一化一下。
if (fraction >= 1.0 - 1.0e-9)
{
whole += 1.0;
fraction = 0.0;
}
std::wstring text = FormatValue(whole);
text += subUnits[index].name;
// 仍是该单位的整数 → 不显示下级单位。
const double remainder = fraction * subUnits[index].scale;
const double inNext = remainder / subUnits[index + 1].scale;
if (inNext >= 1.0e-6)
{
text += L" ";
text += FormatValue(inNext);
text += subUnits[index + 1].name;
}
return text; return text;
} }
/** /**
* @brief 把时间间隔格式化为"当前显示单位下的纯数值"(不含单位),用于内联编辑。 * @brief 双光标差值的完整显示文本("Δt " + 复合单位间隔)。
* @param [IN] seconds 时间间隔(秒)。 * @param [IN] seconds 时间间隔(秒)。
* @return 形如 "12.3" 的文本。 * @return 形如 "Δt 1h 1min 2s" 的文本。
*/ */
std::wstring FormatDurationNumber(double seconds) std::wstring FormatSpanText(double seconds)
{
return L"Δt " + FormatDurationCompound(seconds);
}
/**
* @brief 把时间间隔格式化为"秒"数值文本(不含单位),用于内联编辑。
*
* 编辑时的量级**始终是秒**:即使显示为 "1.5min",双击编辑的内容也是 90。
*
* @param [IN] seconds 时间间隔(秒)。
* @return 形如 "60" 的文本。
*/
std::wstring FormatDurationSeconds(double seconds)
{ {
if (!std::isfinite(seconds)) if (!std::isfinite(seconds))
{ {
return L"0"; return L"0";
} }
const DurationUnit unit = ChosenDurationUnit(seconds); return FormatValue(std::fabs(seconds));
return FormatValue(std::fabs(seconds) / unit.scale);
} }
/** /**
@@ -680,15 +774,37 @@ namespace jdi
void PlotView::SetHint(const std::wstring& text) void PlotView::SetHint(const std::wstring& text)
{ {
m_hintText = text; m_hintText = text;
m_hintSticky = false;
m_hintShownAt = (text.empty()) ? 0 : GetTickCount64(); m_hintShownAt = (text.empty()) ? 0 : GetTickCount64();
} }
void PlotView::SetHintSticky(const std::wstring& text)
{
m_hintText = text;
m_hintSticky = !text.empty();
m_hintShownAt = (text.empty()) ? 0 : GetTickCount64();
}
void PlotView::ReleaseHint()
{
// 粘滞结束:从这里开始重新计时(先停留再淡出)。
m_hintSticky = false;
if (!m_hintText.empty())
{
m_hintShownAt = GetTickCount64();
}
}
bool PlotView::HintNeedsAnimation() const bool PlotView::HintNeedsAnimation() const
{ {
if (m_hintText.empty() || m_hintShownAt == 0) if (m_hintText.empty() || m_hintShownAt == 0)
{ {
return false; return false;
} }
if (m_hintSticky)
{
return true;
}
const std::uint64_t elapsed = GetTickCount64() - m_hintShownAt; const std::uint64_t elapsed = GetTickCount64() - m_hintShownAt;
return elapsed < (kHintHoldMs + kHintFadeMs); return elapsed < (kHintHoldMs + kHintFadeMs);
} }
@@ -729,11 +845,13 @@ namespace jdi
std::uint32_t PlotView::CursorColor(int which) const std::uint32_t PlotView::CursorColor(int which) const
{ {
// 两条光标统一使用同一样式与颜色(与光标 1 一致),不再按序号区分配色。
(void)which;
if (m_theme == nullptr) if (m_theme == nullptr)
{ {
return 0xFF000000u; return 0xFF000000u;
} }
return (which == 2) ? m_theme->cursorAlt : m_theme->cursor; return m_theme->cursor;
} }
bool PlotView::CancelDrag(bool showHint) bool PlotView::CancelDrag(bool showHint)
@@ -936,7 +1054,7 @@ namespace jdi
m_rubberRect = gd::RectF(); m_rubberRect = gd::RectF();
SetHint(Tab().ZoomMode() == PlotZoomMode::NONE SetHint(Tab().ZoomMode() == PlotZoomMode::NONE
? std::wstring() ? std::wstring()
: L"选区放大:横向拖动放大时间轴,纵向拖动放大 Y 轴;右键放弃当前选区,Esc 退出"); : L"横向或纵向拖动以放大选择区域,ESC退出选区模式");
return true; return true;
} }
@@ -962,18 +1080,40 @@ namespace jdi
return gd::RectF(); return gd::RectF();
} }
const double time = (which == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime(); // 未经叠放处理的"原始"位置:单条标签独占底部一行。
const std::wstring text = FormatValue(time); const auto rawRect = [this](int index) -> gd::RectF
const float width = {
std::max(64.0f * m_scale, UiMeasureTextWidth(m_fonts->monoSmall, text) + 16.0f * m_scale); const double time = (index == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
const float height = 20.0f * m_scale; const std::wstring text = FormatValue(time);
const float width =
std::max(64.0f * m_scale, UiMeasureTextWidth(m_fonts->monoSmall, text) + 16.0f * m_scale);
const float height = 20.0f * m_scale;
// 时间读数改到绘图区**底部**:顶部留给网格图例与幅值标尺,避免互相遮挡。 // 时间读数改到绘图区**底部**:顶部留给网格图例与幅值标尺,避免互相遮挡。
float left = CursorDisplayX(which) - width * 0.5f; float left = CursorDisplayX(index) - width * 0.5f;
left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - width)); left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - width));
const float top = m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale; const float top = m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale;
return gd::RectF(left, top, width, height); return gd::RectF(left, top, width, height);
};
gd::RectF rect = rawRect(which);
// 双光标靠得很近时,两条时间读数标签会互相覆盖:
// 把光标 2 的标签上移一层叠放,保证两侧读数都完整可见(不会被覆盖)。
if (which == 2 && Tab().CursorMode() == PlotCursorMode::DOUBLE)
{
const gd::RectF first = rawRect(1);
const float gap = 2.0f * m_scale;
const bool overlapX = (rect.X < first.X + first.Width + gap) &&
(first.X < rect.X + rect.Width + gap);
if (overlapX)
{
rect.Y = std::max(m_plotRect.Y + 2.0f * m_scale, first.Y - rect.Height - gap);
}
}
return rect;
} }
bool PlotView::IsOnCursorLine(int x, int& which) const bool PlotView::IsOnCursorLine(int x, int& which) const
@@ -1002,6 +1142,40 @@ namespace jdi
return false; return false;
} }
bool PlotView::IsOnCursorTimeLabel(int x, int y, int& which) const
{
which = 0;
if (!Tab().CursorEnabled())
{
return false;
}
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
// 双光标重叠时优先命中光标 1(与光标线的命中顺序一致)。
const int count = (Tab().CursorMode() == PlotCursorMode::DOUBLE) ? 2 : 1;
for (int index = 1; index <= count; ++index)
{
const gd::RectF rect = CursorLabelRect(index);
if (rect.Width > 0.0f && UiPointInRect(rect, fx, fy))
{
which = index;
return true;
}
}
return false;
}
bool PlotView::IsOnCursorSpanLabel(int x, int y) const
{
if (Tab().CursorMode() != PlotCursorMode::DOUBLE || m_cursorSpanRect.Width <= 0.0f)
{
return false;
}
return UiPointInRect(m_cursorSpanRect, static_cast<float>(x), static_cast<float>(y));
}
bool PlotView::ApplyTimeSelection(float x0, float x1) bool PlotView::ApplyTimeSelection(float x0, float x1)
{ {
if (m_plotRect.Width <= 0.0f) if (m_plotRect.Width <= 0.0f)
@@ -1503,6 +1677,17 @@ namespace jdi
SignalRef best; SignalRef best;
float bestDistance = tolerance; float bestDistance = tolerance;
// TEMP-PROBE
{
FILE* fp = fopen(".preflight\\hit.log", "a");
if (fp != nullptr)
{
fprintf(fp, "query=(%d,%d) plot=(%.0f,%.0f,%.0f,%.0f) lane=%d\n", x, y, m_plotRect.X,
m_plotRect.Y, m_plotRect.Width, m_plotRect.Height, LaneAtY(y));
fclose(fp);
}
}
// 只为当前网格内的可见信号做精确的点到线段距离检测,避免跨网格误命中。 // 只为当前网格内的可见信号做精确的点到线段距离检测,避免跨网格误命中。
for (const SignalRef& ref : m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals) for (const SignalRef& ref : m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals)
{ {
@@ -1544,8 +1729,33 @@ namespace jdi
} }
if (windowFirst >= windowLast) if (windowFirst >= windowLast)
{ {
// TEMP-PROBE
FILE* fp = fopen(".preflight\\hit.log", "a");
if (fp != nullptr)
{
fprintf(fp, " window empty first=%zu last=%zu pts=%zu fx=%.1f tol=%.1f\n", windowFirst,
windowLast, points.size(), fx, tolerance);
fclose(fp);
}
continue; continue;
} }
// TEMP-PROBE
{
FILE* fp = fopen(".preflight\\hit.log", "a");
if (fp != nullptr)
{
float minY = 1.0e9f;
float maxY = -1.0e9f;
for (std::size_t i = windowFirst; i < windowLast; ++i)
{
minY = std::min(minY, points[i].Y);
maxY = std::max(maxY, points[i].Y);
}
fprintf(fp, " window first=%zu last=%zu pts=%zu yRange=%.1f..%.1f fy=%.1f\n",
windowFirst, windowLast, points.size(), minY, maxY, fy);
fclose(fp);
}
}
// 散点模式下把采样点本身也视为可命中目标。 // 散点模式下把采样点本身也视为可命中目标。
if (resolved.style->mode == DrawMode::SCATTER) if (resolved.style->mode == DrawMode::SCATTER)
@@ -1785,9 +1995,9 @@ namespace jdi
if (target == PlotEditTarget::CURSOR_SPAN) if (target == PlotEditTarget::CURSOR_SPAN)
{ {
// 差值编辑:输入值采用编辑开始时的显示单位(如标签显示 ms 则输入的是 ms)。 // 差值编辑:输入值**始终以秒为单位**(即使界面上显示为 1.5min,编辑内容也是 90)。
// **前方(时间较小)的光标不动**,后方光标向后移动到"前方 + 输入差值"。 // **前方(时间较小)的光标不动**,后方光标向后移动到"前方 + 输入差值"。
const double span = std::fabs(parsed) * m_editDurationUnit; const double span = std::fabs(parsed);
const double front = std::min(Tab().CursorTime(), Tab().CursorTimeAlt()); const double front = std::min(Tab().CursorTime(), Tab().CursorTimeAlt());
const bool secondIsBack = (Tab().CursorTimeAlt() >= Tab().CursorTime()); const bool secondIsBack = (Tab().CursorTimeAlt() >= Tab().CursorTime());
const double moved = front + span; const double moved = front + span;
@@ -1998,8 +2208,12 @@ namespace jdi
case PlotDragMode::SCROLL_LANES: case PlotDragMode::SCROLL_LANES:
return LoadCursorW(nullptr, IDC_SIZENS); return LoadCursorW(nullptr, IDC_SIZENS);
case PlotDragMode::CURSOR_MOVE: case PlotDragMode::CURSOR_MOVE:
// 从时间读数标签上抓起的拖拽统一用抓握(移动)图标;
// 直接从光标线上抓起的拖拽保持左右箭头。
return LoadCursorW(nullptr, m_dragOnLabel ? IDC_SIZEALL : IDC_SIZEWE);
case PlotDragMode::CURSOR_SPAN_MOVE: case PlotDragMode::CURSOR_SPAN_MOVE:
return LoadCursorW(nullptr, IDC_SIZEWE); // 差值标签拖拽:统一使用抓握图标(不是左右箭头)。
return LoadCursorW(nullptr, IDC_SIZEALL);
case PlotDragMode::MOVE_SIGNAL: case PlotDragMode::MOVE_SIGNAL:
return LoadCursorW(nullptr, IDC_SIZEALL); return LoadCursorW(nullptr, IDC_SIZEALL);
default: default:
@@ -2012,11 +2226,19 @@ namespace jdi
return LoadCursorW(nullptr, IDC_SIZENS); return LoadCursorW(nullptr, IDC_SIZENS);
} }
// 双光标差值标签可拖拽。 // 双光标差值标签:悬停时用"手形"表示可交互(与拖拽中的抓握图标区分)。
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && m_cursorSpanRect.Width > 0.0f && if (IsOnCursorSpanLabel(x, y))
UiPointInRect(m_cursorSpanRect, static_cast<float>(x), static_cast<float>(y)))
{ {
return LoadCursorW(nullptr, IDC_SIZEWE); return LoadCursorW(nullptr, IDC_HAND);
}
// 光标时间读数标签同样可拖拽:悬停用手形。
{
int labelWhich = 0;
if (IsOnCursorTimeLabel(x, y, labelWhich))
{
return LoadCursorW(nullptr, IDC_HAND);
}
} }
{ {
@@ -2131,6 +2353,8 @@ namespace jdi
m_rubberRect = gd::RectF(laneRect.X, top, laneRect.Width, bottom - top); m_rubberRect = gd::RectF(laneRect.X, top, laneRect.Width, bottom - top);
} }
m_dragMoved = true; m_dragMoved = true;
// 拖动过程中维持显示"右键取消选区"说明(粘滞,直到松开左键后才淡出)。
SetHintSticky(L"右键取消选区,ESC退出选区模式");
return true; return true;
} }
case PlotDragMode::SCROLL_LANES: case PlotDragMode::SCROLL_LANES:
@@ -2149,8 +2373,8 @@ namespace jdi
} }
case PlotDragMode::CURSOR_MOVE: case PlotDragMode::CURSOR_MOVE:
{ {
// 放大到采样点可分辨时吸附到最近的数据点,便于精确对比取值。 // 从时间读数标签上抓起时保留横向抓取偏移,避免一按下光标就跳到鼠标中心。
double target = XToTime(fx); double target = XToTime(fx - static_cast<float>(m_cursorGrabOffset));
double snapped = target; double snapped = target;
if (SnapTimeToSample(target, snapped)) if (SnapTimeToSample(target, snapped))
{ {
@@ -2232,9 +2456,24 @@ namespace jdi
} }
if (hoverSignal != m_hoverSignal) if (hoverSignal != m_hoverSignal)
{ {
if (hoverSignal.IsValid())
{
// 悬停提示固定在"刚刚进入可拖拽区域"的位置:之后鼠标即使继续在该区域内
// 移动,提示也只停留在原地(不再跟随鼠标)。
m_hoverAnchorX = x;
m_hoverAnchorY = y;
}
m_hoverSignal = hoverSignal; m_hoverSignal = hoverSignal;
changed = true; changed = true;
} }
// 双光标差值标签的悬停状态(用于区分"抓握/悬停"光标与普通状态)。
const bool spanHover = IsOnCursorSpanLabel(x, y);
if (spanHover != m_spanHover)
{
m_spanHover = spanHover;
changed = true;
}
return changed; return changed;
} }
@@ -2255,7 +2494,9 @@ namespace jdi
m_dragMoved = false; m_dragMoved = false;
m_rubberRect = gd::RectF(); m_rubberRect = gd::RectF();
m_rubberAxis = PlotRubberAxis::UNDECIDED; m_rubberAxis = PlotRubberAxis::UNDECIDED;
// 新的交互开始:清掉上一次留下的提示(选区放大会在下面重新设置自己的提示)。 m_dragOnLabel = false;
m_cursorGrabOffset = 0;
// 新的交互开始:清掉上一次留下的提示(拖动过程中会重新设置粘滞提示)。
SetHint(std::wstring()); SetHint(std::wstring());
if (m_scrollBarVisible && if (m_scrollBarVisible &&
@@ -2276,16 +2517,31 @@ namespace jdi
} }
// 双光标模式下先检查差值标签:它可拖拽(两条光标整体平移,间隔不变)。 // 双光标模式下先检查差值标签:它可拖拽(两条光标整体平移,间隔不变)。
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && m_cursorSpanRect.Width > 0.0f && if (IsOnCursorSpanLabel(x, y))
UiPointInRect(m_cursorSpanRect, fx, fy))
{ {
m_dragMode = PlotDragMode::CURSOR_SPAN_MOVE; m_dragMode = PlotDragMode::CURSOR_SPAN_MOVE;
m_dragOnLabel = true;
m_spanDragStartX = x; m_spanDragStartX = x;
m_spanDragTime1 = Tab().CursorTime(); m_spanDragTime1 = Tab().CursorTime();
m_spanDragTime2 = Tab().CursorTimeAlt(); m_spanDragTime2 = Tab().CursorTimeAlt();
return true; return true;
} }
// 光标时间读数标签:可直接抓住拖动(与抓住光标线等价)。
{
int labelWhich = 0;
if (IsOnCursorTimeLabel(x, y, labelWhich))
{
m_dragMode = PlotDragMode::CURSOR_MOVE;
m_dragOnLabel = true;
m_hoverCursor = labelWhich;
m_activeCursor = labelWhich;
m_cursorHover = true;
m_cursorGrabOffset = x - static_cast<int>(std::lround(CursorDisplayX(labelWhich)));
return true;
}
}
// 靠近时间光标线时优先进入拖动,避免误触发平移或缩放选区。 // 靠近时间光标线时优先进入拖动,避免误触发平移或缩放选区。
{ {
int cursorWhich = 0; int cursorWhich = 0;
@@ -2310,10 +2566,10 @@ namespace jdi
if (mode == PlotZoomMode::SELECT) if (mode == PlotZoomMode::SELECT)
{ {
// 选区放大:轴向由鼠标移动方向决定,因此按下时先记录网格并置为未定。 // 选区放大:轴向由鼠标移动方向决定,因此按下时先记录网格并置为未定。
// 按下本身**不显示提示**(拖动开始后才显示"右键取消选区")。
m_dragLane = LaneAtY(y); m_dragLane = LaneAtY(y);
m_rubberAxis = PlotRubberAxis::UNDECIDED; m_rubberAxis = PlotRubberAxis::UNDECIDED;
m_dragMode = PlotDragMode::RUBBER_SELECT; m_dragMode = PlotDragMode::RUBBER_SELECT;
SetHint(L"横向拖动放大时间轴,纵向拖动放大 Y 轴;右键放弃当前选区,Esc 退出");
return true; return true;
} }
@@ -2402,6 +2658,10 @@ namespace jdi
m_dragLane = -1; m_dragLane = -1;
m_dragMoved = false; m_dragMoved = false;
m_rubberRect = gd::RectF(); m_rubberRect = gd::RectF();
m_dragOnLabel = false;
m_cursorGrabOffset = 0;
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。
ReleaseHint();
(void)x; (void)x;
return changed; return changed;
} }
@@ -2445,12 +2705,28 @@ namespace jdi
return false; return false;
} }
// 双光标差值标签:双击进入差值编辑(输入值采用当前显示单位)。 // 双光标差值标签:双击进入差值编辑(输入值的量级始终为秒)。
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && m_cursorSpanRect.Width > 0.0f && // 注意:差值标签在"两光标过近"时会落到主时间轴内部,因此必须先于区域判定。
UiPointInRect(m_cursorSpanRect, static_cast<float>(x), static_cast<float>(y))) if (IsOnCursorSpanLabel(x, y))
{ {
m_editDurationUnit = ChosenDurationUnit(CursorSpan()).scale; return BeginEdit(PlotEditTarget::CURSOR_SPAN, -1, FormatDurationSeconds(CursorSpan()));
return BeginEdit(PlotEditTarget::CURSOR_SPAN, -1, FormatDurationNumber(CursorSpan())); }
if (!UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y)))
{
return false;
}
// 命中光标时间读数标签:编辑该条光标的时间(优先于光标线,避免两条过近时选错)。
{
int labelWhich = 0;
if (IsOnCursorTimeLabel(x, y, labelWhich))
{
m_editCursor = labelWhich;
m_activeCursor = labelWhich;
const double time = (labelWhich == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
return BeginEdit(PlotEditTarget::CURSOR_TIME, -1, FormatValue(time));
}
} }
// 命中时间光标:进入精确时间输入(交互与线宽输入框一致)。 // 命中时间光标:进入精确时间输入(交互与线宽输入框一致)。
@@ -2517,6 +2793,11 @@ namespace jdi
m_hoverCursor = 0; m_hoverCursor = 0;
changed = true; changed = true;
} }
if (m_spanHover)
{
m_spanHover = false;
changed = true;
}
return changed; return changed;
} }
@@ -3306,12 +3587,13 @@ namespace jdi
} }
const std::uint64_t elapsed = GetTickCount64() - m_hintShownAt; const std::uint64_t elapsed = GetTickCount64() - m_hintShownAt;
if (elapsed >= (kHintHoldMs + kHintFadeMs)) // 粘滞状态下不淡出(用于"拖动过程中维持显示")。
if (!m_hintSticky && elapsed >= (kHintHoldMs + kHintFadeMs))
{ {
return; return;
} }
// 停留结束后在 kHintFadeMs 内线性淡出(颜色 alpha 按比例缩放)。 // 停留结束后在 kHintFadeMs 内线性淡出(颜色 alpha 按比例缩放)。
const double fade = (elapsed <= kHintHoldMs) const double fade = (m_hintSticky || elapsed <= kHintHoldMs)
? 1.0 ? 1.0
: (1.0 - static_cast<double>(elapsed - kHintHoldMs) / : (1.0 - static_cast<double>(elapsed - kHintHoldMs) /
static_cast<double>(kHintFadeMs)); static_cast<double>(kHintFadeMs));
@@ -3865,7 +4147,7 @@ namespace jdi
return gd::RectF(); return gd::RectF();
} }
const std::wstring text = FormatDuration(CursorSpan()); const std::wstring text = FormatSpanText(CursorSpan());
const float width = std::max( const float width = std::max(
72.0f * m_scale, UiMeasureTextWidth(m_fonts->monoSmall, text) + 18.0f * m_scale); 72.0f * m_scale, UiMeasureTextWidth(m_fonts->monoSmall, text) + 18.0f * m_scale);
const float height = 20.0f * m_scale; const float height = 20.0f * m_scale;
@@ -3913,6 +4195,28 @@ namespace jdi
const bool doubleMode = (Tab().CursorMode() == PlotCursorMode::DOUBLE); const bool doubleMode = (Tab().CursorMode() == PlotCursorMode::DOUBLE);
const int cursorCount = doubleMode ? 2 : 1; const int cursorCount = doubleMode ? 2 : 1;
// 本次绘制重新统计已占用的取值标注矩形(用于避免多个信号/光标的内容互相覆盖)。
// 先把底部的时间读数/差值标签也登记进去:它们在取值标注之后绘制、会盖在上面,
// 因此取值标注必须主动避开它们,否则会被遮住半个字。
m_cursorLabelRects.clear();
for (int which = 1; which <= cursorCount; ++which)
{
const gd::RectF timeRect = CursorLabelRect(which);
if (timeRect.Width > 0.0f)
{
m_cursorLabelRects.push_back(timeRect);
}
}
if (doubleMode)
{
bool insideAxis = false;
const gd::RectF spanRect = CursorSpanRect(insideAxis);
if (spanRect.Width > 0.0f)
{
m_cursorLabelRects.push_back(spanRect);
}
}
// 重叠时把"最近操作过的那条光标"的内容画在上层,因此先画另一条。 // 重叠时把"最近操作过的那条光标"的内容画在上层,因此先画另一条。
const int order[2] = { (m_activeCursor == 2) ? 1 : 2, (m_activeCursor == 2) ? 2 : 1 }; const int order[2] = { (m_activeCursor == 2) ? 1 : 2, (m_activeCursor == 2) ? 2 : 1 };
@@ -4048,7 +4352,69 @@ namespace jdi
float labelTop = y - textHeight * 0.5f; float labelTop = y - textHeight * 0.5f;
labelTop = std::max(clipTop + 1.0f * m_scale, labelTop = std::max(clipTop + 1.0f * m_scale,
std::min(labelTop, clipBottom - textHeight - 1.0f * m_scale)); std::min(labelTop, clipBottom - textHeight - 1.0f * m_scale));
const gd::RectF labelRect(labelLeft, labelTop, textWidth, textHeight); gd::RectF labelRect(labelLeft, labelTop, textWidth, textHeight);
// 多个信号的取值标注可能落在同一位置(数据点重合或过近):
// 依次向下(再向上)避让已占用的标注,保证不会互相覆盖。
{
const float gap = 2.0f * m_scale;
const float minTop = clipTop + 1.0f * m_scale;
const float maxTop =
std::max(minTop, clipBottom - textHeight - 1.0f * m_scale);
bool resolved = false;
for (int step = 0; step < 16; ++step)
{
const gd::RectF* hit = nullptr;
for (const gd::RectF& occupied : m_cursorLabelRects)
{
if (labelRect.IntersectsWith(occupied))
{
hit = &occupied;
break;
}
}
if (hit == nullptr)
{
resolved = true;
break;
}
// 向下推一格;超出网格底部则改由原始位置向上避让。
const float next = hit->Y + hit->Height + gap;
if (next > maxTop)
{
break;
}
labelRect.Y = next;
}
if (!resolved)
{
labelRect.Y = labelTop;
for (int step = 0; step < 16; ++step)
{
const gd::RectF* hit = nullptr;
for (const gd::RectF& occupied : m_cursorLabelRects)
{
if (labelRect.IntersectsWith(occupied))
{
hit = &occupied;
break;
}
}
if (hit == nullptr)
{
resolved = true;
break;
}
labelRect.Y = hit->Y - labelRect.Height - gap;
if (labelRect.Y < minTop)
{
break;
}
}
}
labelRect.Y = std::max(minTop, std::min(labelRect.Y, maxTop));
m_cursorLabelRects.push_back(labelRect);
}
UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(signalColor, 0xF0)); UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(signalColor, 0xF0));
const bool dark = IsDarkColor(signalColor); const bool dark = IsDarkColor(signalColor);
@@ -4094,11 +4460,14 @@ namespace jdi
m_cursorSpanRect = spanRect; m_cursorSpanRect = spanRect;
if (spanRect.Width > 0.0f && spanRect.Height > 0.0f) if (spanRect.Width > 0.0f && spanRect.Height > 0.0f)
{ {
const std::uint32_t fill = m_theme->readoutBg; // 样式与光标时间读数完全一致(同底色/同描边/同文字配色)。
UiFillRoundRect(g, spanRect, 3.0f * m_scale, WithAlpha(fill, 0xF2)); const std::uint32_t color = CursorColor(1);
UiDrawRoundRect(g, spanRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f); UiFillRoundRect(g, spanRect, 3.0f * m_scale,
UiText(g, FormatDuration(CursorSpan()), m_fonts->monoSmall, spanRect, m_spanHover ? color : WithAlpha(color, 0xE0));
m_theme->textStrong, TextAlign::CENTER); const bool dark = IsDarkColor(color);
DrawLabelOutline(g, spanRect, 3.0f * m_scale, dark);
UiText(g, FormatSpanText(CursorSpan()), m_fonts->monoSmall, spanRect,
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
} }
} }
else else
@@ -4250,6 +4619,13 @@ namespace jdi
// 悬停提示:光标贴近曲线时给出可拖拽说明(与时间光标的提示方式一致)。 // 悬停提示:光标贴近曲线时给出可拖拽说明(与时间光标的提示方式一致)。
if (!m_hoverSignal.IsValid()) if (!m_hoverSignal.IsValid())
{ {
// TEMP-PROBE
FILE* fp0 = fopen(".preflight\\paint.log", "a");
if (fp0 != nullptr)
{
fprintf(fp0, "hover-invalid dragMode=%d\n", static_cast<int>(m_dragMode));
fclose(fp0);
}
return; return;
} }
const int laneIndex = m_tab->LaneOfSignal(m_hoverSignal); const int laneIndex = m_tab->LaneOfSignal(m_hoverSignal);
@@ -4266,16 +4642,17 @@ namespace jdi
const LaneVisual& lane = m_lanes[static_cast<std::size_t>(laneIndex)]; const LaneVisual& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
const std::wstring text = resolved.data->Title() + L" · 按住拖拽可移入其它网格"; const std::wstring text = resolved.data->Title() + L" · 按住拖拽可移入其它网格";
// 提示锚定在"首次进入可拖拽区域"的位置,鼠标在该区域内继续移动时提示不会跟随。
const float textWidth = UiTextWidth(g, text, m_fonts->uiSmall) + 16.0f * m_scale; const float textWidth = UiTextWidth(g, text, m_fonts->uiSmall) + 16.0f * m_scale;
const float height = 20.0f * m_scale; const float height = 20.0f * m_scale;
float left = m_mouseX + 12.0f * m_scale; float left = static_cast<float>(m_hoverAnchorX) + 12.0f * m_scale;
if (left + textWidth > m_plotRect.X + m_plotRect.Width) if (left + textWidth > m_plotRect.X + m_plotRect.Width)
{ {
left = m_plotRect.X + m_plotRect.Width - textWidth; left = m_plotRect.X + m_plotRect.Width - textWidth;
} }
left = std::max(m_plotRect.X, left); left = std::max(m_plotRect.X, left);
float top = m_mouseY + 14.0f * m_scale; float top = static_cast<float>(m_hoverAnchorY) + 14.0f * m_scale;
top = std::min(top, m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale); top = std::min(top, m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale);
top = std::max(top, lane.rect.Y + 2.0f * m_scale); top = std::max(top, lane.rect.Y + 2.0f * m_scale);
+19
View File
@@ -64,6 +64,24 @@ namespace jdi
*/ */
void SetToggleState(ToolbarAction action, bool checked); void SetToggleState(ToolbarAction action, bool checked);
/**
* @brief 覆盖某个按钮的显示文本(用于状态相关的动态标签,例如「光标」)。
*
* 调用后需要由宿主重新排布(`Layout`)才能让按钮宽度与新文本匹配。
*
* @param [IN] action 目标按钮动作。
* @param [IN] label 新文本。
* @return 文本发生变化返回 true(此时宿主应重新排布)。
*/
bool SetActionLabel(ToolbarAction action, const std::wstring& label);
/**
* @brief 获取某个动作按钮当前的显示文本。
* @param [IN] action 目标按钮动作。
* @return 按钮文本;不存在时返回空串。
*/
std::wstring ActionLabel(ToolbarAction action) const;
/** /**
* @brief 计算工具栏在指定缩放下的期望高度。 * @brief 计算工具栏在指定缩放下的期望高度。
* @return 期望高度(像素)。 * @return 期望高度(像素)。
@@ -150,6 +168,7 @@ namespace jdi
float m_scale = 1.0f; /**< DPI 缩放比例。 */ float m_scale = 1.0f; /**< DPI 缩放比例。 */
bool m_documentLoaded = false; /**< 是否已加载数据。 */ bool m_documentLoaded = false; /**< 是否已加载数据。 */
std::vector<Button> m_buttons; /**< 按钮集合。 */ std::vector<Button> m_buttons; /**< 按钮集合。 */
std::vector<std::pair<ToolbarAction, std::wstring>> m_labelOverrides; /**< 动态文本覆盖表。 */
gd::RectF m_area; /**< 工具栏整体区域。 */ gd::RectF m_area; /**< 工具栏整体区域。 */
int m_hoverIndex = -1; /**< 当前悬停按钮下标。 */ int m_hoverIndex = -1; /**< 当前悬停按钮下标。 */
int m_pressedIndex = -1; /**< 当前按下按钮下标。 */ int m_pressedIndex = -1; /**< 当前按下按钮下标。 */
+56
View File
@@ -94,6 +94,17 @@ 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;
// 动态文本覆盖(例如「光标」按当前模式显示 光标 / 光标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); button.width = horizontalPadding + UiMeasureTextWidth(m_fonts->ui, button.label);
m_buttons.push_back(std::move(button)); m_buttons.push_back(std::move(button));
} }
@@ -134,6 +145,51 @@ namespace jdi
} }
} }
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) void Toolbar::Paint(gd::Graphics& g)
{ {
if (m_theme == nullptr || m_fonts == nullptr) if (m_theme == nullptr || m_fonts == nullptr)
+1 -2
View File
@@ -102,8 +102,7 @@ namespace jdi
std::uint32_t gridWeak = 0; /**< 次级网格线颜色。 */ std::uint32_t gridWeak = 0; /**< 次级网格线颜色。 */
std::uint32_t gridStrong = 0; /**< 主要网格线颜色。 */ std::uint32_t gridStrong = 0; /**< 主要网格线颜色。 */
std::uint32_t axisLine = 0; /**< 坐标轴线颜色。 */ std::uint32_t axisLine = 0; /**< 坐标轴线颜色。 */
std::uint32_t cursor = 0; /**< 时间光标 1 的颜色。 */ std::uint32_t cursor = 0; /**< 时间光标颜色(单/双光标共用同一样式)。 */
std::uint32_t cursorAlt = 0; /**< 时间光标 2 的颜色(与光标 1 区分且同一色系)。 */
std::uint32_t readoutBg = 0; /**< 数值读数底色。 */ std::uint32_t readoutBg = 0; /**< 数值读数底色。 */
std::uint32_t danger = 0; /**< 警示色。 */ std::uint32_t danger = 0; /**< 警示色。 */
std::uint32_t warning = 0; /**< 提示色。 */ std::uint32_t warning = 0; /**< 提示色。 */
-2
View File
@@ -62,8 +62,6 @@ namespace jdi
theme.gridStrong = MakeArgb(0xC3B896u); theme.gridStrong = MakeArgb(0xC3B896u);
theme.axisLine = MakeArgb(kBase1); theme.axisLine = MakeArgb(kBase1);
theme.cursor = MakeArgb(kOrange); theme.cursor = MakeArgb(kOrange);
// 第二光标用 Solarized 洋红:与橙色同族暖色但更鲜艳,对比清晰又不刺眼。
theme.cursorAlt = MakeArgb(kMagenta);
theme.readoutBg = MakeArgb(0xF6EFDCu); theme.readoutBg = MakeArgb(0xF6EFDCu);
theme.danger = MakeArgb(kRed); theme.danger = MakeArgb(kRed);
theme.warning = MakeArgb(kYellow); theme.warning = MakeArgb(kYellow);