feat(ui): 双光标样式统一与配色、光标滞留视野外、差值标签与拖拽、选区放大交互与提示
1) 光标 2 的样式与光标 1 完全一致,并改用更鲜艳的颜色 - 原先光标 2 用朝上的三角把手,观感突兀;现两条光标统一为同款朝下三角 - 主题新增 cursorAlt 并改用 Solarized 洋红 #D33682(原青色偏灰),与光标 1 的 橙色 #CB4B16 同属暖色且更醒目,靠颜色而非形状区分 2) 光标允许滞留在网格可视区域之外(不再被强行夹回视野) - 删除"视图变化后夹取光标"的逻辑(时间轴选区放大、适应视图、按网格时间轴展开、 平移时都不再移动光标);仅在拖拽光标本身时才使用 ClampCursorToView(which) - 新增 CursorDisplayX(which):光标在视野外时把它的显示位置"钉"在最近的网格边缘, 取值标注与时间读数因此始终可见;数据点改为边缘三角指示(指向光标实际方向) - 命中测试改用"显示横坐标",因此被钉在边缘的光标仍可抓住拖回 3) 光标读数增加细描边,避免与背景/曲线颜色过于接近 - 新增 DrawLabelOutline():按文字颜色取反色做 1px 半透明描边, 网格内的取值胶囊、光标时间读数、差值标签统一使用 4) 双光标差值显示(时间轴上方居中,自适应单位,可拖拽、可编辑) - 新增 FormatDuration()/FormatDurationNumber()/ChosenDurationUnit(): 在 h / min / s / ms / us / ns / ps 之间自动选择单位(数值落在 [1,1000)) - 差值标签位于两条时间读数的中点,样式与时间读数一致 - 双击进入编辑:输入值采用**当前显示单位**;确认后**前方(时间较小)光标不动**, 后方光标移动到"前方 + 输入差值" - 标签支持左右拖拽(新增 PlotDragMode::CURSOR_SPAN_MOVE):两条光标整体平移、间隔不变 5) 双光标内容重叠时的层次与差值标签位置 - 新增 m_activeCursor:最后操作过(拖拽/单击/编辑)的光标其内容绘制在上层, 两条光标的竖线、取值胶囊、时间读数都按此顺序绘制 - 差值标签改为按**两条时间读数的实际净空**判断:放不下时自动落到主时间轴内部 (原先按光标线间距判断,光标被钉到边缘时会用差值标签盖住时间读数) 6) 选区放大的光标形状与颜色 - 未开始框选时不再改变鼠标形状(保持箭头);按下但未拖动时为十字 IDC_CROSS; 确定为时间轴选区后为左右箭头、Y 轴选区后为上下箭头 - 两种方向的选区统一使用强调色(原先 Y 轴选区用警示红,观感不一致) - 新增 Application::UpdateDragCursor():Windows 在鼠标捕获期间不派发 WM_SETCURSOR, 必须在 WM_MOUSEMOVE / WM_LBUTTONDOWN 后主动刷新光标形状,否则形状不会变化 7) 提示浮层加大并支持"停留后淡出",光标模式不再提示 - 提示字号由 uiSmall 改为 ui,胶囊 30px 高、内边距更大 - 新增停留 1300ms + 淡出 500ms 的时间控制;宿主用 WM_TIMER(33ms) 驱动重绘, 淡出结束自动 KillTimer(文字走 GDI 不支持 alpha,改为向胶囊底色混色实现淡出) - 「光标」三段式切换不再显示任何提示 验证([1].CSV 11 信号,窗口 1725x996,125% DPI): - 双光标:橙 0.63 / 洋红 0.7812,同款把手;底部时间读数 + 居中差值 "151.2 ms" - 放大到不含光标的区间:光标不移动,取值以边缘三角 + 胶囊贴边显示 - 拖拽差值标签:两光标从 0.63/0.7812 整体平移到 0.424202/0.575402,差值仍为 151.2 ms - 差值编辑:输入 300(显示单位为 ms)→ 前方光标保持 0.63,后方移动到 0.93 - 重叠层次:差值设为 0 时洋红(活动)在上;单击光标线后橙色(活动)在上 - 光标形状:未框选 ARROW / 按下 CROSS / 横向 SIZEWE / 纵向 SIZENS(cursor.ps1 实测) - 提示浮层:出现后约 1.3s 开始淡出、约 1.8s 消失;右键放弃选区后视图不变且模式保持选中 - 空闲 CPU 0.00%;make clean; make all 无警告无错误
This commit is contained in:
1 parent
388d3b6441
commit
eaf5b9cadf
5 files changed
+500
-117
No files matched your search
@@ -301,6 +301,17 @@ namespace jdi
|
||||
*/
|
||||
bool HandleMouseMove(int x, int y, bool buttonDown);
|
||||
|
||||
/**
|
||||
* @brief 拖动(鼠标捕获)期间主动刷新光标形状。
|
||||
*
|
||||
* Windows 在鼠标被捕获时不再派发 `WM_SETCURSOR`,若不主动设置,
|
||||
* 光标会一直停留在按下瞬间的形状(例如选区放大时看不到十字/方向箭头)。
|
||||
*
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
*/
|
||||
void UpdateDragCursor(int x, int y);
|
||||
|
||||
HINSTANCE m_instance = nullptr; /**< 模块实例句柄。 */
|
||||
HWND m_hwnd = nullptr; /**< 主窗口句柄。 */
|
||||
UINT m_dpi = 96; /**< 当前 DPI。 */
|
||||
@@ -339,6 +350,7 @@ namespace jdi
|
||||
|
||||
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
|
||||
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
|
||||
bool m_hintTimerActive = false; /**< 提示浮层淡出定时器是否已启动。 */
|
||||
int m_tabMenuIndex = -1; /**< 标签栏右键菜单的目标标签页下标。 */
|
||||
int m_plotMenuLane = -1; /**< 绘图区右键菜单的目标网格下标。 */
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace jdi
|
||||
const int kMinClientWidth = 860; /**< 窗口最小客户区宽度。 */
|
||||
const int kMinClientHeight = 540; /**< 窗口最小客户区高度。 */
|
||||
const UINT kMessageRemoveFile = WM_APP + 1; /**< 面板请求移除文件的自定义消息。 */
|
||||
const UINT_PTR kHintTimerId = 1; /**< 提示浮层淡出用的定时器 ID。 */
|
||||
const UINT kHintTimerPeriodMs = 33; /**< 提示浮层淡出的重绘间隔(约 30fps)。 */
|
||||
|
||||
/** @brief 标签页右键菜单命令 ID。 */
|
||||
const int kMenuTabNew = 1;
|
||||
@@ -716,6 +718,21 @@ namespace jdi
|
||||
BitBlt(windowDc, 0, 0, width, height, m_backDc, 0, 0, SRCCOPY);
|
||||
ReleaseDC(m_hwnd, windowDc);
|
||||
}
|
||||
|
||||
// 提示浮层需要停留后淡出:仅在其仍可见时保持定时重绘,淡出结束即停表。
|
||||
if (m_plot.HintNeedsAnimation())
|
||||
{
|
||||
if (m_hintTimerActive == false)
|
||||
{
|
||||
SetTimer(m_hwnd, kHintTimerId, kHintTimerPeriodMs, nullptr);
|
||||
m_hintTimerActive = true;
|
||||
}
|
||||
}
|
||||
else if (m_hintTimerActive)
|
||||
{
|
||||
KillTimer(m_hwnd, kHintTimerId);
|
||||
m_hintTimerActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Application::SetStatusMessage(const std::wstring& text, bool isError)
|
||||
@@ -1148,6 +1165,17 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
void Application::UpdateDragCursor(int x, int y)
|
||||
{
|
||||
// 鼠标捕获期间不再有 WM_SETCURSOR,只能自己按当前状态设置光标。
|
||||
HCURSOR cursor = nullptr;
|
||||
if (m_activeTarget == Target::PLOT)
|
||||
{
|
||||
cursor = m_plot.QueryCursor(x, y);
|
||||
}
|
||||
SetCursor(cursor != nullptr ? cursor : LoadCursorW(nullptr, IDC_ARROW));
|
||||
}
|
||||
|
||||
bool Application::HandleMouseMove(int x, int y, bool buttonDown)
|
||||
{
|
||||
if (buttonDown && m_activeTarget != Target::NONE)
|
||||
@@ -1820,6 +1848,12 @@ namespace jdi
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
// 拖动(鼠标捕获)期间 Windows 不会派发 WM_SETCURSOR,
|
||||
// 因此需要主动刷新一次,否则光标形状会一直停在按下的状态。
|
||||
if (buttonDown)
|
||||
{
|
||||
UpdateDragCursor(x, y);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSELEAVE:
|
||||
@@ -1916,6 +1950,9 @@ namespace jdi
|
||||
ApplyPendingRename();
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
// 拖动期间 Windows 不再派发 WM_SETCURSOR,需在按下后立即刷新一次,
|
||||
// 使"按下但尚未拖动"时能立刻看到十字/方向箭头提示。
|
||||
UpdateDragCursor(x, y);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2225,6 +2262,16 @@ namespace jdi
|
||||
RemoveFile(static_cast<int>(wParam));
|
||||
return 0;
|
||||
}
|
||||
case WM_TIMER:
|
||||
{
|
||||
// 提示浮层淡出:仅重绘,不再做其它处理;Render 会在淡出结束后停表。
|
||||
if (wParam == kHintTimerId)
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_DROPFILES:
|
||||
{
|
||||
HDROP drop = reinterpret_cast<HDROP>(wParam);
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace jdi
|
||||
RUBBER_SELECT, /**< 选区放大:按拖拽方向决定横向(时间轴)或纵向(幅值轴)选区。 */
|
||||
SCROLL_LANES, /**< 拖动网格滚动条。 */
|
||||
CURSOR_MOVE, /**< 拖拽时间光标。 */
|
||||
CURSOR_SPAN_MOVE, /**< 拖拽双光标差值标签:两条光标整体平移,间隔不变。 */
|
||||
MOVE_SIGNAL /**< 拖拽信号曲线到其它网格。 */
|
||||
};
|
||||
|
||||
@@ -46,6 +47,7 @@ namespace jdi
|
||||
{
|
||||
NONE = 0, /**< 未处于输入状态。 */
|
||||
CURSOR_TIME, /**< 编辑时间光标位置。 */
|
||||
CURSOR_SPAN, /**< 编辑双光标之间的时间差(前方光标不动,后方光标随之移动)。 */
|
||||
LANE_Y_MAX, /**< 编辑某网格的 Y 轴最大值。 */
|
||||
LANE_Y_MIN, /**< 编辑某网格的 Y 轴最小值。 */
|
||||
SIGNAL_NAME /**< 编辑图例中某信号的显示名称。 */
|
||||
@@ -257,6 +259,15 @@ namespace jdi
|
||||
*/
|
||||
float LaneContentHeight() const { return m_laneContentHeight; }
|
||||
|
||||
/**
|
||||
* @brief 提示浮层是否仍在显示(含淡出阶段)。
|
||||
*
|
||||
* 宿主可在返回 true 时保持定时重绘,使提示能"停留后淡出"。
|
||||
*
|
||||
* @return 需要继续重绘返回 true。
|
||||
*/
|
||||
bool HintNeedsAnimation() const;
|
||||
|
||||
/**
|
||||
* @brief 清除已启用的时间光标。
|
||||
*/
|
||||
@@ -641,6 +652,13 @@ namespace jdi
|
||||
*/
|
||||
float CursorX(int which) const;
|
||||
|
||||
/**
|
||||
* @brief 计算指定光标在屏幕上的**显示**横坐标(滞留在可视区外时贴到网格边缘)。
|
||||
* @param [IN] which 光标序号(1 或 2)。
|
||||
* @return 显示横坐标。
|
||||
*/
|
||||
float CursorDisplayX(int which) const;
|
||||
|
||||
/**
|
||||
* @brief 计算指定光标时间读数标签的矩形。
|
||||
* @param [IN] which 光标序号(1 或 2)。
|
||||
@@ -659,11 +677,25 @@ namespace jdi
|
||||
/**
|
||||
* @brief 将两条光标的时间夹取到当前可见时间范围内。
|
||||
*
|
||||
* 缩放/平移/自适应会改变可见区间,若不夹取,光标会落到视图之外并“消失”,
|
||||
* 表现为“放大后无法再拖动光标”。
|
||||
* ⚠️ 现在**只在拖拽光标时**使用。视图变化(平移/放大/自适应)必须**不**移动光标:
|
||||
* 光标允许滞留在网格可视区域之外,此时仅把它的取值/时间读数贴在网格边缘显示。
|
||||
*
|
||||
* @param [IN] which 要夹取的光标序号(1 或 2)。
|
||||
*/
|
||||
void ClampCursorToView();
|
||||
void ClampCursorToView(int which);
|
||||
|
||||
/**
|
||||
* @brief 计算双光标之间的时间差(绝对值)。
|
||||
* @return 时间差;非双光标模式返回 0。
|
||||
*/
|
||||
double CursorSpan() const;
|
||||
|
||||
/**
|
||||
* @brief 计算双光标差值标签的矩形(时间轴上方、两条时间读数的中间位置)。
|
||||
* @param [OUT] insideAxis 为 true 时表示两条光标过近,标签落在主时间轴内部。
|
||||
* @return 标签矩形;非双光标模式返回空矩形。
|
||||
*/
|
||||
gd::RectF CursorSpanRect(bool& insideAxis) const;
|
||||
/**
|
||||
* @brief 获取指定光标对应的显示颜色。
|
||||
* @param [IN] which 光标序号(1 或 2)。
|
||||
@@ -672,11 +704,19 @@ namespace jdi
|
||||
std::uint32_t CursorColor(int which) const;
|
||||
|
||||
/**
|
||||
* @brief 设置并显示一条临时提示(在绘图区底部以浮层方式绘制)。
|
||||
* @param [IN] text 提示文本;传空串则清除提示。
|
||||
* @brief 设置并显示一条临时提示(在绘图区底部以浮层方式绘制,短暂停留后淡出)。
|
||||
* @param [IN] text 提示文本;传空串则立即清除提示。
|
||||
*/
|
||||
void SetHint(const std::wstring& text);
|
||||
|
||||
/**
|
||||
* @brief 画文字时给底色胶囊加一层与文字反色的细描边,提升与背景/曲线的对比度。
|
||||
* @param [IN] rect 胶囊矩形。
|
||||
* @param [IN] radius 圆角半径。
|
||||
* @param [IN] textDark 文字是否用深色。
|
||||
*/
|
||||
void DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius, bool textDark);
|
||||
|
||||
/**
|
||||
* @brief 精确检测鼠标位置是否命中某条可见信号曲线。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
@@ -837,7 +877,12 @@ namespace jdi
|
||||
int m_dragMouseY = 0; /**< 拖拽过程中鼠标的当前纵坐标(虚影跟随)。 */
|
||||
bool m_cursorHover = false; /**< 鼠标是否悬停在某条时间光标上。 */
|
||||
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;
|
||||
PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */
|
||||
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
|
||||
int m_editLane = -1; /**< 内联输入所作用的网格下标。 */
|
||||
@@ -851,6 +896,8 @@ namespace jdi
|
||||
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
|
||||
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
|
||||
std::wstring m_hintText; /**< 临时提示文本(空表示不显示)。 */
|
||||
std::uint64_t m_hintShownAt = 0; /**< 提示出现的时刻(毫秒,GetTickCount64)。 */
|
||||
gd::RectF m_cursorSpanRect; /**< 最近一次绘制时差值标签的矩形(用于命中拖拽/双击)。 */
|
||||
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,6 +44,90 @@ namespace jdi
|
||||
const float kLegendSwatchSize = 8.0f; /**< 图例色块尺寸。 */
|
||||
const float kLegendItemGap = 14.0f; /**< 图例条目之间的间距。 */
|
||||
const float kLegendMaxTextWidth = 200.0f; /**< 图例条目的文本宽度上限(超出则省略)。 */
|
||||
const std::uint64_t kHintHoldMs = 1300; /**< 提示浮层完全显示的停留时长(毫秒)。 */
|
||||
const std::uint64_t kHintFadeMs = 500; /**< 提示浮层的淡出时长(毫秒)。 */
|
||||
|
||||
/**
|
||||
* @brief 时间间隔的显示单位。
|
||||
*/
|
||||
struct DurationUnit
|
||||
{
|
||||
double scale = 1.0; /**< 1 个该单位等于多少秒。 */
|
||||
const wchar_t* name = L"s"; /**< 单位名称。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @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 把时间间隔格式化为自适应单位的可读文本(含单位)。
|
||||
*
|
||||
* 在 h / min / s / ms / us / ns / ps 之间自动选择最合适的单位。
|
||||
*
|
||||
* @param [IN] seconds 时间间隔(秒)。
|
||||
* @return 形如 "12.3 ms" 的文本。
|
||||
*/
|
||||
std::wstring FormatDuration(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;
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 把时间间隔格式化为"当前显示单位下的纯数值"(不含单位),用于内联编辑。
|
||||
* @param [IN] seconds 时间间隔(秒)。
|
||||
* @return 形如 "12.3" 的文本。
|
||||
*/
|
||||
std::wstring FormatDurationNumber(double seconds)
|
||||
{
|
||||
if (!std::isfinite(seconds))
|
||||
{
|
||||
return L"0";
|
||||
}
|
||||
const DurationUnit unit = ChosenDurationUnit(seconds);
|
||||
return FormatValue(std::fabs(seconds) / unit.scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按比例缩放颜色的 alpha 通道(用于提示浮层淡出)。
|
||||
* @param [IN] argb 原始 0xAARRGGBB 颜色。
|
||||
* @param [IN] alpha 新的 alpha(0-255)。
|
||||
* @return 缩放后的颜色。
|
||||
*/
|
||||
std::uint32_t ScaleAlpha(std::uint32_t argb, unsigned int alpha)
|
||||
{
|
||||
const unsigned int a = std::min(255u, alpha);
|
||||
return (argb & 0x00FFFFFFu) | (a << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 判断颜色是否偏暗(用于在信号色底板上选择可读的文字颜色)。
|
||||
@@ -524,7 +608,7 @@ namespace jdi
|
||||
lane.autoRange = true;
|
||||
}
|
||||
}
|
||||
ClampCursorToView();
|
||||
// 适应视图已回到初始状态,这里不再移动光标(光标允许停留在可视区外)。
|
||||
BuildLaneVisuals();
|
||||
}
|
||||
|
||||
@@ -533,11 +617,11 @@ namespace jdi
|
||||
Tab().CursorMode() = PlotCursorMode::NONE;
|
||||
m_cursorHover = false;
|
||||
m_hoverCursor = 0;
|
||||
if (m_editTarget == PlotEditTarget::CURSOR_TIME)
|
||||
if (m_editTarget == PlotEditTarget::CURSOR_TIME || m_editTarget == PlotEditTarget::CURSOR_SPAN)
|
||||
{
|
||||
EndEdit(false);
|
||||
}
|
||||
if (m_dragMode == PlotDragMode::CURSOR_MOVE)
|
||||
if (m_dragMode == PlotDragMode::CURSOR_MOVE || m_dragMode == PlotDragMode::CURSOR_SPAN_MOVE)
|
||||
{
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
}
|
||||
@@ -545,7 +629,7 @@ namespace jdi
|
||||
|
||||
bool PlotView::CycleCursorMode()
|
||||
{
|
||||
// 三态循环:无 → 单光标 → 双光标 → 无。
|
||||
// 三态循环:无 → 单光标 → 双光标 → 无。光标模式本身不显示提示。
|
||||
switch (Tab().CursorMode())
|
||||
{
|
||||
case PlotCursorMode::NONE:
|
||||
@@ -557,7 +641,7 @@ namespace jdi
|
||||
Tab().CursorTimeAlt() = center;
|
||||
m_cursorHover = true;
|
||||
m_hoverCursor = 1;
|
||||
SetHint(L"单光标:拖动竖线移动,双击竖线可输入精确时间");
|
||||
m_activeCursor = 1;
|
||||
break;
|
||||
}
|
||||
case PlotCursorMode::SINGLE:
|
||||
@@ -568,7 +652,7 @@ namespace jdi
|
||||
const double low = Tab().ViewMin();
|
||||
const double high = Tab().ViewMax();
|
||||
const double span = (high > low) ? (high - low) : 1.0;
|
||||
double offset = span * 0.12;
|
||||
const double offset = span * 0.12;
|
||||
double second = Tab().CursorTime() + offset;
|
||||
if (second > high - span * 0.02)
|
||||
{
|
||||
@@ -579,16 +663,15 @@ namespace jdi
|
||||
second = low + span * 0.5;
|
||||
}
|
||||
Tab().CursorTimeAlt() = second;
|
||||
ClampCursorToView();
|
||||
ClampCursorToView(2);
|
||||
m_cursorHover = true;
|
||||
m_hoverCursor = 2;
|
||||
SetHint(L"双光标:右侧光标显示当前值与 ±增量(右键光标可取消当前光标)");
|
||||
m_activeCursor = 2;
|
||||
break;
|
||||
}
|
||||
case PlotCursorMode::DOUBLE:
|
||||
default:
|
||||
DisableCursor();
|
||||
SetHint(std::wstring());
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
@@ -597,9 +680,29 @@ namespace jdi
|
||||
void PlotView::SetHint(const std::wstring& text)
|
||||
{
|
||||
m_hintText = text;
|
||||
m_hintShownAt = (text.empty()) ? 0 : GetTickCount64();
|
||||
}
|
||||
|
||||
void PlotView::ClampCursorToView()
|
||||
bool PlotView::HintNeedsAnimation() const
|
||||
{
|
||||
if (m_hintText.empty() || m_hintShownAt == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::uint64_t elapsed = GetTickCount64() - m_hintShownAt;
|
||||
return elapsed < (kHintHoldMs + kHintFadeMs);
|
||||
}
|
||||
|
||||
double PlotView::CursorSpan() const
|
||||
{
|
||||
if (m_tab == nullptr || Tab().CursorMode() != PlotCursorMode::DOUBLE)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
return std::fabs(Tab().CursorTimeAlt() - Tab().CursorTime());
|
||||
}
|
||||
|
||||
void PlotView::ClampCursorToView(int which)
|
||||
{
|
||||
if (m_tab == nullptr || m_plotRect.Width <= 0.0f)
|
||||
{
|
||||
@@ -611,10 +714,17 @@ namespace jdi
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 不夹到边界上,留一点内缩,保证光标线及把手完整可见。
|
||||
// 只在拖拽时夹取;留一点内缩,保证光标线及把手完整可见。
|
||||
// 视图变化(平移/缩放/自适应)**不**调用本函数:光标允许滞留在网格可视区域之外。
|
||||
const double inset = (high - low) * 0.002;
|
||||
Tab().CursorTime() = ClampDouble(Tab().CursorTime(), low + inset, high - inset);
|
||||
Tab().CursorTimeAlt() = ClampDouble(Tab().CursorTimeAlt(), low + inset, high - inset);
|
||||
if (which == 2)
|
||||
{
|
||||
Tab().CursorTimeAlt() = ClampDouble(Tab().CursorTimeAlt(), low + inset, high - inset);
|
||||
}
|
||||
else
|
||||
{
|
||||
Tab().CursorTime() = ClampDouble(Tab().CursorTime(), low + inset, high - inset);
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t PlotView::CursorColor(int which) const
|
||||
@@ -718,7 +828,7 @@ namespace jdi
|
||||
// 否则展开后会看到“时间对了但幅值区间不合适”的波形。
|
||||
AutoRangeLaneToView(laneIndex);
|
||||
|
||||
ClampCursorToView();
|
||||
// 注意:不在这里移动光标——光标允许滞留在网格可视区域之外。
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
@@ -836,6 +946,15 @@ namespace jdi
|
||||
return TimeToX(time);
|
||||
}
|
||||
|
||||
float PlotView::CursorDisplayX(int which) const
|
||||
{
|
||||
// 光标允许滞留在可视区之外:此时把它"钉"在最近的网格边缘上显示,
|
||||
// 保证它的取值与时间读数始终可见。
|
||||
const float raw = CursorX(which);
|
||||
return std::max(m_plotRect.X + 1.0f * m_scale,
|
||||
std::min(raw, m_plotRect.X + m_plotRect.Width - 1.0f * m_scale));
|
||||
}
|
||||
|
||||
gd::RectF PlotView::CursorLabelRect(int which) const
|
||||
{
|
||||
if (m_fonts == nullptr || m_fonts->monoSmall == nullptr)
|
||||
@@ -850,7 +969,7 @@ namespace jdi
|
||||
const float height = 20.0f * m_scale;
|
||||
|
||||
// 时间读数改到绘图区**底部**:顶部留给网格图例与幅值标尺,避免互相遮挡。
|
||||
float left = CursorX(which) - width * 0.5f;
|
||||
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;
|
||||
|
||||
@@ -869,12 +988,13 @@ namespace jdi
|
||||
const float fx = static_cast<float>(x);
|
||||
|
||||
// 双光标重叠时优先命中光标 1,避免出现“抓不起来第二条”的情况。
|
||||
if (std::fabs(fx - CursorX(1)) <= tolerance)
|
||||
// 使用"显示横坐标":滞留在可视区外的光标被钉在边缘,也应该可以被抓住拖回来。
|
||||
if (std::fabs(fx - CursorDisplayX(1)) <= tolerance)
|
||||
{
|
||||
which = 1;
|
||||
return true;
|
||||
}
|
||||
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && std::fabs(fx - CursorX(2)) <= tolerance)
|
||||
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && std::fabs(fx - CursorDisplayX(2)) <= tolerance)
|
||||
{
|
||||
which = 2;
|
||||
return true;
|
||||
@@ -912,9 +1032,7 @@ namespace jdi
|
||||
Tab().ViewMin() = timeA;
|
||||
Tab().ViewMax() = timeB;
|
||||
ClampTimeView();
|
||||
// 视图范围变化后必须把光标夹回可见区间,否则光标会跑到视野外“消失”,
|
||||
// 用户会看到“放大后光标无法再拖动”。
|
||||
ClampCursorToView();
|
||||
// 注意:不在这里移动光标——即使选区不包含光标,也只把它的读数贴在网格边缘显示。
|
||||
|
||||
// 「选区 Y 轴自适应」开关开启的网格:时间选区放大后自动把 Y 轴匹配到新的可见区间。
|
||||
ApplySelectionAutoFit();
|
||||
@@ -1509,6 +1627,21 @@ namespace jdi
|
||||
height);
|
||||
}
|
||||
|
||||
if (m_editTarget == PlotEditTarget::CURSOR_SPAN)
|
||||
{
|
||||
// 差值输入框落在差值标签处(时间轴上方或时间轴内部)。
|
||||
bool insideAxis = false;
|
||||
const gd::RectF displayRect = CursorSpanRect(insideAxis);
|
||||
if (displayRect.Width <= 0.0f)
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
width = std::max(width, displayRect.Width);
|
||||
float left = displayRect.X + displayRect.Width * 0.5f - width * 0.5f;
|
||||
left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - width));
|
||||
return gd::RectF(left, displayRect.Y, width, height);
|
||||
}
|
||||
|
||||
if (m_editTarget == PlotEditTarget::SIGNAL_NAME)
|
||||
{
|
||||
// 图例改名:输入框直接落在被双击的图例条目位置上。
|
||||
@@ -1646,6 +1779,29 @@ namespace jdi
|
||||
{
|
||||
Tab().CursorTime() = clamped;
|
||||
}
|
||||
m_activeCursor = (m_editCursor == 2) ? 2 : 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target == PlotEditTarget::CURSOR_SPAN)
|
||||
{
|
||||
// 差值编辑:输入值采用编辑开始时的显示单位(如标签显示 ms 则输入的是 ms)。
|
||||
// **前方(时间较小)的光标不动**,后方光标向后移动到"前方 + 输入差值"。
|
||||
const double span = std::fabs(parsed) * m_editDurationUnit;
|
||||
const double front = std::min(Tab().CursorTime(), Tab().CursorTimeAlt());
|
||||
const bool secondIsBack = (Tab().CursorTimeAlt() >= Tab().CursorTime());
|
||||
const double moved = front + span;
|
||||
if (secondIsBack)
|
||||
{
|
||||
Tab().CursorTime() = front;
|
||||
Tab().CursorTimeAlt() = moved;
|
||||
}
|
||||
else
|
||||
{
|
||||
Tab().CursorTimeAlt() = front;
|
||||
Tab().CursorTime() = moved;
|
||||
}
|
||||
m_activeCursor = secondIsBack ? 2 : 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1829,15 +1985,20 @@ namespace jdi
|
||||
case PlotDragMode::PAN:
|
||||
return LoadCursorW(nullptr, IDC_SIZEALL);
|
||||
case PlotDragMode::RUBBER_SELECT:
|
||||
// 选区放大:按当前轴向给出对应方向的光标提示。
|
||||
// 选区放大:轴向未定时为十字,确定后按方向给出上下/左右箭头。
|
||||
if (m_rubberAxis == PlotRubberAxis::VALUE)
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||
}
|
||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||
if (m_rubberAxis == PlotRubberAxis::TIME)
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||
}
|
||||
return LoadCursorW(nullptr, IDC_CROSS);
|
||||
case PlotDragMode::SCROLL_LANES:
|
||||
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||
case PlotDragMode::CURSOR_MOVE:
|
||||
case PlotDragMode::CURSOR_SPAN_MOVE:
|
||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||
case PlotDragMode::MOVE_SIGNAL:
|
||||
return LoadCursorW(nullptr, IDC_SIZEALL);
|
||||
@@ -1851,6 +2012,13 @@ 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)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||
}
|
||||
|
||||
{
|
||||
int cursorWhich = 0;
|
||||
if (IsOnCursorLine(x, cursorWhich) && UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y)))
|
||||
@@ -1867,11 +2035,9 @@ namespace jdi
|
||||
return LoadCursorW(nullptr, IDC_SIZEALL);
|
||||
}
|
||||
|
||||
// 选区放大模式下不固定方向,用四向光标表示“横向/纵向都可拉”。
|
||||
if (Tab().ZoomMode() == PlotZoomMode::SELECT)
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_SIZEALL);
|
||||
}
|
||||
// 选区放大模式下**未开始框选时保持默认箭头**(不改变鼠标形状);
|
||||
// 只有真正按下并拖动后才切成十字/方向箭头。
|
||||
// (m_dragMode == RUBBER_SELECT 的情况已在上面处理。)
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1906,7 +2072,6 @@ namespace jdi
|
||||
Tab().ViewMin() += delta;
|
||||
Tab().ViewMax() += delta;
|
||||
ClampTimeView();
|
||||
ClampCursorToView();
|
||||
}
|
||||
|
||||
const int laneIndex = m_dragLane;
|
||||
@@ -1991,11 +2156,6 @@ namespace jdi
|
||||
{
|
||||
target = snapped;
|
||||
}
|
||||
// 只允许在当前可见区间内移动,避免把光标拖到视野之外而“丢失”。
|
||||
const double low = Tab().ViewMin();
|
||||
const double high = Tab().ViewMax();
|
||||
const double inset = (high > low) ? (high - low) * 0.002 : 0.0;
|
||||
target = ClampDouble(target, low + inset, high - inset);
|
||||
|
||||
if (m_hoverCursor == 2 && Tab().CursorMode() == PlotCursorMode::DOUBLE)
|
||||
{
|
||||
@@ -2009,6 +2169,16 @@ namespace jdi
|
||||
m_cursorHover = true;
|
||||
return true;
|
||||
}
|
||||
case PlotDragMode::CURSOR_SPAN_MOVE:
|
||||
{
|
||||
// 拖拽差值标签:两条光标整体平移,间隔保持不变。
|
||||
const double startTime = XToTime(static_cast<float>(m_spanDragStartX));
|
||||
const double delta = XToTime(fx) - startTime;
|
||||
Tab().CursorTime() = m_spanDragTime1 + delta;
|
||||
Tab().CursorTimeAlt() = m_spanDragTime2 + delta;
|
||||
m_dragMoved = true;
|
||||
return true;
|
||||
}
|
||||
case PlotDragMode::MOVE_SIGNAL:
|
||||
{
|
||||
const int target = LaneAtY(y);
|
||||
@@ -2028,13 +2198,6 @@ namespace jdi
|
||||
|
||||
bool changed = false;
|
||||
|
||||
// 网格内再次移动鼠标即认为用户已读到提示,把浮层收起(避免长期遮挡数据点取值)。
|
||||
if (!m_hintText.empty())
|
||||
{
|
||||
m_hintText.clear();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const int lane = LaneAtY(y);
|
||||
if (lane != m_hoverLane)
|
||||
{
|
||||
@@ -2112,6 +2275,17 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
// 双光标模式下先检查差值标签:它可拖拽(两条光标整体平移,间隔不变)。
|
||||
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && m_cursorSpanRect.Width > 0.0f &&
|
||||
UiPointInRect(m_cursorSpanRect, fx, fy))
|
||||
{
|
||||
m_dragMode = PlotDragMode::CURSOR_SPAN_MOVE;
|
||||
m_spanDragStartX = x;
|
||||
m_spanDragTime1 = Tab().CursorTime();
|
||||
m_spanDragTime2 = Tab().CursorTimeAlt();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 靠近时间光标线时优先进入拖动,避免误触发平移或缩放选区。
|
||||
{
|
||||
int cursorWhich = 0;
|
||||
@@ -2119,6 +2293,8 @@ namespace jdi
|
||||
{
|
||||
m_dragMode = PlotDragMode::CURSOR_MOVE;
|
||||
m_hoverCursor = cursorWhich;
|
||||
// 点中的光标成为“活动光标”:它触发的内容将画在上层。
|
||||
m_activeCursor = cursorWhich;
|
||||
m_cursorHover = true;
|
||||
return true;
|
||||
}
|
||||
@@ -2201,6 +2377,8 @@ namespace jdi
|
||||
case PlotDragMode::CURSOR_MOVE:
|
||||
m_cursorHover = true;
|
||||
break;
|
||||
case PlotDragMode::CURSOR_SPAN_MOVE:
|
||||
break;
|
||||
case PlotDragMode::MOVE_SIGNAL:
|
||||
{
|
||||
const int target = LaneAtY(y);
|
||||
@@ -2267,12 +2445,21 @@ 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)))
|
||||
{
|
||||
m_editDurationUnit = ChosenDurationUnit(CursorSpan()).scale;
|
||||
return BeginEdit(PlotEditTarget::CURSOR_SPAN, -1, FormatDurationNumber(CursorSpan()));
|
||||
}
|
||||
|
||||
// 命中时间光标:进入精确时间输入(交互与线宽输入框一致)。
|
||||
{
|
||||
int cursorWhich = 0;
|
||||
if (IsOnCursorLine(x, cursorWhich))
|
||||
{
|
||||
m_editCursor = (cursorWhich == 2) ? 2 : 1;
|
||||
m_activeCursor = m_editCursor;
|
||||
const double time =
|
||||
(m_editCursor == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
|
||||
return BeginEdit(PlotEditTarget::CURSOR_TIME, -1, FormatValue(time));
|
||||
@@ -3078,10 +3265,10 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 时间轴选区用强调色,Y 轴选区用警示色,便于区分两种方向。
|
||||
const std::uint32_t band = timeMode ? m_theme->accent : m_theme->danger;
|
||||
UiFillRect(g, m_rubberRect, WithAlpha(band, 0x24));
|
||||
UiDrawRoundRect(g, m_rubberRect, 2.0f * m_scale, band, 1.2f);
|
||||
// 两种方向的选区使用**同一种强调色**,保持视觉一致。
|
||||
const std::uint32_t band = m_theme->accent;
|
||||
UiFillRect(g, m_rubberRect, WithAlpha(band, 0x28));
|
||||
UiDrawRoundRect(g, m_rubberRect, 2.0f * m_scale, band, 1.4f);
|
||||
|
||||
// 选区跨度提示(时间用秒/数值用幅度量纲),放在选区顶部。
|
||||
std::wstring text;
|
||||
@@ -3113,25 +3300,41 @@ namespace jdi
|
||||
|
||||
void PlotView::PaintHint(gd::Graphics& g)
|
||||
{
|
||||
if (m_hintText.empty() || m_fonts == nullptr || m_fonts->uiSmall == nullptr)
|
||||
if (m_hintText.empty() || m_fonts == nullptr || m_fonts->ui == nullptr || m_hintShownAt == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const std::uint64_t elapsed = GetTickCount64() - m_hintShownAt;
|
||||
if (elapsed >= (kHintHoldMs + kHintFadeMs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 停留结束后在 kHintFadeMs 内线性淡出(颜色 alpha 按比例缩放)。
|
||||
const double fade = (elapsed <= kHintHoldMs)
|
||||
? 1.0
|
||||
: (1.0 - static_cast<double>(elapsed - kHintHoldMs) /
|
||||
static_cast<double>(kHintFadeMs));
|
||||
const unsigned int alpha =
|
||||
static_cast<unsigned int>(std::max(0.0, std::min(1.0, fade)) * 255.0);
|
||||
|
||||
// 提示条贴在绘图区底部居中;时间光标的时间读数也在底部,
|
||||
// 因此光标启用时把提示条上移一行,避免两者互相遮挡。
|
||||
const float reserved = (m_tab != nullptr && Tab().CursorEnabled()) ? 24.0f * m_scale : 0.0f;
|
||||
const float width = UiTextWidth(g, m_hintText, m_fonts->uiSmall) + 20.0f * m_scale;
|
||||
const float height = 22.0f * m_scale;
|
||||
const float reserved = (m_tab != nullptr && Tab().CursorEnabled()) ? 26.0f * m_scale : 0.0f;
|
||||
const float width = UiTextWidth(g, m_hintText, m_fonts->ui) + 30.0f * m_scale;
|
||||
const float height = 30.0f * m_scale;
|
||||
float left = m_plotRect.X + (m_plotRect.Width - width) * 0.5f;
|
||||
left = std::max(m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(left, m_plotRect.X + m_plotRect.Width - width - 2.0f * m_scale));
|
||||
const gd::RectF pill(left, m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale - reserved,
|
||||
const gd::RectF pill(left, m_plotRect.Y + m_plotRect.Height - height - 4.0f * m_scale - reserved,
|
||||
width, height);
|
||||
|
||||
UiFillRoundRect(g, pill, 3.0f * m_scale, WithAlpha(m_theme->readoutBg, 0xF2));
|
||||
UiDrawRoundRect(g, pill, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
|
||||
UiText(g, m_hintText, m_fonts->uiSmall, pill, m_theme->textStrong, TextAlign::CENTER);
|
||||
UiFillRoundRect(g, pill, 4.0f * m_scale, ScaleAlpha(m_theme->readoutBg, alpha));
|
||||
UiDrawRoundRect(g, pill, 4.0f * m_scale, ScaleAlpha(m_theme->borderStrong, alpha), 1.2f);
|
||||
// 文字走 GDI(不支持 alpha 通道),因此把文字颜色向胶囊底色**混色**来实现淡出。
|
||||
const std::uint32_t textColor =
|
||||
MixColor(m_theme->textStrong, m_theme->readoutBg, 1.0f - static_cast<float>(fade));
|
||||
UiText(g, m_hintText, m_fonts->ui, pill, textColor, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
void PlotView::PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane,
|
||||
@@ -3644,6 +3847,62 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void PlotView::DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius, bool textDark)
|
||||
{
|
||||
// 用与文字同色的半透明细描边:深色文字 → 浅色描边,浅色文字 → 深色描边。
|
||||
// 这样胶囊压在同类色背景或信号曲线上时仍有清晰的边界。
|
||||
const std::uint32_t outline = textDark ? 0x99FDF6E3u : 0x99073642u;
|
||||
UiDrawRoundRect(g, rect, radius, outline, 1.0f);
|
||||
}
|
||||
|
||||
gd::RectF PlotView::CursorSpanRect(bool& insideAxis) const
|
||||
{
|
||||
insideAxis = false;
|
||||
if (m_tab == nullptr || Tab().CursorMode() != PlotCursorMode::DOUBLE ||
|
||||
m_fonts == nullptr || m_fonts->monoSmall == nullptr)
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
|
||||
const std::wstring text = FormatDuration(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;
|
||||
|
||||
// 水平位置取两条光标时间读数的中点(与它们同一行,位于时间轴上方)。
|
||||
// 注意用"显示横坐标":光标滞留在可视区外时会被钉在网格边缘,
|
||||
// 此时二者在屏幕上必然很近,差值标签应改放到主时间轴内部。
|
||||
const gd::RectF first = CursorLabelRect(1);
|
||||
const gd::RectF second = CursorLabelRect(2);
|
||||
float center = (CursorDisplayX(1) + CursorDisplayX(2)) * 0.5f;
|
||||
if (first.Width > 0.0f && second.Width > 0.0f)
|
||||
{
|
||||
center = ((first.X + first.Width * 0.5f) + (second.X + second.Width * 0.5f)) * 0.5f;
|
||||
}
|
||||
|
||||
float left = center - width * 0.5f;
|
||||
left = std::max(m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(left, m_plotRect.X + m_plotRect.Width - width - 2.0f * m_scale));
|
||||
|
||||
// 两条**时间读数标签**之间的净空不足时,同一行放不下第三个标签
|
||||
// → 改放到主时间轴内部(时间轴上方已无空间)。
|
||||
const gd::RectF& leftRect = (first.X <= second.X) ? first : second;
|
||||
const gd::RectF& rightRect = (first.X <= second.X) ? second : first;
|
||||
const float freeStart = leftRect.X + leftRect.Width;
|
||||
const float freeEnd = rightRect.X;
|
||||
const bool fits = (freeStart <= freeEnd) && ((freeEnd - freeStart) >= (width + 2.0f * m_scale));
|
||||
if (!fits)
|
||||
{
|
||||
insideAxis = true;
|
||||
const float top = m_timeAxisRect.Y + 2.0f * m_scale;
|
||||
const float axisHeight = std::max(14.0f * m_scale, m_timeAxisRect.Height - 4.0f * m_scale);
|
||||
return gd::RectF(left, top, width, std::min(height, axisHeight));
|
||||
}
|
||||
|
||||
return gd::RectF(left, m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale, width, height);
|
||||
}
|
||||
|
||||
void PlotView::PaintCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes)
|
||||
{
|
||||
if (!Tab().CursorEnabled() || m_tab == nullptr)
|
||||
@@ -3652,73 +3911,51 @@ namespace jdi
|
||||
}
|
||||
|
||||
const bool doubleMode = (Tab().CursorMode() == PlotCursorMode::DOUBLE);
|
||||
|
||||
// 双光标时统一以“时间较小者为左、较大者为右”,右侧光标负责显示 ±增量,
|
||||
// 这样不论用户拖动哪一条,读数含义都稳定。
|
||||
double leftTime = Tab().CursorTime();
|
||||
double rightTime = Tab().CursorTimeAlt();
|
||||
int leftWhich = 1;
|
||||
int rightWhich = 2;
|
||||
if (doubleMode && rightTime < leftTime)
|
||||
{
|
||||
std::swap(leftTime, rightTime);
|
||||
leftWhich = 2;
|
||||
rightWhich = 1;
|
||||
}
|
||||
|
||||
const int cursorCount = doubleMode ? 2 : 1;
|
||||
|
||||
// 重叠时把"最近操作过的那条光标"的内容画在上层,因此先画另一条。
|
||||
const int order[2] = { (m_activeCursor == 2) ? 1 : 2, (m_activeCursor == 2) ? 2 : 1 };
|
||||
|
||||
// ---- 光标竖线 + 顶部把手 ----
|
||||
for (int which = 1; which <= cursorCount; ++which)
|
||||
for (int pass = 0; pass < cursorCount; ++pass)
|
||||
{
|
||||
const int which = doubleMode ? order[pass] : 1;
|
||||
const double time = (which == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
|
||||
const float x = TimeToX(time);
|
||||
if (x < m_plotRect.X - 2.0f || x > m_plotRect.X + m_plotRect.Width + 2.0f)
|
||||
{
|
||||
// 光标滞留在可视区外:不画竖线(其取值/时间读数会在下面贴边显示)。
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool active = (m_hoverCursor == which) || (m_dragMode == PlotDragMode::CURSOR_MOVE && m_hoverCursor == which);
|
||||
const bool hot = (m_hoverCursor == which);
|
||||
const std::uint32_t color = CursorColor(which);
|
||||
// 光标纵向贯穿全部网格,并延伸到时间轴底部,便于与刻度对齐读数。
|
||||
const float lineTop = m_plotRect.Y;
|
||||
const float lineBottom = m_timeAxisRect.Y + m_timeAxisRect.Height;
|
||||
UiDrawLine(g, x, lineTop, x, lineBottom, color, (active ? 2.0f : 1.4f) * m_scale);
|
||||
UiDrawLine(g, x, lineTop, x, lineBottom, color, (hot ? 2.0f : 1.4f) * m_scale);
|
||||
|
||||
// 顶部三角把手,提示该线可以被拖拽。(光标 2 用朝下的把手与光标 1 区分)
|
||||
// 顶部三角把手(两条光标样式一致,靠颜色区分)。
|
||||
const float arm = 5.0f * m_scale;
|
||||
gd::PointF points[3];
|
||||
if (which == 1)
|
||||
{
|
||||
points[0] = gd::PointF(x - arm, lineTop);
|
||||
points[1] = gd::PointF(x + arm, lineTop);
|
||||
points[2] = gd::PointF(x, lineTop + arm * 1.4f);
|
||||
}
|
||||
else
|
||||
{
|
||||
const float bottom = lineTop + arm * 1.4f + arm * 1.4f;
|
||||
points[0] = gd::PointF(x - arm, bottom);
|
||||
points[1] = gd::PointF(x + arm, bottom);
|
||||
points[2] = gd::PointF(x, lineTop);
|
||||
}
|
||||
points[0] = gd::PointF(x - arm, lineTop);
|
||||
points[1] = gd::PointF(x + arm, lineTop);
|
||||
points[2] = gd::PointF(x, lineTop + arm * 1.4f);
|
||||
gd::SolidBrush handle(ToGdiColor(color));
|
||||
g.FillPolygon(&handle, points, 3);
|
||||
}
|
||||
|
||||
// ---- 直接在光标穿过的数据点上标注取值 ----
|
||||
// 每个网格里每条可见信号都标注一次,颜色与信号自身颜色对应;
|
||||
// 双光标时右侧光标额外显示“当前值 , ±增量”。
|
||||
// ---- 数据点取值标注 ----
|
||||
for (int pass = 0; pass < cursorCount; ++pass)
|
||||
{
|
||||
const int which = doubleMode ? ((pass == 0) ? leftWhich : rightWhich) : 1;
|
||||
const int which = doubleMode ? order[pass] : 1;
|
||||
const double time = (which == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
|
||||
const double otherTime = (which == 2) ? Tab().CursorTime() : Tab().CursorTimeAlt();
|
||||
const bool showDelta = doubleMode && (which == rightWhich);
|
||||
const float x = TimeToX(time);
|
||||
if (x < m_plotRect.X - 2.0f || x > m_plotRect.X + m_plotRect.Width + 2.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const bool showDelta = doubleMode && (which == order[1]);
|
||||
|
||||
// 光标在可视区外时,把标注"钉"在对应的网格边上,保证取值仍然可见。
|
||||
const float pinX = CursorDisplayX(which);
|
||||
const float rawX = TimeToX(time);
|
||||
const bool pinned = (std::fabs(rawX - pinX) > 0.5f);
|
||||
|
||||
for (std::size_t laneIndex = 0; laneIndex < lanes.size(); ++laneIndex)
|
||||
{
|
||||
@@ -3755,16 +3992,34 @@ namespace jdi
|
||||
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
|
||||
// 数据点:底色环 + 信号色实心点。
|
||||
// 数据点:底色环 + 信号色实心点(贴边时改画成三角指示方向)。
|
||||
const float radius = 3.4f * m_scale;
|
||||
gd::SolidBrush ring(ToGdiColor(m_theme->cardBg));
|
||||
gd::SolidBrush fill(ToGdiColor(signalColor));
|
||||
g.FillEllipse(&ring, x - radius - 1.2f * m_scale, y - radius - 1.2f * m_scale,
|
||||
(radius + 1.2f * m_scale) * 2.0f, (radius + 1.2f * m_scale) * 2.0f);
|
||||
g.FillEllipse(&fill, x - radius, y - radius, radius * 2.0f, radius * 2.0f);
|
||||
if (pinned)
|
||||
{
|
||||
const float arm = 5.0f * m_scale;
|
||||
const bool offLeft = (rawX < pinX);
|
||||
const float baseX = offLeft ? (pinX - arm) : (pinX + arm);
|
||||
gd::PointF tri[3];
|
||||
tri[0] = gd::PointF(baseX, y - arm);
|
||||
tri[1] = gd::PointF(baseX, y + arm);
|
||||
tri[2] = gd::PointF(pinX, y);
|
||||
g.FillPolygon(&ring, tri, 3);
|
||||
gd::PointF inner[3];
|
||||
inner[0] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale, y - arm * 0.72f);
|
||||
inner[1] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale, y + arm * 0.72f);
|
||||
inner[2] = gd::PointF(pinX - (offLeft ? -1.4f : 1.4f) * m_scale, y);
|
||||
g.FillPolygon(&fill, inner, 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.FillEllipse(&ring, pinX - radius - 1.2f * m_scale, y - radius - 1.2f * m_scale,
|
||||
(radius + 1.2f * m_scale) * 2.0f, (radius + 1.2f * m_scale) * 2.0f);
|
||||
g.FillEllipse(&fill, pinX - radius, y - radius, radius * 2.0f, radius * 2.0f);
|
||||
}
|
||||
|
||||
// 取值标注:底色用信号色、文字用反色,保证在任何波形上都清晰可读,
|
||||
// 并让“读数 ↔ 信号”一一对应。
|
||||
// 取值标注:底色用信号色、文字用反色,并加一层细描边提升对比度。
|
||||
std::wstring text = FormatValue(value);
|
||||
if (showDelta)
|
||||
{
|
||||
@@ -3782,14 +4037,14 @@ namespace jdi
|
||||
|
||||
const float textWidth = UiTextWidth(g, text, m_fonts->monoSmall) + 10.0f * m_scale;
|
||||
const float textHeight = 17.0f * m_scale;
|
||||
// 默认标在数据点右侧;右侧空间不足时改到左侧。
|
||||
float labelLeft = x + radius + 4.0f * m_scale;
|
||||
float labelLeft = pinX + radius + 4.0f * m_scale;
|
||||
if (labelLeft + textWidth > m_plotRect.X + m_plotRect.Width - 2.0f * m_scale)
|
||||
{
|
||||
labelLeft = x - radius - 4.0f * m_scale - textWidth;
|
||||
labelLeft = pinX - radius - 4.0f * m_scale - textWidth;
|
||||
}
|
||||
labelLeft = std::max(m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(labelLeft, m_plotRect.X + m_plotRect.Width - textWidth - 2.0f * m_scale));
|
||||
labelLeft = std::max(
|
||||
m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(labelLeft, m_plotRect.X + m_plotRect.Width - textWidth - 2.0f * m_scale));
|
||||
float labelTop = y - textHeight * 0.5f;
|
||||
labelTop = std::max(clipTop + 1.0f * m_scale,
|
||||
std::min(labelTop, clipBottom - textHeight - 1.0f * m_scale));
|
||||
@@ -3797,23 +4052,24 @@ namespace jdi
|
||||
|
||||
UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(signalColor, 0xF0));
|
||||
const bool dark = IsDarkColor(signalColor);
|
||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
|
||||
UiText(g, text, m_fonts->monoSmall, labelRect,
|
||||
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 时间读数:改到绘图区底部 ----
|
||||
if (m_editTarget == PlotEditTarget::CURSOR_TIME)
|
||||
// ---- 底部:时间读数 + 双光标差值 ----
|
||||
if (m_editTarget == PlotEditTarget::CURSOR_TIME ||
|
||||
m_editTarget == PlotEditTarget::CURSOR_SPAN)
|
||||
{
|
||||
PaintEditField(g);
|
||||
return;
|
||||
}
|
||||
|
||||
// 时间读数固定在绘图区底部,单/双光标都始终显示,
|
||||
// 这样不必把鼠标移到光线上就能读到时间。
|
||||
for (int which = 1; which <= cursorCount; ++which)
|
||||
for (int pass = 0; pass < cursorCount; ++pass)
|
||||
{
|
||||
const int which = doubleMode ? order[pass] : 1;
|
||||
const gd::RectF labelRect = CursorLabelRect(which);
|
||||
if (labelRect.Width <= 0.0f)
|
||||
{
|
||||
@@ -3824,10 +4080,31 @@ namespace jdi
|
||||
UiFillRoundRect(g, labelRect, 3.0f * m_scale,
|
||||
highlighted ? color : WithAlpha(color, 0xE0));
|
||||
const bool dark = IsDarkColor(color);
|
||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
|
||||
UiText(g, FormatValue(which == 2 ? Tab().CursorTimeAlt() : Tab().CursorTime()),
|
||||
m_fonts->monoSmall, labelRect,
|
||||
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
// 双光标差值:显示在两条时间读数中间(时间轴上方;过近时落到主时间轴上)。
|
||||
if (doubleMode)
|
||||
{
|
||||
bool insideAxis = false;
|
||||
const gd::RectF spanRect = CursorSpanRect(insideAxis);
|
||||
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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_cursorSpanRect = gd::RectF();
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::PaintEditField(gd::Graphics& g)
|
||||
|
||||
@@ -62,8 +62,8 @@ namespace jdi
|
||||
theme.gridStrong = MakeArgb(0xC3B896u);
|
||||
theme.axisLine = MakeArgb(kBase1);
|
||||
theme.cursor = MakeArgb(kOrange);
|
||||
// 第二光标用 Solarized 青色:与橙色同族饱和度、对比清晰又不刺眼。
|
||||
theme.cursorAlt = MakeArgb(kCyan);
|
||||
// 第二光标用 Solarized 洋红:与橙色同族暖色但更鲜艳,对比清晰又不刺眼。
|
||||
theme.cursorAlt = MakeArgb(kMagenta);
|
||||
theme.readoutBg = MakeArgb(0xF6EFDCu);
|
||||
theme.danger = MakeArgb(kRed);
|
||||
theme.warning = MakeArgb(kYellow);
|
||||
|
||||
Reference in new issue
Block a user