交互细节修复:悬停提示锚定、选区放大提示生命周期、光标样式统一与差值标签
- 信号悬停提示改为"锚定在首次出现位置":新增 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:
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_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);
|
||||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
|
||||
@@ -709,6 +709,37 @@ namespace jdi
|
||||
*/
|
||||
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 画文字时给底色胶囊加一层与文字反色的细描边,提升与背景/曲线的对比度。
|
||||
* @param [IN] rect 胶囊矩形。
|
||||
@@ -879,10 +910,11 @@ namespace jdi
|
||||
int m_hoverCursor = 0; /**< 悬停/拖拽中的光标序号(1 或 2,0 表示无)。 */
|
||||
int m_activeCursor = 1; /**< 最近操作过的光标:内容重叠时它绘制在上层。 */
|
||||
int m_editCursor = 1; /**< 内联输入所作用的 Time 光标序号。 */
|
||||
double m_editDurationUnit = 1.0; /**< 差值编辑时采用的显示单位(秒)。 */
|
||||
int m_spanDragStartX = 0; /**< 差值标签拖拽起点横坐标。 */
|
||||
double m_spanDragTime1 = 0.0; /**< 差值标签拖拽起始时两条光标的时间。 */
|
||||
double m_spanDragTime2 = 0.0;
|
||||
bool m_dragOnLabel = false; /**< 本次光标拖拽是从时间/差值标签上抓起的(用抓握光标)。 */
|
||||
int m_cursorGrabOffset = 0; /**< 从光标时间标签上抓起时的横向偏移(像素)。 */
|
||||
PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */
|
||||
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
|
||||
int m_editLane = -1; /**< 内联输入所作用的网格下标。 */
|
||||
@@ -895,9 +927,14 @@ namespace jdi
|
||||
int m_hoverLane = -1; /**< 当前悬停网格下标。 */
|
||||
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
|
||||
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
|
||||
int m_hoverAnchorX = 0; /**< 信号悬停提示的锚点横坐标(首次进入可拖拽区域时确定,不随鼠标移动)。 */
|
||||
int m_hoverAnchorY = 0; /**< 信号悬停提示的锚点纵坐标。 */
|
||||
bool m_spanHover = false; /**< 鼠标是否悬停在双光标差值标签上。 */
|
||||
std::wstring m_hintText; /**< 临时提示文本(空表示不显示)。 */
|
||||
std::uint64_t m_hintShownAt = 0; /**< 提示出现的时刻(毫秒,GetTickCount64)。 */
|
||||
bool m_hintSticky = false; /**< 提示是否处于粘滞状态(拖动期间维持显示,不淡出)。 */
|
||||
gd::RectF m_cursorSpanRect; /**< 最近一次绘制时差值标签的矩形(用于命中拖拽/双击)。 */
|
||||
std::vector<gd::RectF> m_cursorLabelRects; /**< 本次绘制已占用的取值标注矩形(用于避让重叠)。 */
|
||||
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
|
||||
|
||||
/**
|
||||
|
||||
@@ -57,64 +57,158 @@ namespace jdi
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 为给定时间间隔挑选最合适的显示单位(使数值落在 [1, 1000))。
|
||||
* @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 把时间间隔格式化为自适应单位的可读文本(含单位)。
|
||||
* @brief 把时间间隔格式化为"Δt 语义"的复合单位文本。
|
||||
*
|
||||
* 在 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 时间间隔(秒)。
|
||||
* @return 形如 "12.3 ms" 的文本。
|
||||
* @return 不含 "Δt" 前缀的间隔文本。
|
||||
*/
|
||||
std::wstring FormatDuration(double seconds)
|
||||
std::wstring FormatDurationCompound(double seconds)
|
||||
{
|
||||
if (!std::isfinite(seconds))
|
||||
{
|
||||
return L"—";
|
||||
}
|
||||
const DurationUnit unit = ChosenDurationUnit(seconds);
|
||||
std::wstring text = FormatValue(std::fabs(seconds) / unit.scale);
|
||||
text += L" ";
|
||||
text += unit.name;
|
||||
const double value = std::fabs(seconds);
|
||||
if (value <= 0.0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 把时间间隔格式化为"当前显示单位下的纯数值"(不含单位),用于内联编辑。
|
||||
* @brief 双光标差值的完整显示文本("Δt " + 复合单位间隔)。
|
||||
* @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))
|
||||
{
|
||||
return L"0";
|
||||
}
|
||||
const DurationUnit unit = ChosenDurationUnit(seconds);
|
||||
return FormatValue(std::fabs(seconds) / unit.scale);
|
||||
return FormatValue(std::fabs(seconds));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -680,15 +774,37 @@ namespace jdi
|
||||
void PlotView::SetHint(const std::wstring& text)
|
||||
{
|
||||
m_hintText = text;
|
||||
m_hintSticky = false;
|
||||
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
|
||||
{
|
||||
if (m_hintText.empty() || m_hintShownAt == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_hintSticky)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const std::uint64_t elapsed = GetTickCount64() - m_hintShownAt;
|
||||
return elapsed < (kHintHoldMs + kHintFadeMs);
|
||||
}
|
||||
@@ -729,11 +845,13 @@ namespace jdi
|
||||
|
||||
std::uint32_t PlotView::CursorColor(int which) const
|
||||
{
|
||||
// 两条光标统一使用同一样式与颜色(与光标 1 一致),不再按序号区分配色。
|
||||
(void)which;
|
||||
if (m_theme == nullptr)
|
||||
{
|
||||
return 0xFF000000u;
|
||||
}
|
||||
return (which == 2) ? m_theme->cursorAlt : m_theme->cursor;
|
||||
return m_theme->cursor;
|
||||
}
|
||||
|
||||
bool PlotView::CancelDrag(bool showHint)
|
||||
@@ -936,7 +1054,7 @@ namespace jdi
|
||||
m_rubberRect = gd::RectF();
|
||||
SetHint(Tab().ZoomMode() == PlotZoomMode::NONE
|
||||
? std::wstring()
|
||||
: L"选区放大:横向拖动放大时间轴,纵向拖动放大 Y 轴;右键放弃当前选区,Esc 退出");
|
||||
: L"横向或纵向拖动以放大选择区域,ESC退出选区模式");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -962,18 +1080,40 @@ namespace jdi
|
||||
return gd::RectF();
|
||||
}
|
||||
|
||||
const double time = (which == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
|
||||
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;
|
||||
// 未经叠放处理的"原始"位置:单条标签独占底部一行。
|
||||
const auto rawRect = [this](int index) -> gd::RectF
|
||||
{
|
||||
const double time = (index == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
|
||||
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;
|
||||
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;
|
||||
// 时间读数改到绘图区**底部**:顶部留给网格图例与幅值标尺,避免互相遮挡。
|
||||
float left = CursorDisplayX(index) - width * 0.5f;
|
||||
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;
|
||||
|
||||
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
|
||||
@@ -1002,6 +1142,40 @@ namespace jdi
|
||||
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)
|
||||
{
|
||||
if (m_plotRect.Width <= 0.0f)
|
||||
@@ -1503,6 +1677,17 @@ namespace jdi
|
||||
SignalRef best;
|
||||
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)
|
||||
{
|
||||
@@ -1544,8 +1729,33 @@ namespace jdi
|
||||
}
|
||||
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;
|
||||
}
|
||||
// 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)
|
||||
@@ -1785,9 +1995,9 @@ namespace jdi
|
||||
|
||||
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 bool secondIsBack = (Tab().CursorTimeAlt() >= Tab().CursorTime());
|
||||
const double moved = front + span;
|
||||
@@ -1998,8 +2208,12 @@ namespace jdi
|
||||
case PlotDragMode::SCROLL_LANES:
|
||||
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||
case PlotDragMode::CURSOR_MOVE:
|
||||
// 从时间读数标签上抓起的拖拽统一用抓握(移动)图标;
|
||||
// 直接从光标线上抓起的拖拽保持左右箭头。
|
||||
return LoadCursorW(nullptr, m_dragOnLabel ? IDC_SIZEALL : IDC_SIZEWE);
|
||||
case PlotDragMode::CURSOR_SPAN_MOVE:
|
||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||
// 差值标签拖拽:统一使用抓握图标(不是左右箭头)。
|
||||
return LoadCursorW(nullptr, IDC_SIZEALL);
|
||||
case PlotDragMode::MOVE_SIGNAL:
|
||||
return LoadCursorW(nullptr, IDC_SIZEALL);
|
||||
default:
|
||||
@@ -2012,11 +2226,19 @@ namespace jdi
|
||||
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||
}
|
||||
|
||||
// 双光标差值标签可拖拽。
|
||||
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))
|
||||
{
|
||||
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_dragMoved = true;
|
||||
// 拖动过程中维持显示"右键取消选区"说明(粘滞,直到松开左键后才淡出)。
|
||||
SetHintSticky(L"右键取消选区,ESC退出选区模式");
|
||||
return true;
|
||||
}
|
||||
case PlotDragMode::SCROLL_LANES:
|
||||
@@ -2149,8 +2373,8 @@ namespace jdi
|
||||
}
|
||||
case PlotDragMode::CURSOR_MOVE:
|
||||
{
|
||||
// 放大到采样点可分辨时吸附到最近的数据点,便于精确对比取值。
|
||||
double target = XToTime(fx);
|
||||
// 从时间读数标签上抓起时保留横向抓取偏移,避免一按下光标就跳到鼠标中心。
|
||||
double target = XToTime(fx - static_cast<float>(m_cursorGrabOffset));
|
||||
double snapped = target;
|
||||
if (SnapTimeToSample(target, snapped))
|
||||
{
|
||||
@@ -2232,9 +2456,24 @@ namespace jdi
|
||||
}
|
||||
if (hoverSignal != m_hoverSignal)
|
||||
{
|
||||
if (hoverSignal.IsValid())
|
||||
{
|
||||
// 悬停提示固定在"刚刚进入可拖拽区域"的位置:之后鼠标即使继续在该区域内
|
||||
// 移动,提示也只停留在原地(不再跟随鼠标)。
|
||||
m_hoverAnchorX = x;
|
||||
m_hoverAnchorY = y;
|
||||
}
|
||||
m_hoverSignal = hoverSignal;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 双光标差值标签的悬停状态(用于区分"抓握/悬停"光标与普通状态)。
|
||||
const bool spanHover = IsOnCursorSpanLabel(x, y);
|
||||
if (spanHover != m_spanHover)
|
||||
{
|
||||
m_spanHover = spanHover;
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -2255,7 +2494,9 @@ namespace jdi
|
||||
m_dragMoved = false;
|
||||
m_rubberRect = gd::RectF();
|
||||
m_rubberAxis = PlotRubberAxis::UNDECIDED;
|
||||
// 新的交互开始:清掉上一次留下的提示(选区放大会在下面重新设置自己的提示)。
|
||||
m_dragOnLabel = false;
|
||||
m_cursorGrabOffset = 0;
|
||||
// 新的交互开始:清掉上一次留下的提示(拖动过程中会重新设置粘滞提示)。
|
||||
SetHint(std::wstring());
|
||||
|
||||
if (m_scrollBarVisible &&
|
||||
@@ -2276,16 +2517,31 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 双光标模式下先检查差值标签:它可拖拽(两条光标整体平移,间隔不变)。
|
||||
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && m_cursorSpanRect.Width > 0.0f &&
|
||||
UiPointInRect(m_cursorSpanRect, fx, fy))
|
||||
if (IsOnCursorSpanLabel(x, y))
|
||||
{
|
||||
m_dragMode = PlotDragMode::CURSOR_SPAN_MOVE;
|
||||
m_dragOnLabel = true;
|
||||
m_spanDragStartX = x;
|
||||
m_spanDragTime1 = Tab().CursorTime();
|
||||
m_spanDragTime2 = Tab().CursorTimeAlt();
|
||||
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;
|
||||
@@ -2310,10 +2566,10 @@ namespace jdi
|
||||
if (mode == PlotZoomMode::SELECT)
|
||||
{
|
||||
// 选区放大:轴向由鼠标移动方向决定,因此按下时先记录网格并置为未定。
|
||||
// 按下本身**不显示提示**(拖动开始后才显示"右键取消选区")。
|
||||
m_dragLane = LaneAtY(y);
|
||||
m_rubberAxis = PlotRubberAxis::UNDECIDED;
|
||||
m_dragMode = PlotDragMode::RUBBER_SELECT;
|
||||
SetHint(L"横向拖动放大时间轴,纵向拖动放大 Y 轴;右键放弃当前选区,Esc 退出");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2402,6 +2658,10 @@ namespace jdi
|
||||
m_dragLane = -1;
|
||||
m_dragMoved = false;
|
||||
m_rubberRect = gd::RectF();
|
||||
m_dragOnLabel = false;
|
||||
m_cursorGrabOffset = 0;
|
||||
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。
|
||||
ReleaseHint();
|
||||
(void)x;
|
||||
return changed;
|
||||
}
|
||||
@@ -2445,12 +2705,28 @@ namespace jdi
|
||||
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, FormatDurationNumber(CursorSpan()));
|
||||
return BeginEdit(PlotEditTarget::CURSOR_SPAN, -1, FormatDurationSeconds(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;
|
||||
changed = true;
|
||||
}
|
||||
if (m_spanHover)
|
||||
{
|
||||
m_spanHover = false;
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -3306,12 +3587,13 @@ namespace jdi
|
||||
}
|
||||
|
||||
const std::uint64_t elapsed = GetTickCount64() - m_hintShownAt;
|
||||
if (elapsed >= (kHintHoldMs + kHintFadeMs))
|
||||
// 粘滞状态下不淡出(用于"拖动过程中维持显示")。
|
||||
if (!m_hintSticky && elapsed >= (kHintHoldMs + kHintFadeMs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 停留结束后在 kHintFadeMs 内线性淡出(颜色 alpha 按比例缩放)。
|
||||
const double fade = (elapsed <= kHintHoldMs)
|
||||
const double fade = (m_hintSticky || elapsed <= kHintHoldMs)
|
||||
? 1.0
|
||||
: (1.0 - static_cast<double>(elapsed - kHintHoldMs) /
|
||||
static_cast<double>(kHintFadeMs));
|
||||
@@ -3865,7 +4147,7 @@ namespace jdi
|
||||
return gd::RectF();
|
||||
}
|
||||
|
||||
const std::wstring text = FormatDuration(CursorSpan());
|
||||
const std::wstring text = FormatSpanText(CursorSpan());
|
||||
const float width = std::max(
|
||||
72.0f * m_scale, UiMeasureTextWidth(m_fonts->monoSmall, text) + 18.0f * m_scale);
|
||||
const float height = 20.0f * m_scale;
|
||||
@@ -3913,6 +4195,28 @@ namespace jdi
|
||||
const bool doubleMode = (Tab().CursorMode() == PlotCursorMode::DOUBLE);
|
||||
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 };
|
||||
|
||||
@@ -4048,7 +4352,69 @@ namespace jdi
|
||||
float labelTop = y - textHeight * 0.5f;
|
||||
labelTop = std::max(clipTop + 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));
|
||||
const bool dark = IsDarkColor(signalColor);
|
||||
@@ -4094,11 +4460,14 @@ namespace jdi
|
||||
m_cursorSpanRect = spanRect;
|
||||
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));
|
||||
UiDrawRoundRect(g, spanRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
|
||||
UiText(g, FormatDuration(CursorSpan()), m_fonts->monoSmall, spanRect,
|
||||
m_theme->textStrong, TextAlign::CENTER);
|
||||
// 样式与光标时间读数完全一致(同底色/同描边/同文字配色)。
|
||||
const std::uint32_t color = CursorColor(1);
|
||||
UiFillRoundRect(g, spanRect, 3.0f * m_scale,
|
||||
m_spanHover ? color : WithAlpha(color, 0xE0));
|
||||
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
|
||||
@@ -4250,6 +4619,13 @@ namespace jdi
|
||||
// 悬停提示:光标贴近曲线时给出可拖拽说明(与时间光标的提示方式一致)。
|
||||
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;
|
||||
}
|
||||
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 std::wstring text = resolved.data->Title() + L" · 按住拖拽可移入其它网格";
|
||||
|
||||
// 提示锚定在"首次进入可拖拽区域"的位置,鼠标在该区域内继续移动时提示不会跟随。
|
||||
const float textWidth = UiTextWidth(g, text, m_fonts->uiSmall) + 16.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)
|
||||
{
|
||||
left = m_plotRect.X + m_plotRect.Width - textWidth;
|
||||
}
|
||||
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::max(top, lane.rect.Y + 2.0f * m_scale);
|
||||
|
||||
|
||||
@@ -64,6 +64,24 @@ namespace jdi
|
||||
*/
|
||||
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 计算工具栏在指定缩放下的期望高度。
|
||||
* @return 期望高度(像素)。
|
||||
@@ -150,6 +168,7 @@ namespace jdi
|
||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||
bool m_documentLoaded = false; /**< 是否已加载数据。 */
|
||||
std::vector<Button> m_buttons; /**< 按钮集合。 */
|
||||
std::vector<std::pair<ToolbarAction, std::wstring>> m_labelOverrides; /**< 动态文本覆盖表。 */
|
||||
gd::RectF m_area; /**< 工具栏整体区域。 */
|
||||
int m_hoverIndex = -1; /**< 当前悬停按钮下标。 */
|
||||
int m_pressedIndex = -1; /**< 当前按下按钮下标。 */
|
||||
|
||||
@@ -94,6 +94,17 @@ namespace jdi
|
||||
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));
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
if (m_theme == nullptr || m_fonts == nullptr)
|
||||
|
||||
@@ -102,8 +102,7 @@ namespace jdi
|
||||
std::uint32_t gridWeak = 0; /**< 次级网格线颜色。 */
|
||||
std::uint32_t gridStrong = 0; /**< 主要网格线颜色。 */
|
||||
std::uint32_t axisLine = 0; /**< 坐标轴线颜色。 */
|
||||
std::uint32_t cursor = 0; /**< 时间光标 1 的颜色。 */
|
||||
std::uint32_t cursorAlt = 0; /**< 时间光标 2 的颜色(与光标 1 区分且同一色系)。 */
|
||||
std::uint32_t cursor = 0; /**< 时间光标颜色(单/双光标共用同一样式)。 */
|
||||
std::uint32_t readoutBg = 0; /**< 数值读数底色。 */
|
||||
std::uint32_t danger = 0; /**< 警示色。 */
|
||||
std::uint32_t warning = 0; /**< 提示色。 */
|
||||
|
||||
@@ -62,8 +62,6 @@ namespace jdi
|
||||
theme.gridStrong = MakeArgb(0xC3B896u);
|
||||
theme.axisLine = MakeArgb(kBase1);
|
||||
theme.cursor = MakeArgb(kOrange);
|
||||
// 第二光标用 Solarized 洋红:与橙色同族暖色但更鲜艳,对比清晰又不刺眼。
|
||||
theme.cursorAlt = MakeArgb(kMagenta);
|
||||
theme.readoutBg = MakeArgb(0xF6EFDCu);
|
||||
theme.danger = MakeArgb(kRed);
|
||||
theme.warning = MakeArgb(kYellow);
|
||||
|
||||
Reference in new issue
Block a user