输入框鼠标编辑、图例省略、信号跨视图共享选择与网格菜单布局修复
- 输入框:支持鼠标左键点击定位插入符、拖拽框选区域,并且不再限制字符长度 - 输入框:长文本使用 ETO_CLIPPED 裁剪绘制,避免字符溢出到编辑区域之外 - 图例:信号名称超过 200px 时省略号截断,不再挤压标尺区域 - 选中状态从 TabConfig 迁移到 DataSet,切换视图后信号选中状态保持一致 - 网格右键菜单:Y 轴最大值/最小值直接显示当前值,点击后原位置弹出内联编辑框 - 标尺数值改为绘制在图例下层,不再因避让图例而隐藏 - 移除已无用的 LegendBandRect,新增共享的编辑框布局与命中检测工具 - .gitignore 忽略 TestData 测试数据目录
This commit is contained in:
1 parent
e3744f3eff
commit
12b3531485
19 files changed
+790
-186
No files matched your search
@@ -36,6 +36,7 @@ namespace jdi
|
||||
const float kLegendPad = 5.0f; /**< 图例相对网格左上角的内缩。 */
|
||||
const float kLegendSwatchSize = 8.0f; /**< 图例色块尺寸。 */
|
||||
const float kLegendItemGap = 14.0f; /**< 图例条目之间的间距。 */
|
||||
const float kLegendMaxTextWidth = 200.0f; /**< 图例条目的文本宽度上限(超出则省略)。 */
|
||||
|
||||
/**
|
||||
* @brief 将数值限制到合法区间。
|
||||
@@ -95,9 +96,10 @@ namespace jdi
|
||||
m_theme = theme;
|
||||
}
|
||||
|
||||
void PlotView::SetContext(const DataSet* dataSet, TabConfig* tab)
|
||||
void PlotView::SetContext(DataSet* dataSet, TabConfig* tab)
|
||||
{
|
||||
m_dataSet = dataSet;
|
||||
m_dataSetWritable = dataSet;
|
||||
m_tab = tab;
|
||||
m_hoverLane = -1;
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
@@ -216,6 +218,20 @@ namespace jdi
|
||||
BuildLaneVisuals();
|
||||
}
|
||||
|
||||
const SignalRef& PlotView::SelectedRef() const
|
||||
{
|
||||
static const SignalRef fallback;
|
||||
return (m_dataSet != nullptr) ? m_dataSet->Selected() : fallback;
|
||||
}
|
||||
|
||||
void PlotView::SelectSignal(const SignalRef& ref)
|
||||
{
|
||||
if (m_dataSetWritable != nullptr)
|
||||
{
|
||||
m_dataSetWritable->SetSelected(ref);
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::ClampLaneScroll()
|
||||
{
|
||||
const float maxOffset = std::max(0.0f, m_laneContentHeight - m_plotRect.Height);
|
||||
@@ -837,7 +853,7 @@ namespace jdi
|
||||
BuildLaneVisuals();
|
||||
}
|
||||
|
||||
bool PlotView::BeginLaneValueEdit(int laneIndex, bool editMax)
|
||||
bool PlotView::BeginLaneValueEdit(int laneIndex, bool editMax, const gd::RectF& anchorRect)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
@@ -855,7 +871,27 @@ namespace jdi
|
||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
const double current = editMax ? visual.yMax : visual.yMin;
|
||||
return BeginEdit(editMax ? PlotEditTarget::LANE_Y_MAX : PlotEditTarget::LANE_Y_MIN, laneIndex,
|
||||
FormatValue(current));
|
||||
FormatValue(current), anchorRect);
|
||||
}
|
||||
|
||||
bool PlotView::LaneRange(int laneIndex, double& yMin, double& yMax) const
|
||||
{
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
// 菜单弹出时几何可能尚未建立,回退到配置内的自适应范围。
|
||||
if (m_tab == nullptr || m_dataSet == nullptr || laneIndex < 0 ||
|
||||
laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_tab->ComputeLaneRange(*m_dataSet, laneIndex, yMin, yMax);
|
||||
return true;
|
||||
}
|
||||
|
||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
yMin = visual.yMin;
|
||||
yMax = visual.yMax;
|
||||
return true;
|
||||
}
|
||||
|
||||
SignalRef PlotView::HitTestSignal(int x, int y) const
|
||||
@@ -988,7 +1024,18 @@ namespace jdi
|
||||
return gd::RectF(m_editFieldRect.X, m_editFieldRect.Y, nameWidth, height);
|
||||
}
|
||||
|
||||
// 网格 Y 轴上下限输入框贴在该网格内侧标尺带下方。
|
||||
// 网格 Y 轴上下限输入框:优先落在被点击的菜单条目位置,保证"就地编辑"的观感。
|
||||
if (m_editFieldRect.Width > 0.0f && m_editFieldRect.Height > 0.0f)
|
||||
{
|
||||
float left = m_editFieldRect.X;
|
||||
float top = m_editFieldRect.Y;
|
||||
left = std::max(m_area.X + 2.0f * m_scale,
|
||||
std::min(left, m_area.X + m_area.Width - width - 2.0f * m_scale));
|
||||
top = std::max(m_area.Y + 2.0f * m_scale,
|
||||
std::min(top, m_area.Y + m_area.Height - height - 2.0f * m_scale));
|
||||
return gd::RectF(left, top, width, height);
|
||||
}
|
||||
|
||||
if (m_editLane < 0 || m_editLane >= static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
return gd::RectF();
|
||||
@@ -999,7 +1046,8 @@ namespace jdi
|
||||
return gd::RectF(left, top, std::min(width, lane.rect.Width - 2.0f * kLaneLabelPad * m_scale), height);
|
||||
}
|
||||
|
||||
bool PlotView::BeginEdit(PlotEditTarget target, int laneIndex, const std::wstring& initialText)
|
||||
bool PlotView::BeginEdit(PlotEditTarget target, int laneIndex, const std::wstring& initialText,
|
||||
const gd::RectF& anchorRect)
|
||||
{
|
||||
if (target == PlotEditTarget::NONE)
|
||||
{
|
||||
@@ -1009,15 +1057,23 @@ namespace jdi
|
||||
if (target != PlotEditTarget::SIGNAL_NAME)
|
||||
{
|
||||
m_editSignal = SignalRef();
|
||||
}
|
||||
// 调用方可以用锚点矩形指定输入框位置(如菜单条目、图例条目);
|
||||
// 未提供锚点时由 EditFieldRect() 按目标类型自动推导。
|
||||
if (anchorRect.Width > 0.0f && anchorRect.Height > 0.0f)
|
||||
{
|
||||
m_editFieldRect = anchorRect;
|
||||
}
|
||||
else if (target != PlotEditTarget::SIGNAL_NAME)
|
||||
{
|
||||
m_editFieldRect = gd::RectF();
|
||||
}
|
||||
m_editTarget = target;
|
||||
m_editLane = laneIndex;
|
||||
|
||||
// 数值字段限制为数字与小数点,信号名可输入任意字符(保留括号与空格)。
|
||||
// 名称与数值均不限长度(数值在提交时会做范围夹取)。
|
||||
const bool nameField = (target == PlotEditTarget::SIGNAL_NAME);
|
||||
m_edit.Begin(initialText, true, nameField ? EditFilter::ANY : EditFilter::NUMBER,
|
||||
nameField ? 96u : 18u);
|
||||
m_edit.Begin(initialText, true, nameField ? EditFilter::ANY : EditFilter::NUMBER, 0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1177,6 +1233,44 @@ namespace jdi
|
||||
return m_edit.DeselectToEnd();
|
||||
}
|
||||
|
||||
bool PlotView::OnEditMouseDown(int x, int y)
|
||||
{
|
||||
if (m_editTarget == PlotEditTarget::NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const gd::RectF rect = EditFieldRect();
|
||||
if (rect.Width <= 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return UiEditFieldMouseDown(m_fonts->uiSmall, rect, m_edit, m_scale, x, y);
|
||||
}
|
||||
|
||||
bool PlotView::OnEditMouseMove(int x)
|
||||
{
|
||||
if (m_editTarget == PlotEditTarget::NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const gd::RectF rect = EditFieldRect();
|
||||
if (rect.Width <= 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return UiEditFieldMouseMove(m_fonts->uiSmall, rect, m_edit, m_scale, x);
|
||||
}
|
||||
|
||||
bool PlotView::OnEditMouseUp()
|
||||
{
|
||||
if (!m_edit.Dragging())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_edit.EndDrag();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::OnEditKey(WPARAM key)
|
||||
{
|
||||
if (m_editTarget == PlotEditTarget::NONE)
|
||||
@@ -1508,7 +1602,7 @@ namespace jdi
|
||||
const SignalRef legendHit = LegendAtPoint(x, y);
|
||||
if (legendHit.IsValid())
|
||||
{
|
||||
Tab().SetSelected(legendHit);
|
||||
SelectSignal(legendHit);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
@@ -1520,7 +1614,7 @@ namespace jdi
|
||||
{
|
||||
m_dragSignal = hit;
|
||||
m_dragTargetLane = Tab().LaneOfSignal(hit);
|
||||
Tab().SetSelected(hit);
|
||||
SelectSignal(hit);
|
||||
m_dragMouseX = x;
|
||||
m_dragMouseY = y;
|
||||
m_dragMode = PlotDragMode::MOVE_SIGNAL;
|
||||
@@ -1633,7 +1727,7 @@ namespace jdi
|
||||
const ResolvedSignal resolved = ResolveSignal(legendHit);
|
||||
if (resolved.data != nullptr)
|
||||
{
|
||||
Tab().SetSelected(legendHit);
|
||||
SelectSignal(legendHit);
|
||||
m_editSignal = legendHit;
|
||||
|
||||
// 先把输入框位置落在被双击的条目上,再进入编辑态。
|
||||
@@ -2077,9 +2171,6 @@ namespace jdi
|
||||
// 内侧纵轴标尺作为覆盖层绘制:每个刻度数值带半透明底,保证在波形之上仍清晰可读,
|
||||
// 同时只在数值背后遮挡波形,不牺牲波形本身的可见范围。
|
||||
{
|
||||
// 图例固定占用网格左上角,冲突时优先显示图例,故落在图例带内的刻度数值直接跳过。
|
||||
const gd::RectF legendBand = LegendBandRect(laneIndex, lane);
|
||||
|
||||
const bool showLabels = (lane.rect.Height >= 34.0f * m_scale);
|
||||
float lastLabelY = -1.0e9f;
|
||||
|
||||
@@ -2117,16 +2208,10 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 与图例带冲突时让位(图例优先)。
|
||||
if (legendBand.Width > 0.0f &&
|
||||
pill.X < legendBand.X + legendBand.Width &&
|
||||
pill.X + pill.Width > legendBand.X &&
|
||||
pill.Y < legendBand.Y + legendBand.Height &&
|
||||
pill.Y + pill.Height > legendBand.Y)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 文本必须完整落在可见区内,否则会越过绘图区顶部(GDI 文本不受 GDI+ 裁剪约束)。
|
||||
// 与图例重叠时不再跳过:标尺值先绘制,随后绘制的图例会盖在其上层
|
||||
// (顶层始终为图例),因此两者都不丢失。
|
||||
// 但仍要求数值完整落在可见区内——否则会在网格分界处露出被截断的半行数字,
|
||||
// 且 GDI 文本不受 GDI+ 裁剪约束会越过绘图区(改用 UiTextAtClipped 兜底)。
|
||||
if (!IsFullyVisible(pill, clipTop, clipBottom))
|
||||
{
|
||||
continue;
|
||||
@@ -2152,43 +2237,6 @@ namespace jdi
|
||||
g.Restore(saveState);
|
||||
}
|
||||
|
||||
gd::RectF PlotView::LegendBandRect(int laneIndex, const LaneVisual& lane) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
|
||||
// 逐条累加已可见图例的宽度,得到整条图例带(用于让标尺数字避让)。
|
||||
float width = 0.0f;
|
||||
for (const SignalRef& ref : m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
std::wstring label = BuildLegendText(*resolved.data);
|
||||
if (LaneSpansMultipleFiles(laneIndex) && resolved.file != nullptr)
|
||||
{
|
||||
label += L" · " + resolved.file->Title();
|
||||
}
|
||||
const float textWidth = UiMeasureTextWidth(m_fonts->uiSmall, label);
|
||||
width += kLegendSwatchSize * m_scale + 5.0f * m_scale + textWidth + kLegendItemGap * m_scale;
|
||||
}
|
||||
|
||||
if (width <= 0.0f)
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
|
||||
const float left = lane.rect.X + kLegendPad * m_scale;
|
||||
const float maxWidth = std::max(0.0f, lane.rect.Width - kLegendPad * 2.0f * m_scale);
|
||||
return gd::RectF(left, lane.rect.Y + kLegendPad * m_scale - 1.0f * m_scale,
|
||||
std::min(width + 6.0f * m_scale, maxWidth), kLegendHeight * m_scale + 2.0f * m_scale);
|
||||
}
|
||||
|
||||
SignalRef PlotView::LegendAtPoint(int x, int y) const
|
||||
{
|
||||
const float fx = static_cast<float>(x);
|
||||
@@ -2248,7 +2296,9 @@ namespace jdi
|
||||
label += L" · " + resolved.file->Title();
|
||||
}
|
||||
const float itemWidth = kLegendSwatchSize * m_scale + 5.0f * m_scale +
|
||||
UiTextWidth(g, label, m_fonts->uiSmall) + kLegendItemGap * m_scale;
|
||||
std::min(UiTextWidth(g, label, m_fonts->uiSmall),
|
||||
kLegendMaxTextWidth * m_scale) +
|
||||
kLegendItemGap * m_scale;
|
||||
if (cursorX + legendWidth + itemWidth > maxX)
|
||||
{
|
||||
break;
|
||||
@@ -2292,9 +2342,14 @@ namespace jdi
|
||||
{
|
||||
label += L" · " + resolved.file->Title();
|
||||
}
|
||||
const float textWidth = UiTextWidth(g, label, m_fonts->uiSmall);
|
||||
const float itemWidth =
|
||||
kLegendSwatchSize * m_scale + 5.0f * m_scale + textWidth + kLegendItemGap * m_scale;
|
||||
// 文本宽度按上限夹取,超出部分用省略号(与测量阶段保持一致)。
|
||||
const float maxTextWidth = kLegendMaxTextWidth * m_scale;
|
||||
const float fullTextWidth = UiTextWidth(g, label, m_fonts->uiSmall);
|
||||
const float textWidth = std::min(fullTextWidth, maxTextWidth);
|
||||
const std::wstring drawLabel =
|
||||
(fullTextWidth > maxTextWidth) ? UiEllipsize(g, label, m_fonts->uiSmall, maxTextWidth) : label;
|
||||
const float itemWidth = kLegendSwatchSize * m_scale + 5.0f * m_scale + textWidth +
|
||||
kLegendItemGap * m_scale;
|
||||
if (drawnWidth + itemWidth > legendWidth + 0.5f)
|
||||
{
|
||||
break;
|
||||
@@ -2305,7 +2360,7 @@ namespace jdi
|
||||
const float swatchSize = kLegendSwatchSize * m_scale;
|
||||
|
||||
// 图例条目与被选中的信号保持一致的高亮反馈(与左侧列表选中态呼应)。
|
||||
const bool selected = (m_tab->Selected() == ref);
|
||||
const bool selected = (SelectedRef() == ref);
|
||||
const bool hovered = (ref == m_hoverSignal) || (ref == m_dragSignal);
|
||||
if (selected || hovered)
|
||||
{
|
||||
@@ -2334,7 +2389,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
cursorX += swatchSize + 5.0f * m_scale;
|
||||
UiText(g, label, m_fonts->uiSmall,
|
||||
UiText(g, drawLabel, m_fonts->uiSmall,
|
||||
gd::RectF(cursorX, top, std::max(8.0f, textWidth + 2.0f), height),
|
||||
selected ? m_theme->textStrong : m_theme->text, TextAlign::LEFT);
|
||||
cursorX += textWidth + kLegendItemGap * m_scale;
|
||||
|
||||
Reference in new issue
Block a user