fix: 修正滑块填充残留、数值内联输入与信号/分组重命名,拖拽虚影跟随鼠标
- 滑块最小值时不再有超出圆钮的填充残留,三处滑块统一为共享实现 - 透明度/RGB/色号支持点击或双击直接输入,整段高亮、首次键入替换、回车提交、Ctrl+C 复制 - 双击信号行或分组名进入重命名编辑;分组竖线改为绘制而非字符 - 拖拽信号时曲线虚影跟随鼠标纵向位移 - 数据层新增展示名覆盖,面板改为持有可写数据集合
This commit is contained in:
1 parent
542aa51e40
commit
261b8ac07d
13 files changed
+1074
-274
No files matched your search
@@ -703,7 +703,7 @@ namespace jdi
|
|||||||
const DataFile* file = m_dataSet.FileAt(fileIndex);
|
const DataFile* file = m_dataSet.FileAt(fileIndex);
|
||||||
if (file != nullptr)
|
if (file != nullptr)
|
||||||
{
|
{
|
||||||
std::wstring message = L"已加载 组" + std::to_wstring(fileIndex + 1) + L"|" + file->fileName +
|
std::wstring message = L"已加载 组" + std::to_wstring(fileIndex + 1) + L"|" + file->Title() +
|
||||||
L"(" + FormatCount(static_cast<unsigned long long>(file->time.size())) +
|
L"(" + FormatCount(static_cast<unsigned long long>(file->time.size())) +
|
||||||
L" 行采样," + std::to_wstring(file->signals.size()) +
|
L" 行采样," + std::to_wstring(file->signals.size()) +
|
||||||
L" 条信号,时间列:" + file->timeColumnName + L")";
|
L" 条信号,时间列:" + file->timeColumnName + L")";
|
||||||
@@ -1270,6 +1270,11 @@ namespace jdi
|
|||||||
repaint = true;
|
repaint = true;
|
||||||
consume = true;
|
consume = true;
|
||||||
break;
|
break;
|
||||||
|
case WM_CHAR:
|
||||||
|
// 字符输入转发给拾色器,用于 RGB 分量与十六进制色号的内联编辑。
|
||||||
|
repaint = m_colorPicker.OnChar(static_cast<wchar_t>(wParam));
|
||||||
|
consume = true;
|
||||||
|
break;
|
||||||
case WM_RBUTTONDOWN:
|
case WM_RBUTTONDOWN:
|
||||||
case WM_MBUTTONDOWN:
|
case WM_MBUTTONDOWN:
|
||||||
case WM_MOUSEWHEEL:
|
case WM_MOUSEWHEEL:
|
||||||
@@ -1616,7 +1621,7 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
case WM_KEYDOWN:
|
case WM_KEYDOWN:
|
||||||
// 数值输入状态优先接管按键(回车提交、Esc 取消、退格删除)。
|
// 数值输入状态优先接管按键(回车提交、Esc 取消、退格删除)。
|
||||||
if (m_panel.IsEditingWidth())
|
if (m_panel.IsEditing())
|
||||||
{
|
{
|
||||||
if (m_panel.OnEditKey(wParam))
|
if (m_panel.OnEditKey(wParam))
|
||||||
{
|
{
|
||||||
@@ -1640,8 +1645,8 @@ namespace jdi
|
|||||||
break;
|
break;
|
||||||
case WM_CHAR:
|
case WM_CHAR:
|
||||||
{
|
{
|
||||||
// 数值输入仅接受数字与小数点(或科学计数法符号),其余字符一律吞掉。
|
// 输入状态由对应控件自行校验可接受的字符(数值仅收数字与小数点,名称可收任意可打印字符)。
|
||||||
if (m_panel.IsEditingWidth())
|
if (m_panel.IsEditing())
|
||||||
{
|
{
|
||||||
if (m_panel.OnEditChar(static_cast<wchar_t>(wParam)))
|
if (m_panel.OnEditChar(static_cast<wchar_t>(wParam)))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -616,8 +616,7 @@ namespace jdi
|
|||||||
|
|
||||||
SignalRef m_hoverSignal; /**< 鼠标悬停命中的信号(曲线高亮)。 */
|
SignalRef m_hoverSignal; /**< 鼠标悬停命中的信号(曲线高亮)。 */
|
||||||
SignalRef m_dragSignal; /**< 正在被拖拽的信号。 */
|
SignalRef m_dragSignal; /**< 正在被拖拽的信号。 */
|
||||||
int m_dragTargetLane = -1; /**< 拖拽信号时的目标网格下标。 */
|
int m_dragTargetLane = -1; /**< 拖拽信号时的目标网格下标。 */ int m_dragMouseY = 0; /**< 拖拽过程中鼠标的当前纵坐标(虚影跟随)。 */
|
||||||
|
|
||||||
bool m_cursorHover = false; /**< 鼠标是否悬停在时间光标上。 */
|
bool m_cursorHover = false; /**< 鼠标是否悬停在时间光标上。 */
|
||||||
PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */
|
PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */
|
||||||
bool m_editFresh = false; /**< 输入框是否处于“首次输入整体替换”状态。 */
|
bool m_editFresh = false; /**< 输入框是否处于“首次输入整体替换”状态。 */
|
||||||
|
|||||||
@@ -1185,6 +1185,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
const int target = LaneAtY(y);
|
const int target = LaneAtY(y);
|
||||||
m_dragMoved = true;
|
m_dragMoved = true;
|
||||||
|
m_dragMouseY = y;
|
||||||
if (target != m_dragTargetLane)
|
if (target != m_dragTargetLane)
|
||||||
{
|
{
|
||||||
m_dragTargetLane = target;
|
m_dragTargetLane = target;
|
||||||
@@ -1883,7 +1884,7 @@ namespace jdi
|
|||||||
std::wstring label = BuildLegendText(*resolved.data);
|
std::wstring label = BuildLegendText(*resolved.data);
|
||||||
if (multiFile && resolved.file != nullptr)
|
if (multiFile && resolved.file != nullptr)
|
||||||
{
|
{
|
||||||
label += L" · " + resolved.file->fileName;
|
label += L" · " + resolved.file->Title();
|
||||||
}
|
}
|
||||||
const float itemWidth = 13.0f * m_scale + UiTextWidth(g, label, m_fonts->uiSmall) +
|
const float itemWidth = 13.0f * m_scale + UiTextWidth(g, label, m_fonts->uiSmall) +
|
||||||
14.0f * m_scale;
|
14.0f * m_scale;
|
||||||
@@ -1915,7 +1916,7 @@ namespace jdi
|
|||||||
std::wstring label = BuildLegendText(*resolved.data);
|
std::wstring label = BuildLegendText(*resolved.data);
|
||||||
if (multiFile && resolved.file != nullptr)
|
if (multiFile && resolved.file != nullptr)
|
||||||
{
|
{
|
||||||
label += L" · " + resolved.file->fileName;
|
label += L" · " + resolved.file->Title();
|
||||||
}
|
}
|
||||||
const float textWidth = UiTextWidth(g, label, m_fonts->uiSmall);
|
const float textWidth = UiTextWidth(g, label, m_fonts->uiSmall);
|
||||||
const float itemWidth = 13.0f * m_scale + textWidth + 14.0f * m_scale;
|
const float itemWidth = 13.0f * m_scale + textWidth + 14.0f * m_scale;
|
||||||
@@ -1992,10 +1993,10 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
ReadoutLine line;
|
ReadoutLine line;
|
||||||
line.text = resolved.data->name;
|
line.text = resolved.data->Title();
|
||||||
if (multiFile)
|
if (multiFile)
|
||||||
{
|
{
|
||||||
line.text += L" · " + resolved.file->fileName;
|
line.text += L" · " + resolved.file->Title();
|
||||||
}
|
}
|
||||||
line.text += L" = " + FormatValue(value);
|
line.text += L" = " + FormatValue(value);
|
||||||
lines.push_back(std::move(line));
|
lines.push_back(std::move(line));
|
||||||
@@ -2226,55 +2227,75 @@ namespace jdi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖拽中:在目标网格内以虚线绘制该信号的“虚影”,并高亮目标网格。
|
// 拖拽中:虚影跟随鼠标纵向位移,同时在目标网格内高亮落点。
|
||||||
if (m_dragMode == PlotDragMode::MOVE_SIGNAL && m_dragSignal.IsValid())
|
if (m_dragMode == PlotDragMode::MOVE_SIGNAL && m_dragSignal.IsValid())
|
||||||
{
|
{
|
||||||
const int target = (m_dragTargetLane >= 0) ? m_dragTargetLane : m_tab->LaneOfSignal(m_dragSignal);
|
const ResolvedSignal resolved = ResolveSignal(m_dragSignal);
|
||||||
|
const int sourceLane = m_tab->LaneOfSignal(m_dragSignal);
|
||||||
|
if (resolved.data == nullptr || resolved.style == nullptr || sourceLane < 0 ||
|
||||||
|
sourceLane >= static_cast<int>(m_lanes.size()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 虚影:以源网格的幅值映射构建波形,并整体平移跟随鼠标,形成“拖着曲线走”的观感。
|
||||||
|
LaneVisual ghostLane = m_lanes[static_cast<std::size_t>(sourceLane)];
|
||||||
|
const float follow = static_cast<float>(m_dragMouseY - m_dragStartY);
|
||||||
|
ghostLane.rect.Y += follow;
|
||||||
|
|
||||||
|
{
|
||||||
|
const float clipTop = std::max(m_plotRect.Y, m_plotRect.Y);
|
||||||
|
const float clipHeight = m_plotRect.Height;
|
||||||
|
const int saveState = g.Save();
|
||||||
|
g.SetClip(gd::RectF(m_plotRect.X, clipTop, m_plotRect.Width, clipHeight),
|
||||||
|
gd::CombineModeIntersect);
|
||||||
|
|
||||||
|
std::vector<gd::PointF> points;
|
||||||
|
std::vector<std::size_t> breaks;
|
||||||
|
BuildPolyline(resolved, ghostLane, points, breaks);
|
||||||
|
|
||||||
|
gd::Pen ghost(ToGdiColor(WithAlpha(m_theme->accent, 0xC0)),
|
||||||
|
std::max(1.8f, resolved.style->lineWidth) * m_scale);
|
||||||
|
ghost.SetDashStyle(gd::DashStyleDash);
|
||||||
|
ghost.SetStartCap(gd::LineCapRound);
|
||||||
|
ghost.SetEndCap(gd::LineCapRound);
|
||||||
|
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||||
|
{
|
||||||
|
const std::size_t start = breaks[k];
|
||||||
|
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||||
|
if (stop <= start + 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
g.DrawLines(&ghost, points.data() + start, static_cast<INT>(stop - start));
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Restore(saveState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 落点提示:高亮鼠标所在网格并给出文字说明。
|
||||||
|
const int target = (m_dragTargetLane >= 0) ? m_dragTargetLane : sourceLane;
|
||||||
if (target >= 0 && target < static_cast<int>(m_lanes.size()))
|
if (target >= 0 && target < static_cast<int>(m_lanes.size()))
|
||||||
{
|
{
|
||||||
const LaneVisual& lane = m_lanes[static_cast<std::size_t>(target)];
|
const gd::RectF& targetRect = m_lanes[static_cast<std::size_t>(target)].rect;
|
||||||
const ResolvedSignal resolved = ResolveSignal(m_dragSignal);
|
const float visibleTop = std::max(targetRect.Y, m_plotRect.Y);
|
||||||
if (resolved.data != nullptr && resolved.style != nullptr)
|
const float visibleBottom = std::min(targetRect.Y + targetRect.Height,
|
||||||
|
m_plotRect.Y + m_plotRect.Height);
|
||||||
|
if (visibleBottom > visibleTop)
|
||||||
{
|
{
|
||||||
const int saveState = g.Save();
|
|
||||||
g.SetClip(gd::RectF(m_plotRect.X, std::max(lane.rect.Y, m_plotRect.Y), m_plotRect.Width,
|
|
||||||
std::min(lane.rect.Y + lane.rect.Height,
|
|
||||||
m_plotRect.Y + m_plotRect.Height) -
|
|
||||||
std::max(lane.rect.Y, m_plotRect.Y)),
|
|
||||||
gd::CombineModeIntersect);
|
|
||||||
|
|
||||||
// 目标网格边框高亮。
|
|
||||||
gd::Pen frame(ToGdiColor(m_theme->accent), 2.0f);
|
gd::Pen frame(ToGdiColor(m_theme->accent), 2.0f);
|
||||||
g.DrawRectangle(&frame, lane.rect.X, lane.rect.Y, lane.rect.Width, lane.rect.Height);
|
g.DrawRectangle(&frame, targetRect.X, visibleTop, targetRect.Width,
|
||||||
|
visibleBottom - visibleTop);
|
||||||
|
|
||||||
std::vector<gd::PointF> points;
|
const std::wstring hint = (target == sourceLane)
|
||||||
std::vector<std::size_t> breaks;
|
? std::wstring(L"松开左键放回本网格")
|
||||||
BuildPolyline(resolved, lane, points, breaks);
|
: (L"移动到网格 " + std::to_wstring(target + 1) +
|
||||||
|
L":松开左键确认");
|
||||||
gd::Pen ghost(ToGdiColor(WithAlpha(m_theme->accent, 0xB0)),
|
|
||||||
std::max(1.6f, resolved.style->lineWidth) * m_scale);
|
|
||||||
ghost.SetDashStyle(gd::DashStyleDash);
|
|
||||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
|
||||||
{
|
|
||||||
const std::size_t start = breaks[k];
|
|
||||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
|
||||||
if (stop <= start + 1)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
g.DrawLines(&ghost, points.data() + start, static_cast<INT>(stop - start));
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Restore(saveState);
|
|
||||||
|
|
||||||
// 目标网格名称提示。
|
|
||||||
const std::wstring hint =
|
|
||||||
L"移动到网格 " + std::to_wstring(target + 1) + L":松开左键确认";
|
|
||||||
const float textWidth = UiTextWidth(g, hint, m_fonts->uiSmall) + 16.0f * m_scale;
|
const float textWidth = UiTextWidth(g, hint, m_fonts->uiSmall) + 16.0f * m_scale;
|
||||||
const float height = 20.0f * m_scale;
|
const float height = 20.0f * m_scale;
|
||||||
float left = lane.rect.X + (lane.rect.Width - textWidth) * 0.5f;
|
float left = targetRect.X + (targetRect.Width - textWidth) * 0.5f;
|
||||||
left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - textWidth));
|
left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - textWidth));
|
||||||
const gd::RectF pill(left, lane.rect.Y + 4.0f * m_scale, textWidth, height);
|
const gd::RectF pill(left, visibleTop + 4.0f * m_scale, textWidth, height);
|
||||||
UiFillRoundRect(g, pill, 3.0f * m_scale, m_theme->accent);
|
UiFillRoundRect(g, pill, 3.0f * m_scale, m_theme->accent);
|
||||||
UiText(g, hint, m_fonts->uiSmall, pill, m_theme->textOnAccent, TextAlign::CENTER);
|
UiText(g, hint, m_fonts->uiSmall, pill, m_theme->textOnAccent, TextAlign::CENTER);
|
||||||
}
|
}
|
||||||
@@ -2299,7 +2320,7 @@ 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->name + 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;
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ namespace jdi
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绑定数据层与当前标签页配置。
|
* @brief 绑定数据层与当前标签页配置。
|
||||||
* @param [IN] dataSet 数据集合,允许为空。
|
* @param [IN] dataSet 数据集合(可写,用于重命名信号与分组)。
|
||||||
* @param [IN] tab 当前标签页配置,允许为空。
|
* @param [IN] tab 当前标签页配置,允许为空。
|
||||||
*/
|
*/
|
||||||
void SetContext(const DataSet* dataSet, TabConfig* tab);
|
void SetContext(DataSet* dataSet, TabConfig* tab);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 设置宿主客户区矩形(用于约束浮动面板位置)。
|
* @brief 设置宿主客户区矩形(用于约束浮动面板位置)。
|
||||||
@@ -163,20 +163,20 @@ namespace jdi
|
|||||||
bool OnMenuCommand(const MenuResult& result);
|
bool OnMenuCommand(const MenuResult& result);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 判断线宽数值框是否处于输入状态。
|
* @brief 判断面板是否处于内联文本输入状态。
|
||||||
* @return 正在输入返回 true。
|
* @return 正在输入返回 true。
|
||||||
*/
|
*/
|
||||||
bool IsEditingWidth() const { return m_widthEditing; }
|
bool IsEditing() const { return m_editField != StyleEditField::NONE; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 向线宽输入框送入一个字符。
|
* @brief 向当前输入框送入一个字符。
|
||||||
* @param [IN] character 输入字符('\r' 提交、'\b' 退格)。
|
* @param [IN] character 输入字符('\r' 提交、'\b' 退格、0x03 复制)。
|
||||||
* @return 界面状态发生变化返回 true。
|
* @return 界面状态发生变化返回 true。
|
||||||
*/
|
*/
|
||||||
bool OnEditChar(wchar_t character);
|
bool OnEditChar(wchar_t character);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 向线宽输入框送入一个按键。
|
* @brief 向当前输入框送入一个按键。
|
||||||
* @param [IN] key 虚拟键码。
|
* @param [IN] key 虚拟键码。
|
||||||
* @return 界面状态发生变化返回 true。
|
* @return 界面状态发生变化返回 true。
|
||||||
*/
|
*/
|
||||||
@@ -216,6 +216,18 @@ namespace jdi
|
|||||||
void ScrollToSignal(const SignalRef& ref);
|
void ScrollToSignal(const SignalRef& ref);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/**
|
||||||
|
* @brief 样式区与列表行中可进行内联文本编辑的字段。
|
||||||
|
*/
|
||||||
|
enum class StyleEditField : int
|
||||||
|
{
|
||||||
|
NONE = 0, /**< 未处于编辑状态。 */
|
||||||
|
WIDTH = 1, /**< 线宽数值。 */
|
||||||
|
OPACITY = 2, /**< 不透明度数值(百分比)。 */
|
||||||
|
SIGNAL_NAME = 3, /**< 信号显示名称(列表行内)。 */
|
||||||
|
FILE_NAME = 4 /**< 分组显示名称(列表行内)。 */
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 列表行的类型。
|
* @brief 列表行的类型。
|
||||||
*/
|
*/
|
||||||
@@ -243,14 +255,15 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
enum class StyleHit : int
|
enum class StyleHit : int
|
||||||
{
|
{
|
||||||
NONE = 0, /**< 未命中。 */
|
NONE = 0, /**< 未命中。 */
|
||||||
COLOR, /**< 预设颜色色块。 */
|
COLOR, /**< 预设颜色色块。 */
|
||||||
CUSTOM_CHIP, /**< 自定义颜色入口。 */
|
CUSTOM_CHIP, /**< 自定义颜色入口。 */
|
||||||
MODE, /**< 绘制样式按钮。 */
|
MODE, /**< 绘制样式按钮。 */
|
||||||
OPACITY_SLIDER, /**< 不透明度滑块。 */
|
OPACITY_SLIDER, /**< 不透明度滑块。 */
|
||||||
WIDTH_SLIDER, /**< 线宽滑块。 */
|
OPACITY_VALUE, /**< 不透明度数值框(可点击输入)。 */
|
||||||
WIDTH_VALUE, /**< 线宽数值框(可双击输入)。 */
|
WIDTH_SLIDER, /**< 线宽滑块。 */
|
||||||
TOGGLE_ALL /**< 全部显示/隐藏按钮。 */
|
WIDTH_VALUE, /**< 线宽数值框(可点击输入)。 */
|
||||||
|
TOGGLE_ALL /**< 全部显示/隐藏按钮。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -407,17 +420,39 @@ namespace jdi
|
|||||||
int TotalSignalCount() const;
|
int TotalSignalCount() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 进入线宽数值输入状态。
|
* @brief 进入指定字段的内联输入状态。
|
||||||
|
* @param [IN] field 目标字段。
|
||||||
|
* @param [IN] row 列表行下标(仅行内字段有效)。
|
||||||
* @return 状态发生变化返回 true。
|
* @return 状态发生变化返回 true。
|
||||||
*/
|
*/
|
||||||
bool BeginWidthEdit();
|
bool BeginEdit(StyleEditField field, int row = -1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 结束线宽数值输入状态。
|
* @brief 结束内联输入状态。
|
||||||
* @param [IN] commit 是否提交输入值。
|
* @param [IN] commit 是否提交输入值。
|
||||||
* @return 状态或取值发生变化返回 true。
|
* @return 状态或取值发生变化返回 true。
|
||||||
*/
|
*/
|
||||||
bool EndWidthEdit(bool commit);
|
bool EndEdit(bool commit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算当前输入框的矩形。
|
||||||
|
* @return 输入框矩形,未处于编辑状态时返回空矩形。
|
||||||
|
*/
|
||||||
|
gd::RectF EditFieldRect() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算指定行中名称区域的矩形(列表行内联编辑使用)。
|
||||||
|
* @param [IN] row 行下标。
|
||||||
|
* @return 名称区域矩形。
|
||||||
|
*/
|
||||||
|
gd::RectF RowNameRect(int row) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算分组行中“组X”标签的像素宽度。
|
||||||
|
* @param [IN] row 行下标。
|
||||||
|
* @return 标签宽度。
|
||||||
|
*/
|
||||||
|
float FileGroupLabelWidth(int row) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 调整当前信号不透明度。
|
* @brief 调整当前信号不透明度。
|
||||||
@@ -438,7 +473,8 @@ namespace jdi
|
|||||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||||
PopupMenu* m_menu = nullptr; /**< 共用弹出菜单。 */
|
PopupMenu* m_menu = nullptr; /**< 共用弹出菜单。 */
|
||||||
ColorPickerPopup* m_colorPicker = nullptr; /**< 共用浮动拾色器。 */
|
ColorPickerPopup* m_colorPicker = nullptr; /**< 共用浮动拾色器。 */
|
||||||
const DataSet* m_dataSet = nullptr; /**< 数据集合。 */
|
const DataSet* m_dataSet = nullptr; /**< 数据集合(只读访问)。 */
|
||||||
|
DataSet* m_dataSetWritable = nullptr; /**< 数据集合(重命名等写入操作)。 */
|
||||||
TabConfig* m_tab = nullptr; /**< 当前标签页配置。 */
|
TabConfig* m_tab = nullptr; /**< 当前标签页配置。 */
|
||||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||||
gd::RectF m_clientBounds; /**< 宿主客户区矩形。 */
|
gd::RectF m_clientBounds; /**< 宿主客户区矩形。 */
|
||||||
@@ -481,9 +517,10 @@ namespace jdi
|
|||||||
StyleHit m_hoverStyle = StyleHit::NONE; /**< 当前悬停的样式条目类型。 */
|
StyleHit m_hoverStyle = StyleHit::NONE; /**< 当前悬停的样式条目类型。 */
|
||||||
int m_hoverStyleIndex = -1; /**< 当前悬停的样式条目下标。 */
|
int m_hoverStyleIndex = -1; /**< 当前悬停的样式条目下标。 */
|
||||||
|
|
||||||
bool m_widthEditing = false; /**< 线宽数值是否处于输入状态。 */
|
StyleEditField m_editField = StyleEditField::NONE; /**< 当前处于输入状态的字段。 */
|
||||||
bool m_widthEditFresh = false; /**< 输入缓冲是否仍为初始值(首次键入应整体替换)。 */
|
int m_editRow = -1; /**< 行内输入所作用的列表行下标。 */
|
||||||
std::wstring m_widthEditText; /**< 线宽输入缓冲。 */
|
bool m_editFresh = false; /**< 输入缓冲是否仍为初始值(首次键入应整体替换)。 */
|
||||||
|
std::wstring m_editText; /**< 输入缓冲。 */
|
||||||
|
|
||||||
bool m_pendingLaneMenu = false; /**< 左键抬起时是否弹出网格归属菜单。 */
|
bool m_pendingLaneMenu = false; /**< 左键抬起时是否弹出网格归属菜单。 */
|
||||||
SignalRef m_pendingLaneRef; /**< 待弹出菜单的目标信号。 */
|
SignalRef m_pendingLaneRef; /**< 待弹出菜单的目标信号。 */
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ namespace jdi
|
|||||||
const float kIndent = 14.0f; /**< 信号行相对文件行的缩进。 */
|
const float kIndent = 14.0f; /**< 信号行相对文件行的缩进。 */
|
||||||
const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */
|
const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */
|
||||||
const float kCloseButtonSize = 18.0f; /**< 文件行关闭按钮尺寸。 */
|
const float kCloseButtonSize = 18.0f; /**< 文件行关闭按钮尺寸。 */
|
||||||
|
const float kGroupSeparatorGap = 7.0f; /**< 分组标签与分隔线/名称的间距。 */
|
||||||
|
const float kGroupSeparatorWidth = 1.0f; /**< 分组标签右侧分隔线宽度。 */
|
||||||
|
|
||||||
// 样式编辑区:固定四行网格(颜色 / 样式 / 透明度 / 线宽),行高、间距与内容列宽度统一,
|
// 样式编辑区:固定四行网格(颜色 / 样式 / 透明度 / 线宽),行高、间距与内容列宽度统一,
|
||||||
// 保证面板宽度变化时四行控件始终对齐,且不会被无限制拉伸。
|
// 保证面板宽度变化时四行控件始终对齐,且不会被无限制拉伸。
|
||||||
@@ -62,36 +64,6 @@ namespace jdi
|
|||||||
track.Height + 18.0f * scale);
|
track.Height + 18.0f * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 绘制水平滑块(轨道 + 已填充部分 + 圆钮)。
|
|
||||||
* @param [IN,OUT] g 绘图上下文。
|
|
||||||
* @param [IN] track 轨道矩形。
|
|
||||||
* @param [IN] ratio 当前值比例(0-1)。
|
|
||||||
* @param [IN] fillColor 已填充部分颜色。
|
|
||||||
* @param [IN] active 是否处于悬停或拖拽状态。
|
|
||||||
* @param [IN] scale 缩放比例。
|
|
||||||
*/
|
|
||||||
void DrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
|
||||||
bool active, float scale)
|
|
||||||
{
|
|
||||||
const float radius = track.Height * 0.5f;
|
|
||||||
UiFillRoundRect(g, track, radius, 0x2A657B83u);
|
|
||||||
|
|
||||||
ratio = std::max(0.0, std::min(1.0, ratio));
|
|
||||||
const float fillWidth = std::max(track.Height, static_cast<float>(ratio) * track.Width);
|
|
||||||
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
|
|
||||||
|
|
||||||
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
|
|
||||||
const float knobRadius = radius + (active ? 2.5f * scale : 0.8f * scale);
|
|
||||||
|
|
||||||
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
|
|
||||||
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
|
|
||||||
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
|
||||||
knobRadius * 2.0f);
|
|
||||||
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
|
||||||
knobRadius * 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 判断颜色是否偏暗(用于在色块之上选择可读的图标颜色)。
|
* @brief 判断颜色是否偏暗(用于在色块之上选择可读的图标颜色)。
|
||||||
* @param [IN] color 0xAARRGGBB 颜色值。
|
* @param [IN] color 0xAARRGGBB 颜色值。
|
||||||
@@ -134,16 +106,19 @@ namespace jdi
|
|||||||
m_colorPicker = colorPicker;
|
m_colorPicker = colorPicker;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SignalPanel::SetContext(const DataSet* dataSet, TabConfig* tab)
|
void SignalPanel::SetContext(DataSet* dataSet, TabConfig* tab)
|
||||||
{
|
{
|
||||||
m_dataSet = dataSet;
|
m_dataSet = dataSet;
|
||||||
|
m_dataSetWritable = dataSet;
|
||||||
m_tab = tab;
|
m_tab = tab;
|
||||||
m_rowsDirty = true;
|
m_rowsDirty = true;
|
||||||
m_scrollOffset = 0.0f;
|
m_scrollOffset = 0.0f;
|
||||||
m_draggingOpacity = false;
|
m_draggingOpacity = false;
|
||||||
m_draggingWidth = false;
|
m_draggingWidth = false;
|
||||||
m_widthEditing = false;
|
m_editField = StyleEditField::NONE;
|
||||||
m_widthEditText.clear();
|
m_editRow = -1;
|
||||||
|
m_editFresh = false;
|
||||||
|
m_editText.clear();
|
||||||
m_pendingLaneMenu = false;
|
m_pendingLaneMenu = false;
|
||||||
m_pendingColorPicker = false;
|
m_pendingColorPicker = false;
|
||||||
}
|
}
|
||||||
@@ -682,22 +657,39 @@ namespace jdi
|
|||||||
|
|
||||||
const gd::RectF closeRect = FileCloseRect(row);
|
const gd::RectF closeRect = FileCloseRect(row);
|
||||||
|
|
||||||
// 分组标题:以“组X | <文件名>”呈现,X 为该组在当前列表中的序号。
|
// 分组标题:左侧为“组X”标签,右侧以绘制的竖线分隔分组名(不使用字符),再显示可重命名的名称。
|
||||||
const std::wstring groupLabel = L"组" + std::to_wstring(item.fileIndex + 1) + L" | ";
|
const std::wstring groupLabel = L"组" + std::to_wstring(item.fileIndex + 1);
|
||||||
const std::wstring stats = std::to_wstring(file->signals.size()) + L" 项";
|
const std::wstring stats = std::to_wstring(file->signals.size()) + L" 项";
|
||||||
const float groupWidth = UiMeasureTextWidth(m_fonts->uiBold, groupLabel);
|
const float groupWidth = FileGroupLabelWidth(row);
|
||||||
const float statsWidth = UiMeasureTextWidth(m_fonts->uiSmall, stats);
|
const float statsWidth = UiMeasureTextWidth(m_fonts->uiSmall, stats);
|
||||||
const float statsRight = closeRect.X - 6.0f * m_scale;
|
const float statsRight = closeRect.X - 6.0f * m_scale;
|
||||||
const float nameLeft = checkRect.X + checkRect.Width + 8.0f * m_scale;
|
const float nameLeft = checkRect.X + checkRect.Width + 8.0f * m_scale;
|
||||||
|
const float nameAreaLeft =
|
||||||
|
nameLeft + groupWidth + kGroupSeparatorGap * 2.0f * m_scale + kGroupSeparatorWidth * m_scale;
|
||||||
const float nameWidth =
|
const float nameWidth =
|
||||||
std::max(20.0f, statsRight - statsWidth - 6.0f * m_scale - nameLeft - groupWidth);
|
std::max(20.0f, statsRight - statsWidth - 6.0f * m_scale - nameAreaLeft);
|
||||||
|
|
||||||
// 序号以强调色突出,便于在多组之间快速定位。
|
// 序号以强调色突出,便于在多组之间快速定位。
|
||||||
UiText(g, groupLabel, m_fonts->uiBold,
|
UiText(g, groupLabel, m_fonts->uiBold, gd::RectF(nameLeft, rect.Y, groupWidth, rect.Height),
|
||||||
gd::RectF(nameLeft, rect.Y, groupWidth, rect.Height), m_theme->accent, TextAlign::LEFT);
|
m_theme->accent, TextAlign::LEFT);
|
||||||
UiText(g, UiEllipsize(g, file->fileName, m_fonts->uiBold, nameWidth), m_fonts->uiBold,
|
|
||||||
gd::RectF(nameLeft + groupWidth, rect.Y, nameWidth, rect.Height), m_theme->textStrong,
|
// 竖线分隔符:以绘制方式实现,与文字基线无关,排版更稳。
|
||||||
TextAlign::LEFT);
|
UiFillRect(g, gd::RectF(nameLeft + groupWidth + kGroupSeparatorGap * m_scale,
|
||||||
|
rect.Y + 7.0f * m_scale, kGroupSeparatorWidth * m_scale,
|
||||||
|
rect.Height - 14.0f * m_scale),
|
||||||
|
WithAlpha(m_theme->borderStrong, 0xB0));
|
||||||
|
|
||||||
|
if (m_editField == StyleEditField::FILE_NAME && m_editRow == row)
|
||||||
|
{
|
||||||
|
UiDrawEditField(g, RowNameRect(row), m_editText, m_editFresh, m_fonts->uiSmall, m_scale);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UiText(g, UiEllipsize(g, file->Title(), m_fonts->uiBold, nameWidth), m_fonts->uiBold,
|
||||||
|
gd::RectF(nameAreaLeft, rect.Y, nameWidth, rect.Height), m_theme->textStrong,
|
||||||
|
TextAlign::LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
UiText(g, stats, m_fonts->uiSmall, gd::RectF(statsRight - statsWidth, rect.Y, statsWidth, rect.Height),
|
UiText(g, stats, m_fonts->uiSmall, gd::RectF(statsRight - statsWidth, rect.Y, statsWidth, rect.Height),
|
||||||
m_theme->textMuted, TextAlign::RIGHT);
|
m_theme->textMuted, TextAlign::RIGHT);
|
||||||
|
|
||||||
@@ -766,12 +758,19 @@ namespace jdi
|
|||||||
nameRightLimit = badgeRect.X - 6.0f * m_scale;
|
nameRightLimit = badgeRect.X - 6.0f * m_scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 名称:完全保留文件内的列名(含 "[n]" 序号与括号描述)。
|
// 名称:完全保留文件内的列名(含 "[n]" 序号与括号描述),双击可重命名。
|
||||||
const float nameLeft = swatchRect.X + swatchRect.Width + 6.0f * m_scale;
|
const float nameLeft = swatchRect.X + swatchRect.Width + 6.0f * m_scale;
|
||||||
const float nameWidth = std::max(16.0f, nameRightLimit - nameLeft);
|
if (m_editField == StyleEditField::SIGNAL_NAME && m_editRow == row)
|
||||||
const std::uint32_t nameColor = style->visible ? m_theme->text : m_theme->textMuted;
|
{
|
||||||
UiText(g, UiEllipsize(g, signal->name, m_fonts->ui, nameWidth), m_fonts->ui,
|
UiDrawEditField(g, RowNameRect(row), m_editText, m_editFresh, m_fonts->ui, m_scale);
|
||||||
gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), nameColor, TextAlign::LEFT);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const float nameWidth = std::max(16.0f, nameRightLimit - nameLeft);
|
||||||
|
const std::uint32_t nameColor = style->visible ? m_theme->text : m_theme->textMuted;
|
||||||
|
UiText(g, UiEllipsize(g, signal->Title(), m_fonts->ui, nameWidth), m_fonts->ui,
|
||||||
|
gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), nameColor, TextAlign::LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
if (badgeVisible)
|
if (badgeVisible)
|
||||||
{
|
{
|
||||||
@@ -830,7 +829,6 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const float labelWidth = kStyleLabelWidth * m_scale;
|
const float labelWidth = kStyleLabelWidth * m_scale;
|
||||||
const float rowHeight = kStyleRowHeight * m_scale;
|
|
||||||
|
|
||||||
// 行标签统一以整行为基准垂直居中,确保与同行控件纵向对齐。
|
// 行标签统一以整行为基准垂直居中,确保与同行控件纵向对齐。
|
||||||
auto paintRowLabel = [&](int rowIndex, const wchar_t* text)
|
auto paintRowLabel = [&](int rowIndex, const wchar_t* text)
|
||||||
@@ -906,65 +904,38 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 第三行:透明度 ----
|
// ---- 第三行:透明度(数值可点击输入)----
|
||||||
{
|
{
|
||||||
paintRowLabel(2, L"透明度");
|
paintRowLabel(2, L"透明度");
|
||||||
|
|
||||||
const bool active = m_draggingOpacity || (m_hoverStyle == StyleHit::OPACITY_SLIDER);
|
const bool active = m_draggingOpacity || (m_hoverStyle == StyleHit::OPACITY_SLIDER);
|
||||||
DrawSlider(g, m_opacityTrackRect, static_cast<double>(style->opacity), m_theme->accent, active,
|
UiDrawSlider(g, m_opacityTrackRect, static_cast<double>(style->opacity), m_theme->accent, active,
|
||||||
m_scale);
|
m_scale);
|
||||||
|
|
||||||
const int percent = static_cast<int>(style->opacity * 100.0f + 0.5f);
|
if (m_editField == StyleEditField::OPACITY)
|
||||||
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect,
|
{
|
||||||
m_theme->textStrong, TextAlign::RIGHT);
|
UiDrawEditField(g, m_opacityValueRect, m_editText, m_editFresh, m_fonts->monoSmall, m_scale);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const int percent = static_cast<int>(style->opacity * 100.0f + 0.5f);
|
||||||
|
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect,
|
||||||
|
m_theme->textStrong, TextAlign::RIGHT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 第四行:线宽(与透明度行几何一致)----
|
// ---- 第四行:线宽(与透明度行几何一致,数值可点击输入)----
|
||||||
{
|
{
|
||||||
paintRowLabel(3, L"线宽");
|
paintRowLabel(3, L"线宽");
|
||||||
|
|
||||||
const double ratio =
|
const double ratio =
|
||||||
(static_cast<double>(style->lineWidth) - kWidthMin) / (kWidthMax - kWidthMin);
|
(static_cast<double>(style->lineWidth) - kWidthMin) / (kWidthMax - kWidthMin);
|
||||||
const bool active = m_draggingWidth || (m_hoverStyle == StyleHit::WIDTH_SLIDER);
|
const bool active = m_draggingWidth || (m_hoverStyle == StyleHit::WIDTH_SLIDER);
|
||||||
DrawSlider(g, m_widthTrackRect, ratio, m_theme->accent, active, m_scale);
|
UiDrawSlider(g, m_widthTrackRect, ratio, m_theme->accent, active, m_scale);
|
||||||
|
|
||||||
if (m_widthEditing)
|
if (m_editField == StyleEditField::WIDTH)
|
||||||
{
|
{
|
||||||
// 输入状态:数值整体居中;处于“全部选中”状态时以高亮背景表示内容将被覆盖,
|
UiDrawEditField(g, m_widthValueRect, m_editText, m_editFresh, m_fonts->monoSmall, m_scale);
|
||||||
// 交互观感与常见登录界面的账号/密码输入框一致。
|
|
||||||
UiFillRoundRect(g, m_widthValueRect, kCorner * m_scale, m_theme->cardBg);
|
|
||||||
UiDrawRoundRect(g, m_widthValueRect, kCorner * m_scale, m_theme->accent, 1.6f);
|
|
||||||
|
|
||||||
const std::wstring editText = m_widthEditText.empty() ? std::wstring(L" ") : m_widthEditText;
|
|
||||||
const float textWidth = UiTextWidth(g, editText, m_fonts->monoSmall);
|
|
||||||
const float textX =
|
|
||||||
m_widthValueRect.X + std::max(2.0f * m_scale, (m_widthValueRect.Width - textWidth) * 0.5f);
|
|
||||||
|
|
||||||
if (m_widthEditFresh)
|
|
||||||
{
|
|
||||||
// 高亮选区:覆盖整段数值,提示首次键入即整体替换。
|
|
||||||
const float padX = 3.0f * m_scale;
|
|
||||||
UiFillRoundRect(g, gd::RectF(textX - padX, m_widthValueRect.Y + 4.0f * m_scale,
|
|
||||||
textWidth + padX * 2.0f,
|
|
||||||
m_widthValueRect.Height - 8.0f * m_scale),
|
|
||||||
2.0f * m_scale, m_theme->accent);
|
|
||||||
UiText(g, editText, m_fonts->monoSmall, m_widthValueRect, m_theme->textOnAccent,
|
|
||||||
TextAlign::CENTER);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UiText(g, editText, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong,
|
|
||||||
TextAlign::CENTER);
|
|
||||||
|
|
||||||
// 已开始键入:在文本末尾显示插入符。
|
|
||||||
const float caretX = textX + textWidth + 1.0f * m_scale;
|
|
||||||
if (caretX < m_widthValueRect.X + m_widthValueRect.Width - 2.0f * m_scale)
|
|
||||||
{
|
|
||||||
UiDrawLine(g, caretX, m_widthValueRect.Y + 5.0f * m_scale, caretX,
|
|
||||||
m_widthValueRect.Y + rowHeight - 5.0f * m_scale, m_theme->accent,
|
|
||||||
1.4f * m_scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1013,115 +984,304 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SignalPanel::BeginWidthEdit()
|
gd::RectF SignalPanel::RowNameRect(int row) const
|
||||||
{
|
{
|
||||||
if (m_tab == nullptr)
|
if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||||
|
{
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
|
|
||||||
|
const gd::RectF rect = RowRect(row);
|
||||||
|
const bool isFile = (m_rows[static_cast<std::size_t>(row)].kind == RowKind::FILE);
|
||||||
|
const float baseX = rect.X + kPadding * m_scale + (isFile ? 0.0f : kIndent * m_scale);
|
||||||
|
const float checkRight = baseX + kDisclosureWidth * m_scale + kCheckSize * m_scale;
|
||||||
|
|
||||||
|
float left = 0.0f;
|
||||||
|
float right = 0.0f;
|
||||||
|
if (isFile)
|
||||||
|
{
|
||||||
|
// 分组名需要跳过左侧的“组X”标签与分隔线。
|
||||||
|
left = checkRight + 8.0f * m_scale + FileGroupLabelWidth(row) + kGroupSeparatorGap * 2.0f * m_scale +
|
||||||
|
kGroupSeparatorWidth * m_scale;
|
||||||
|
right = FileCloseRect(row).X - 6.0f * m_scale;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
left = checkRight + 6.0f * m_scale + kSwatchSize * m_scale + 6.0f * m_scale;
|
||||||
|
right = SignalBadgeRect(row).X - 6.0f * m_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right <= left)
|
||||||
|
{
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
|
return gd::RectF(left, rect.Y + 3.0f * m_scale, right - left, rect.Height - 6.0f * m_scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
float SignalPanel::FileGroupLabelWidth(int row) const
|
||||||
|
{
|
||||||
|
if (m_fonts == nullptr || m_fonts->uiBold == nullptr || row < 0 ||
|
||||||
|
row >= static_cast<int>(m_rows.size()))
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
const std::wstring label = L"组" + std::to_wstring(m_rows[static_cast<std::size_t>(row)].fileIndex + 1);
|
||||||
|
return UiMeasureTextWidth(m_fonts->uiBold, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SignalPanel::BeginEdit(StyleEditField field, int row)
|
||||||
|
{
|
||||||
|
if (m_tab == nullptr || field == StyleEditField::NONE)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const SignalStyle* style = m_tab->Style(m_tab->Selected());
|
if (m_editField == field && m_editRow == row)
|
||||||
if (style == nullptr)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (m_widthEditing)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_widthEditText = FormatNumber(style->lineWidth, 1);
|
std::wstring initial;
|
||||||
m_widthEditing = true;
|
switch (field)
|
||||||
m_widthEditFresh = true;
|
{
|
||||||
|
case StyleEditField::WIDTH:
|
||||||
|
{
|
||||||
|
const SignalStyle* style = m_tab->Style(m_tab->Selected());
|
||||||
|
if (style == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
initial = FormatNumber(style->lineWidth, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case StyleEditField::OPACITY:
|
||||||
|
{
|
||||||
|
const SignalStyle* style = m_tab->Style(m_tab->Selected());
|
||||||
|
if (style == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
initial = std::to_wstring(static_cast<int>(style->opacity * 100.0f + 0.5f));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case StyleEditField::SIGNAL_NAME:
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||||
|
const SignalData* signal = m_dataSet->Resolve(SignalRef{item.fileIndex, item.signalIndex});
|
||||||
|
if (signal == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
initial = signal->Title();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case StyleEditField::FILE_NAME:
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const DataFile* file = m_dataSet->FileAt(m_rows[static_cast<std::size_t>(row)].fileIndex);
|
||||||
|
if (file == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
initial = file->Title();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_editField = field;
|
||||||
|
m_editRow = row;
|
||||||
|
m_editText = std::move(initial);
|
||||||
|
m_editFresh = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SignalPanel::EndWidthEdit(bool commit)
|
bool SignalPanel::EndEdit(bool commit)
|
||||||
{
|
{
|
||||||
if (!m_widthEditing)
|
if (m_editField == StyleEditField::NONE)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool changed = true;
|
const StyleEditField field = m_editField;
|
||||||
if (commit && m_tab != nullptr)
|
const int row = m_editRow;
|
||||||
|
const std::wstring text = m_editText;
|
||||||
|
|
||||||
|
m_editField = StyleEditField::NONE;
|
||||||
|
m_editRow = -1;
|
||||||
|
m_editFresh = false;
|
||||||
|
m_editText.clear();
|
||||||
|
|
||||||
|
if (!commit || m_tab == nullptr || m_dataSet == nullptr)
|
||||||
{
|
{
|
||||||
const std::wstring trimmed = Trim(m_widthEditText);
|
return true;
|
||||||
if (!trimmed.empty())
|
|
||||||
{
|
|
||||||
wchar_t* stop = nullptr;
|
|
||||||
const double parsed = std::wcstod(trimmed.c_str(), &stop);
|
|
||||||
if (stop != trimmed.c_str() && parsed == parsed)
|
|
||||||
{
|
|
||||||
if (SignalStyle* style = m_tab->Style(m_tab->Selected()))
|
|
||||||
{
|
|
||||||
const float value = ClampFloat(static_cast<float>(parsed), kWidthMin, kWidthMax);
|
|
||||||
style->lineWidth = std::round(value * 10.0f) / 10.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_widthEditing = false;
|
const std::wstring trimmed = Trim(text);
|
||||||
m_widthEditFresh = false;
|
switch (field)
|
||||||
m_widthEditText.clear();
|
{
|
||||||
return changed;
|
case StyleEditField::WIDTH:
|
||||||
|
{
|
||||||
|
if (trimmed.empty())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
wchar_t* stop = nullptr;
|
||||||
|
const double parsed = std::wcstod(trimmed.c_str(), &stop);
|
||||||
|
if (stop == trimmed.c_str() || parsed != parsed)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (SignalStyle* style = m_tab->Style(m_tab->Selected()))
|
||||||
|
{
|
||||||
|
const float value = ClampFloat(static_cast<float>(parsed), kWidthMin, kWidthMax);
|
||||||
|
style->lineWidth = std::round(value * 10.0f) / 10.0f;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case StyleEditField::OPACITY:
|
||||||
|
{
|
||||||
|
if (trimmed.empty())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
wchar_t* stop = nullptr;
|
||||||
|
const double parsed = std::wcstod(trimmed.c_str(), &stop);
|
||||||
|
if (stop == trimmed.c_str() || parsed != parsed)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (SignalStyle* style = m_tab->Style(m_tab->Selected()))
|
||||||
|
{
|
||||||
|
style->opacity = ClampFloat(static_cast<float>(parsed) / 100.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case StyleEditField::SIGNAL_NAME:
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= static_cast<int>(m_rows.size()) || m_dataSetWritable == nullptr)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||||
|
m_dataSetWritable->SetSignalDisplayName(item.fileIndex, item.signalIndex, trimmed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case StyleEditField::FILE_NAME:
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= static_cast<int>(m_rows.size()) || m_dataSetWritable == nullptr)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_dataSetWritable->SetFileDisplayName(m_rows[static_cast<std::size_t>(row)].fileIndex, trimmed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::RectF SignalPanel::EditFieldRect() const
|
||||||
|
{
|
||||||
|
switch (m_editField)
|
||||||
|
{
|
||||||
|
case StyleEditField::WIDTH:
|
||||||
|
return m_widthValueRect;
|
||||||
|
case StyleEditField::OPACITY:
|
||||||
|
return m_opacityValueRect;
|
||||||
|
case StyleEditField::SIGNAL_NAME:
|
||||||
|
case StyleEditField::FILE_NAME:
|
||||||
|
return RowNameRect(m_editRow);
|
||||||
|
case StyleEditField::NONE:
|
||||||
|
default:
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SignalPanel::OnEditChar(wchar_t character)
|
bool SignalPanel::OnEditChar(wchar_t character)
|
||||||
{
|
{
|
||||||
if (!m_widthEditing)
|
if (m_editField == StyleEditField::NONE)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ctrl+C:把当前文本复制到剪贴板。
|
||||||
|
if (character == 0x03)
|
||||||
|
{
|
||||||
|
CopyTextToClipboard(m_host, m_editText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (character == L'\r' || character == L'\n')
|
if (character == L'\r' || character == L'\n')
|
||||||
{
|
{
|
||||||
return EndWidthEdit(true);
|
return EndEdit(true);
|
||||||
}
|
}
|
||||||
if (character == L'\b')
|
if (character == L'\b')
|
||||||
{
|
{
|
||||||
m_widthEditFresh = false;
|
m_editFresh = false;
|
||||||
if (!m_widthEditText.empty())
|
if (!m_editText.empty())
|
||||||
{
|
{
|
||||||
m_widthEditText.pop_back();
|
m_editText.pop_back();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (character >= L'0' && character <= L'9')
|
// 名称类字段允许任意可打印字符(含空格、方括号、中文)。
|
||||||
|
const bool nameField = (m_editField == StyleEditField::SIGNAL_NAME) ||
|
||||||
|
(m_editField == StyleEditField::FILE_NAME);
|
||||||
|
if (m_editField == StyleEditField::OPACITY && (character == L'%'))
|
||||||
{
|
{
|
||||||
// 首次键入直接替换原有数值,无需先手动删除。
|
|
||||||
if (m_widthEditFresh)
|
|
||||||
{
|
|
||||||
m_widthEditText.clear();
|
|
||||||
m_widthEditFresh = false;
|
|
||||||
}
|
|
||||||
if (m_widthEditText.size() < 5)
|
|
||||||
{
|
|
||||||
m_widthEditText.push_back(character);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (character == L'.')
|
if (character < 0x20 || character == 0x7F)
|
||||||
{
|
{
|
||||||
if (m_widthEditFresh)
|
|
||||||
{
|
|
||||||
m_widthEditText.clear();
|
|
||||||
m_widthEditFresh = false;
|
|
||||||
}
|
|
||||||
// 只允许一个小数点。
|
|
||||||
if (m_widthEditText.find(L'.') == std::wstring::npos && m_widthEditText.size() < 4)
|
|
||||||
{
|
|
||||||
m_widthEditText.push_back(character);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (nameField && m_editText.size() >= 64)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool numericField = !nameField;
|
||||||
|
if (numericField)
|
||||||
|
{
|
||||||
|
const bool digit = (character >= L'0' && character <= L'9');
|
||||||
|
const bool dot = (character == L'.');
|
||||||
|
if (!digit && !dot)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (dot && m_editText.find(L'.') != std::wstring::npos)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!dot && m_editText.size() >= 5)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首次键入直接替换原有内容,无需先手动删除。
|
||||||
|
if (m_editFresh)
|
||||||
|
{
|
||||||
|
m_editText.clear();
|
||||||
|
m_editFresh = false;
|
||||||
|
}
|
||||||
|
m_editText.push_back(character);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SignalPanel::OnEditKey(WPARAM key)
|
bool SignalPanel::OnEditKey(WPARAM key)
|
||||||
{
|
{
|
||||||
if (!m_widthEditing)
|
if (m_editField == StyleEditField::NONE)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1129,14 +1289,14 @@ namespace jdi
|
|||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
case VK_RETURN:
|
case VK_RETURN:
|
||||||
return EndWidthEdit(true);
|
return EndEdit(true);
|
||||||
case VK_ESCAPE:
|
case VK_ESCAPE:
|
||||||
return EndWidthEdit(false);
|
return EndEdit(false);
|
||||||
case VK_BACK:
|
case VK_BACK:
|
||||||
m_widthEditFresh = false;
|
m_editFresh = false;
|
||||||
if (!m_widthEditText.empty())
|
if (!m_editText.empty())
|
||||||
{
|
{
|
||||||
m_widthEditText.pop_back();
|
m_editText.pop_back();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
@@ -1201,9 +1361,9 @@ namespace jdi
|
|||||||
bool SignalPanel::CloseTransientUi()
|
bool SignalPanel::CloseTransientUi()
|
||||||
{
|
{
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
if (m_widthEditing)
|
if (m_editField != StyleEditField::NONE)
|
||||||
{
|
{
|
||||||
changed = EndWidthEdit(true) || changed;
|
changed = EndEdit(true) || changed;
|
||||||
}
|
}
|
||||||
if (m_colorPicker != nullptr && m_colorPicker->IsOpen())
|
if (m_colorPicker != nullptr && m_colorPicker->IsOpen())
|
||||||
{
|
{
|
||||||
@@ -1288,6 +1448,10 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
hoverStyle = StyleHit::WIDTH_SLIDER;
|
hoverStyle = StyleHit::WIDTH_SLIDER;
|
||||||
}
|
}
|
||||||
|
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_opacityValueRect, fx, fy))
|
||||||
|
{
|
||||||
|
hoverStyle = StyleHit::OPACITY_VALUE;
|
||||||
|
}
|
||||||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthValueRect, fx, fy))
|
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthValueRect, fx, fy))
|
||||||
{
|
{
|
||||||
hoverStyle = StyleHit::WIDTH_VALUE;
|
hoverStyle = StyleHit::WIDTH_VALUE;
|
||||||
@@ -1344,10 +1508,11 @@ namespace jdi
|
|||||||
m_pendingLaneMenu = false;
|
m_pendingLaneMenu = false;
|
||||||
m_pendingColorPicker = false;
|
m_pendingColorPicker = false;
|
||||||
|
|
||||||
// 点击样式区以外的位置时结束线宽输入。
|
// 点击当前输入框以外的位置时提交输入。
|
||||||
if (m_widthEditing && !UiPointInRect(m_widthValueRect, fx, fy))
|
if (m_editField != StyleEditField::NONE &&
|
||||||
|
!UiPointInRect(EditFieldRect(), fx, fy))
|
||||||
{
|
{
|
||||||
EndWidthEdit(true);
|
EndEdit(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全部显示/隐藏。
|
// 全部显示/隐藏。
|
||||||
@@ -1398,10 +1563,14 @@ namespace jdi
|
|||||||
ApplyWidthFromX(fx);
|
ApplyWidthFromX(fx);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// 数值框的命中优先于滑块:两者几何重叠时要能进入输入状态。
|
||||||
if (UiPointInRect(m_widthValueRect, fx, fy))
|
if (UiPointInRect(m_widthValueRect, fx, fy))
|
||||||
{
|
{
|
||||||
// 单击即进入输入状态,双击亦等效。
|
return BeginEdit(StyleEditField::WIDTH);
|
||||||
return BeginWidthEdit();
|
}
|
||||||
|
if (UiPointInRect(m_opacityValueRect, fx, fy))
|
||||||
|
{
|
||||||
|
return BeginEdit(StyleEditField::OPACITY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1522,10 +1691,14 @@ namespace jdi
|
|||||||
const float fx = static_cast<float>(x);
|
const float fx = static_cast<float>(x);
|
||||||
const float fy = static_cast<float>(y);
|
const float fy = static_cast<float>(y);
|
||||||
|
|
||||||
// 线宽数值框:双击进入(或保持)输入状态。
|
// 样式区数值框:双击进入(或保持)输入状态。
|
||||||
if (UiPointInRect(m_widthValueRect, fx, fy))
|
if (UiPointInRect(m_widthValueRect, fx, fy))
|
||||||
{
|
{
|
||||||
return BeginWidthEdit();
|
return BeginEdit(StyleEditField::WIDTH);
|
||||||
|
}
|
||||||
|
if (UiPointInRect(m_opacityValueRect, fx, fy))
|
||||||
|
{
|
||||||
|
return BeginEdit(StyleEditField::OPACITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
RowPart part = RowPart::NONE;
|
RowPart part = RowPart::NONE;
|
||||||
@@ -1536,24 +1709,35 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||||
|
|
||||||
|
// 分组行:双击折叠三角用于折叠,双击名称区域进入重命名。
|
||||||
if (item.kind == RowKind::FILE)
|
if (item.kind == RowKind::FILE)
|
||||||
{
|
{
|
||||||
m_tab->ToggleCollapsed(item.fileIndex);
|
if (part == RowPart::DISCLOSURE)
|
||||||
|
{
|
||||||
|
m_tab->ToggleCollapsed(item.fileIndex);
|
||||||
|
m_rowsDirty = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (part != RowPart::BODY)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 重命名分组会改变列表行高度以外的展示内容,重建行缓存以保证文字刷新。
|
||||||
m_rowsDirty = true;
|
m_rowsDirty = true;
|
||||||
return true;
|
return BeginEdit(StyleEditField::FILE_NAME, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网格徽标单击已可打开菜单,双击不再切换可见性。
|
// 复选框与网格徽标各自由单击处理,双击不再切换可见性。
|
||||||
if (part == RowPart::LANE_BADGE)
|
if (part == RowPart::CHECK || part == RowPart::LANE_BADGE)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignalRef ref{item.fileIndex, item.signalIndex};
|
const SignalRef ref{item.fileIndex, item.signalIndex};
|
||||||
m_tab->SetSelected(ref);
|
m_tab->SetSelected(ref);
|
||||||
m_tab->ToggleVisible(*m_dataSet, ref);
|
|
||||||
m_rowsDirty = true;
|
m_rowsDirty = true;
|
||||||
return true;
|
return BeginEdit(StyleEditField::SIGNAL_NAME, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SignalPanel::OnRButtonDown(int x, int y)
|
bool SignalPanel::OnRButtonDown(int x, int y)
|
||||||
@@ -1630,7 +1814,7 @@ namespace jdi
|
|||||||
|
|
||||||
MenuItem header;
|
MenuItem header;
|
||||||
header.kind = MenuItemKind::HEADER;
|
header.kind = MenuItemKind::HEADER;
|
||||||
header.label = signal->name;
|
header.label = signal->Title();
|
||||||
items.push_back(header);
|
items.push_back(header);
|
||||||
|
|
||||||
MenuItem toggle;
|
MenuItem toggle;
|
||||||
@@ -1712,7 +1896,7 @@ namespace jdi
|
|||||||
|
|
||||||
MenuItem header;
|
MenuItem header;
|
||||||
header.kind = MenuItemKind::HEADER;
|
header.kind = MenuItemKind::HEADER;
|
||||||
header.label = file->fileName;
|
header.label = file->Title();
|
||||||
items.push_back(header);
|
items.push_back(header);
|
||||||
|
|
||||||
MenuItem showAll;
|
MenuItem showAll;
|
||||||
|
|||||||
@@ -99,12 +99,25 @@ namespace jdi
|
|||||||
bool OnLButtonUp();
|
bool OnLButtonUp();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 处理键盘按键(Esc 收起)。
|
* @brief 处理键盘按键(Esc 收起、回车提交输入)。
|
||||||
* @param [IN] key 虚拟键码。
|
* @param [IN] key 虚拟键码。
|
||||||
* @return 事件已被消费返回 true。
|
* @return 事件已被消费返回 true。
|
||||||
*/
|
*/
|
||||||
bool OnKeyDown(WPARAM key);
|
bool OnKeyDown(WPARAM key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理字符输入。
|
||||||
|
* @param [IN] character 输入字符('\r' 提交、'\b' 退格、0x03 复制)。
|
||||||
|
* @return 界面状态发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool OnChar(wchar_t character);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断面板是否处于内联文本输入状态。
|
||||||
|
* @return 正在输入返回 true。
|
||||||
|
*/
|
||||||
|
bool IsEditing() const { return m_editField != EditField::NONE; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 判断坐标是否落在拾色面板内部。
|
* @brief 判断坐标是否落在拾色面板内部。
|
||||||
* @param [IN] x 客户区横坐标。
|
* @param [IN] x 客户区横坐标。
|
||||||
@@ -122,6 +135,18 @@ namespace jdi
|
|||||||
HCURSOR QueryCursor(int x, int y) const;
|
HCURSOR QueryCursor(int x, int y) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/**
|
||||||
|
* @brief 面板内可进行内联文本输入的字段。
|
||||||
|
*/
|
||||||
|
enum class EditField : int
|
||||||
|
{
|
||||||
|
NONE = 0, /**< 未处于编辑状态。 */
|
||||||
|
R = 1, /**< 红色分量(0-255)。 */
|
||||||
|
G = 2, /**< 绿色分量(0-255)。 */
|
||||||
|
B = 3, /**< 蓝色分量(0-255)。 */
|
||||||
|
HEX = 4 /**< 十六进制色号。 */
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 依据锚点矩形与客户区计算面板矩形与内部控件矩形。
|
* @brief 依据锚点矩形与客户区计算面板矩形与内部控件矩形。
|
||||||
* @param [IN] bounds 宿主客户区矩形。
|
* @param [IN] bounds 宿主客户区矩形。
|
||||||
@@ -129,6 +154,26 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void BuildLayout(const gd::RectF& bounds, const gd::RectF& anchor);
|
void BuildLayout(const gd::RectF& bounds, const gd::RectF& anchor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 进入指定字段的内联输入状态。
|
||||||
|
* @param [IN] field 目标字段。
|
||||||
|
* @return 状态发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool BeginEdit(EditField field);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结束内联输入状态。
|
||||||
|
* @param [IN] commit 是否提交输入值。
|
||||||
|
* @return 状态发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool EndEdit(bool commit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算当前输入框的矩形。
|
||||||
|
* @return 输入框矩形。
|
||||||
|
*/
|
||||||
|
gd::RectF EditFieldRect() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 查询坐标命中的滑块下标。
|
* @brief 查询坐标命中的滑块下标。
|
||||||
* @param [IN] x 客户区横坐标。
|
* @param [IN] x 客户区横坐标。
|
||||||
@@ -160,6 +205,10 @@ namespace jdi
|
|||||||
std::array<gd::RectF, 3> m_values; /**< 数值文本矩形。 */
|
std::array<gd::RectF, 3> m_values; /**< 数值文本矩形。 */
|
||||||
gd::RectF m_previewRect; /**< 预览色块矩形。 */
|
gd::RectF m_previewRect; /**< 预览色块矩形。 */
|
||||||
gd::RectF m_hexRect; /**< 十六进制文本矩形。 */
|
gd::RectF m_hexRect; /**< 十六进制文本矩形。 */
|
||||||
|
|
||||||
|
EditField m_editField = EditField::NONE; /**< 当前处于输入状态的字段。 */
|
||||||
|
bool m_editFresh = false; /**< 输入缓冲是否仍为初始值(首次键入应整体替换)。 */
|
||||||
|
std::wstring m_editText; /**< 输入缓冲。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
|
|||||||
@@ -85,13 +85,20 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
struct SignalData
|
struct SignalData
|
||||||
{
|
{
|
||||||
std::wstring name; /**< 显示名称(已剥离 "[n]" 序号前缀)。 */
|
std::wstring name; /**< 名称(取自文件列名,完全保留原样)。 */
|
||||||
|
std::wstring displayName; /**< 用户重命名的显示名;为空时使用 name。 */
|
||||||
std::wstring rawHeader; /**< 原始列名。 */
|
std::wstring rawHeader; /**< 原始列名。 */
|
||||||
std::wstring unit; /**< 从列名括号中提取的单位,可能为空。 */
|
std::wstring unit; /**< 从列名括号中提取的单位,可能为空。 */
|
||||||
std::vector<double> values; /**< 采样值,缺失处为 NaN。 */
|
std::vector<double> values; /**< 采样值,缺失处为 NaN。 */
|
||||||
double minValue = 0.0; /**< 有限采样值最小值。 */
|
double minValue = 0.0; /**< 有限采样值最小值。 */
|
||||||
double maxValue = 0.0; /**< 有限采样值最大值。 */
|
double maxValue = 0.0; /**< 有限采样值最大值。 */
|
||||||
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
|
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取用于界面展示的名称。
|
||||||
|
* @return 用户重命名后的显示名,未重命名时返回文件中的原始名称。
|
||||||
|
*/
|
||||||
|
const std::wstring& Title() const { return displayName.empty() ? name : displayName; }
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,14 +106,21 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
struct DataFile
|
struct DataFile
|
||||||
{
|
{
|
||||||
std::wstring filePath; /**< 文件完整路径。 */
|
std::wstring filePath; /**< 文件完整路径(仅用于状态提示)。 */
|
||||||
std::wstring fileName; /**< 文件名称。 */
|
std::wstring fileName; /**< 文件名称。 */
|
||||||
|
std::wstring displayName; /**< 用户重命名的分组名;为空时使用 fileName。 */
|
||||||
std::wstring timeColumnName; /**< 时间列列名。 */
|
std::wstring timeColumnName; /**< 时间列列名。 */
|
||||||
std::vector<double> time; /**< 时间轴数值。 */
|
std::vector<double> time; /**< 时间轴数值。 */
|
||||||
std::vector<SignalData> signals; /**< 该文件包含的信号。 */
|
std::vector<SignalData> signals; /**< 该文件包含的信号。 */
|
||||||
double minTime = 0.0; /**< 时间轴最小值。 */
|
double minTime = 0.0; /**< 时间轴最小值。 */
|
||||||
double maxTime = 0.0; /**< 时间轴最大值。 */
|
double maxTime = 0.0; /**< 时间轴最大值。 */
|
||||||
bool timeMonotonic = true; /**< 时间轴是否单调不减。 */
|
bool timeMonotonic = true; /**< 时间轴是否单调不减。 */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取用于界面展示的分组名。
|
||||||
|
* @return 用户重命名后的显示名,未重命名时返回文件名。
|
||||||
|
*/
|
||||||
|
const std::wstring& Title() const { return displayName.empty() ? fileName : displayName; }
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,6 +182,23 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
const DataFile* FileAt(int fileIndex) const;
|
const DataFile* FileAt(int fileIndex) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 修改某个信号在界面上的显示名称。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
* @param [IN] signalIndex 文件内信号下标。
|
||||||
|
* @param [IN] name 新的显示名;传入空串表示恢复使用原始列名。
|
||||||
|
* @return 修改成功返回 true。
|
||||||
|
*/
|
||||||
|
bool SetSignalDisplayName(int fileIndex, int signalIndex, const std::wstring& name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 修改某个文件分组的显示名称。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
* @param [IN] name 新的分组名;传入空串表示恢复使用文件名。
|
||||||
|
* @return 修改成功返回 true。
|
||||||
|
*/
|
||||||
|
bool SetFileDisplayName(int fileIndex, const std::wstring& name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 解析信号引用。
|
* @brief 解析信号引用。
|
||||||
* @param [IN] ref 信号引用。
|
* @param [IN] ref 信号引用。
|
||||||
|
|||||||
@@ -209,6 +209,34 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
double UiSliderRatioFromX(const gd::RectF& trackRect, float x);
|
double UiSliderRatioFromX(const gd::RectF& trackRect, float x);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制水平滑块(轨道 + 已填充部分 + 圆钮)。
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
* @param [IN] track 轨道矩形。
|
||||||
|
* @param [IN] ratio 当前值比例(0-1)。
|
||||||
|
* @param [IN] fillColor 已填充部分颜色。
|
||||||
|
* @param [IN] active 是否处于悬停或拖拽状态(影响圆钮半径)。
|
||||||
|
* @param [IN] scale DPI 缩放比例。
|
||||||
|
*/
|
||||||
|
void UiDrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
||||||
|
bool active, float scale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制内联数值输入框。
|
||||||
|
*
|
||||||
|
* 处于“首次输入整体替换”状态时以高亮底衬表示整段内容将被覆盖,
|
||||||
|
* 交互观感与常见登录界面的账号/密码输入框一致;已在键入时显示插入符。
|
||||||
|
*
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
* @param [IN] rect 输入框矩形。
|
||||||
|
* @param [IN] text 当前文本(可为空)。
|
||||||
|
* @param [IN] fresh 是否仍处于“首次输入整体替换”状态。
|
||||||
|
* @param [IN] font 文本字体。
|
||||||
|
* @param [IN] scale DPI 缩放比例。
|
||||||
|
*/
|
||||||
|
void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const std::wstring& text, bool fresh,
|
||||||
|
HFONT font, float scale);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制一个带边框的复选框。
|
* @brief 绘制一个带边框的复选框。
|
||||||
* @param [IN,OUT] g 绘图上下文。
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#ifndef __UTF8_UTIL_HPP__
|
#ifndef __UTF8_UTIL_HPP__
|
||||||
#define __UTF8_UTIL_HPP__
|
#define __UTF8_UTIL_HPP__
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -116,6 +118,14 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void BuildTicks(double minValue, double maxValue, double step, std::vector<double>& ticks);
|
void BuildTicks(double minValue, double maxValue, double step, std::vector<double>& ticks);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将文本复制到系统剪贴板。
|
||||||
|
* @param [IN] owner 剪贴板所有者窗口句柄,允许为空。
|
||||||
|
* @param [IN] text 待复制文本。
|
||||||
|
* @return 复制成功返回 true。
|
||||||
|
*/
|
||||||
|
bool CopyTextToClipboard(HWND owner, const std::wstring& text);
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
|
|
||||||
#endif // __UTF8_UTIL_HPP__
|
#endif // __UTF8_UTIL_HPP__
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include "Utf8Util.hpp"
|
||||||
|
|
||||||
namespace jdi
|
namespace jdi
|
||||||
{
|
{
|
||||||
@@ -30,7 +33,7 @@ namespace jdi
|
|||||||
const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u};
|
const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制水平滑块(轨道 + 已填充部分 + 圆钮)。
|
* @brief 绘制水平滑块(与样式区共用同一实现,保证行为一致)。
|
||||||
* @param [IN,OUT] g 绘图上下文。
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
* @param [IN] track 轨道矩形。
|
* @param [IN] track 轨道矩形。
|
||||||
* @param [IN] ratio 当前值比例(0-1)。
|
* @param [IN] ratio 当前值比例(0-1)。
|
||||||
@@ -41,22 +44,7 @@ namespace jdi
|
|||||||
void DrawChannelSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
void DrawChannelSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
||||||
bool hovered, float scale)
|
bool hovered, float scale)
|
||||||
{
|
{
|
||||||
const float radius = track.Height * 0.5f;
|
UiDrawSlider(g, track, ratio, fillColor, hovered, scale);
|
||||||
UiFillRoundRect(g, track, radius, 0x2A657B83u);
|
|
||||||
|
|
||||||
ratio = std::max(0.0, std::min(1.0, ratio));
|
|
||||||
const float fillWidth = std::max(track.Height, static_cast<float>(ratio) * track.Width);
|
|
||||||
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
|
|
||||||
|
|
||||||
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
|
|
||||||
const float knobRadius = radius + (hovered ? 2.5f * scale : 0.8f * scale);
|
|
||||||
|
|
||||||
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
|
|
||||||
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
|
|
||||||
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
|
||||||
knobRadius * 2.0f);
|
|
||||||
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
|
||||||
knobRadius * 2.0f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,11 +167,238 @@ namespace jdi
|
|||||||
|
|
||||||
void ColorPickerPopup::Close()
|
void ColorPickerPopup::Close()
|
||||||
{
|
{
|
||||||
|
// 收起前先提交正在编辑的取值,避免用户输入被静默丢弃。
|
||||||
|
if (m_editField != EditField::NONE)
|
||||||
|
{
|
||||||
|
EndEdit(true);
|
||||||
|
}
|
||||||
m_open = false;
|
m_open = false;
|
||||||
m_draggingSlider = -1;
|
m_draggingSlider = -1;
|
||||||
m_hoverSlider = -1;
|
m_hoverSlider = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gd::RectF ColorPickerPopup::EditFieldRect() const
|
||||||
|
{
|
||||||
|
switch (m_editField)
|
||||||
|
{
|
||||||
|
case EditField::R:
|
||||||
|
return m_values[0];
|
||||||
|
case EditField::G:
|
||||||
|
return m_values[1];
|
||||||
|
case EditField::B:
|
||||||
|
return m_values[2];
|
||||||
|
case EditField::HEX:
|
||||||
|
return m_hexRect;
|
||||||
|
case EditField::NONE:
|
||||||
|
default:
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ColorPickerPopup::BeginEdit(EditField field)
|
||||||
|
{
|
||||||
|
if (!m_open || field == EditField::NONE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_editField == field)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (field)
|
||||||
|
{
|
||||||
|
case EditField::R:
|
||||||
|
case EditField::G:
|
||||||
|
case EditField::B:
|
||||||
|
{
|
||||||
|
const int index = static_cast<int>(field) - 1;
|
||||||
|
m_editText = std::to_wstring(ToByte(m_channels[static_cast<std::size_t>(index)]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EditField::HEX:
|
||||||
|
{
|
||||||
|
const std::uint32_t color = CurrentColor();
|
||||||
|
wchar_t buffer[16];
|
||||||
|
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%02X%02X%02X", (color >> 16) & 0xFFu,
|
||||||
|
(color >> 8) & 0xFFu, color & 0xFFu);
|
||||||
|
m_editText = std::wstring(buffer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_draggingSlider = -1;
|
||||||
|
m_editField = field;
|
||||||
|
m_editFresh = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ColorPickerPopup::EndEdit(bool commit)
|
||||||
|
{
|
||||||
|
if (m_editField == EditField::NONE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditField field = m_editField;
|
||||||
|
const std::wstring text = m_editText;
|
||||||
|
m_editField = EditField::NONE;
|
||||||
|
m_editFresh = false;
|
||||||
|
m_editText.clear();
|
||||||
|
|
||||||
|
if (!commit)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::wstring trimmed = Trim(text);
|
||||||
|
if (field == EditField::HEX)
|
||||||
|
{
|
||||||
|
// 接受带或不带 '#' 的 6 位十六进制色号。
|
||||||
|
std::wstring digits = trimmed;
|
||||||
|
if (!digits.empty() && digits.front() == L'#')
|
||||||
|
{
|
||||||
|
digits.erase(digits.begin());
|
||||||
|
}
|
||||||
|
if (digits.size() == 6)
|
||||||
|
{
|
||||||
|
unsigned int value = 0;
|
||||||
|
bool valid = true;
|
||||||
|
for (const wchar_t c : digits)
|
||||||
|
{
|
||||||
|
unsigned int digit = 0;
|
||||||
|
if (c >= L'0' && c <= L'9')
|
||||||
|
{
|
||||||
|
digit = static_cast<unsigned int>(c - L'0');
|
||||||
|
}
|
||||||
|
else if (c >= L'a' && c <= L'f')
|
||||||
|
{
|
||||||
|
digit = static_cast<unsigned int>(c - L'a') + 10u;
|
||||||
|
}
|
||||||
|
else if (c >= L'A' && c <= L'F')
|
||||||
|
{
|
||||||
|
digit = static_cast<unsigned int>(c - L'A') + 10u;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
value = (value << 4) | digit;
|
||||||
|
}
|
||||||
|
if (valid)
|
||||||
|
{
|
||||||
|
m_channels[0] = static_cast<double>((value >> 16) & 0xFFu) / 255.0;
|
||||||
|
m_channels[1] = static_cast<double>((value >> 8) & 0xFFu) / 255.0;
|
||||||
|
m_channels[2] = static_cast<double>(value & 0xFFu) / 255.0;
|
||||||
|
m_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.empty())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
wchar_t* stop = nullptr;
|
||||||
|
const double parsed = std::wcstod(trimmed.c_str(), &stop);
|
||||||
|
if (stop == trimmed.c_str() || parsed != parsed)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double value = parsed;
|
||||||
|
if (value < 0.0)
|
||||||
|
{
|
||||||
|
value = 0.0;
|
||||||
|
}
|
||||||
|
if (value > 255.0)
|
||||||
|
{
|
||||||
|
value = 255.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int index = static_cast<int>(field) - 1;
|
||||||
|
if (index >= 0 && index < 3)
|
||||||
|
{
|
||||||
|
m_channels[static_cast<std::size_t>(index)] = value / 255.0;
|
||||||
|
m_changed = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ColorPickerPopup::OnChar(wchar_t character)
|
||||||
|
{
|
||||||
|
if (m_editField == EditField::NONE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+C:把当前文本复制到剪贴板。
|
||||||
|
if (character == 0x03)
|
||||||
|
{
|
||||||
|
CopyTextToClipboard(nullptr, m_editText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character == L'\r' || character == L'\n')
|
||||||
|
{
|
||||||
|
return EndEdit(true);
|
||||||
|
}
|
||||||
|
if (character == L'\b')
|
||||||
|
{
|
||||||
|
m_editFresh = false;
|
||||||
|
if (!m_editText.empty())
|
||||||
|
{
|
||||||
|
m_editText.pop_back();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (character < 0x20 || character == 0x7F)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_editField == EditField::HEX)
|
||||||
|
{
|
||||||
|
const bool hexDigit = (character >= L'0' && character <= L'9') ||
|
||||||
|
(character >= L'a' && character <= L'f') ||
|
||||||
|
(character >= L'A' && character <= L'F') || (character == L'#');
|
||||||
|
if (!hexDigit)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (m_editFresh)
|
||||||
|
{
|
||||||
|
m_editText.clear();
|
||||||
|
m_editFresh = false;
|
||||||
|
}
|
||||||
|
if (m_editText.size() < 7)
|
||||||
|
{
|
||||||
|
m_editText.push_back(character);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool digit = (character >= L'0' && character <= L'9');
|
||||||
|
if (!digit)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (m_editFresh)
|
||||||
|
{
|
||||||
|
m_editText.clear();
|
||||||
|
m_editFresh = false;
|
||||||
|
}
|
||||||
|
if (m_editText.size() < 3)
|
||||||
|
{
|
||||||
|
m_editText.push_back(character);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ColorPickerPopup::ContainsPoint(int x, int y) const
|
bool ColorPickerPopup::ContainsPoint(int x, int y) const
|
||||||
{
|
{
|
||||||
if (!m_open)
|
if (!m_open)
|
||||||
@@ -283,6 +498,29 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点击当前输入框以外的位置时提交输入。
|
||||||
|
if (m_editField != EditField::NONE && !UiPointInRect(EditFieldRect(), static_cast<float>(x),
|
||||||
|
static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
EndEdit(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数值与色号文本优先于滑块:两者几何重叠时要能进入输入状态。
|
||||||
|
for (int index = 0; index < 3; ++index)
|
||||||
|
{
|
||||||
|
if (UiPointInRect(m_values[static_cast<std::size_t>(index)], static_cast<float>(x),
|
||||||
|
static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
BeginEdit(static_cast<EditField>(index + 1));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (UiPointInRect(m_hexRect, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
BeginEdit(EditField::HEX);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
m_draggingSlider = SliderAtPoint(x, y);
|
m_draggingSlider = SliderAtPoint(x, y);
|
||||||
if (m_draggingSlider >= 0)
|
if (m_draggingSlider >= 0)
|
||||||
{
|
{
|
||||||
@@ -308,6 +546,28 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (m_editField != EditField::NONE)
|
||||||
|
{
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case VK_RETURN:
|
||||||
|
EndEdit(true);
|
||||||
|
return true;
|
||||||
|
case VK_ESCAPE:
|
||||||
|
EndEdit(false);
|
||||||
|
return true;
|
||||||
|
case VK_BACK:
|
||||||
|
m_editFresh = false;
|
||||||
|
if (!m_editText.empty())
|
||||||
|
{
|
||||||
|
m_editText.pop_back();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (key == VK_ESCAPE)
|
if (key == VK_ESCAPE)
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
@@ -347,8 +607,15 @@ namespace jdi
|
|||||||
const bool active = (m_draggingSlider == index) || (m_hoverSlider == index);
|
const bool active = (m_draggingSlider == index) || (m_hoverSlider == index);
|
||||||
DrawChannelSlider(g, track, m_channels[slot], kChannelColors[index], active, m_scale);
|
DrawChannelSlider(g, track, m_channels[slot], kChannelColors[index], active, m_scale);
|
||||||
|
|
||||||
UiText(g, std::to_wstring(ToByte(m_channels[slot])), m_fonts->monoSmall, m_values[slot],
|
if (m_editField == static_cast<EditField>(index + 1))
|
||||||
m_theme->text, TextAlign::RIGHT);
|
{
|
||||||
|
UiDrawEditField(g, m_values[slot], m_editText, m_editFresh, m_fonts->monoSmall, m_scale);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UiText(g, std::to_wstring(ToByte(m_channels[slot])), m_fonts->monoSmall, m_values[slot],
|
||||||
|
m_theme->text, TextAlign::RIGHT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预览与十六进制值。
|
// 预览与十六进制值。
|
||||||
@@ -356,10 +623,17 @@ namespace jdi
|
|||||||
UiFillRoundRect(g, m_previewRect, 3.0f * m_scale, color);
|
UiFillRoundRect(g, m_previewRect, 3.0f * m_scale, color);
|
||||||
UiDrawRoundRect(g, m_previewRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
|
UiDrawRoundRect(g, m_previewRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
|
||||||
|
|
||||||
wchar_t buffer[16];
|
if (m_editField == EditField::HEX)
|
||||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu,
|
{
|
||||||
(color >> 8) & 0xFFu, color & 0xFFu);
|
UiDrawEditField(g, m_hexRect, m_editText, m_editFresh, m_fonts->monoSmall, m_scale);
|
||||||
UiText(g, std::wstring(buffer), m_fonts->monoSmall, m_hexRect, m_theme->text, TextAlign::LEFT);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
wchar_t buffer[16];
|
||||||
|
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu,
|
||||||
|
(color >> 8) & 0xFFu, color & 0xFFu);
|
||||||
|
UiText(g, std::wstring(buffer), m_fonts->monoSmall, m_hexRect, m_theme->text, TextAlign::LEFT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
@@ -446,6 +446,47 @@ namespace jdi
|
|||||||
return &m_files[static_cast<std::size_t>(fileIndex)];
|
return &m_files[static_cast<std::size_t>(fileIndex)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DataSet::SetSignalDisplayName(int fileIndex, int signalIndex, const std::wstring& name)
|
||||||
|
{
|
||||||
|
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
|
||||||
|
if (signalIndex < 0 || signalIndex >= static_cast<int>(file.signals.size()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一去除首尾空白;结果为空则视为恢复原始列名。
|
||||||
|
std::wstring trimmed = Trim(name);
|
||||||
|
SignalData& signal = file.signals[static_cast<std::size_t>(signalIndex)];
|
||||||
|
if (trimmed == signal.displayName)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
signal.displayName = std::move(trimmed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DataSet::SetFileDisplayName(int fileIndex, const std::wstring& name)
|
||||||
|
{
|
||||||
|
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring trimmed = Trim(name);
|
||||||
|
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
|
||||||
|
if (trimmed == file.displayName)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
file.displayName = std::move(trimmed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const SignalData* DataSet::Resolve(const SignalRef& ref) const
|
const SignalData* DataSet::Resolve(const SignalRef& ref) const
|
||||||
{
|
{
|
||||||
const DataFile* file = FileAt(ref.fileIndex);
|
const DataFile* file = FileAt(ref.fileIndex);
|
||||||
|
|||||||
@@ -419,6 +419,83 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UiDrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
||||||
|
bool active, float scale)
|
||||||
|
{
|
||||||
|
if (track.Width <= 0.0f || track.Height <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ratio < 0.0)
|
||||||
|
{
|
||||||
|
ratio = 0.0;
|
||||||
|
}
|
||||||
|
if (ratio > 1.0)
|
||||||
|
{
|
||||||
|
ratio = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float radius = track.Height * 0.5f;
|
||||||
|
UiFillRoundRect(g, track, radius, 0x2A657B83u);
|
||||||
|
|
||||||
|
// 已填充部分严格止于圆钮中心,最小值时不会留下超出圆钮的填充残留。
|
||||||
|
const float fillWidth = static_cast<float>(ratio) * track.Width;
|
||||||
|
if (fillWidth > 0.5f)
|
||||||
|
{
|
||||||
|
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float knobRadius = radius + (active ? 2.5f * scale : 0.8f * scale);
|
||||||
|
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
|
||||||
|
|
||||||
|
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
|
||||||
|
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
|
||||||
|
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
||||||
|
knobRadius * 2.0f);
|
||||||
|
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
||||||
|
knobRadius * 2.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const std::wstring& text, bool fresh,
|
||||||
|
HFONT font, float scale)
|
||||||
|
{
|
||||||
|
if (rect.Width <= 0.0f || rect.Height <= 0.0f || font == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Theme& theme = SolarizedLight();
|
||||||
|
const float corner = 3.0f * scale;
|
||||||
|
|
||||||
|
UiFillRoundRect(g, rect, corner, theme.cardBg);
|
||||||
|
UiDrawRoundRect(g, rect, corner, theme.accent, 1.6f);
|
||||||
|
|
||||||
|
const std::wstring shown = text.empty() ? std::wstring(L" ") : text;
|
||||||
|
|
||||||
|
if (fresh)
|
||||||
|
{
|
||||||
|
// 整段内容处于“将被覆盖”状态:以强调色底衬表示已全选。
|
||||||
|
const float textWidth = UiTextWidth(g, shown, font) + 8.0f * scale;
|
||||||
|
const gd::RectF highlight(rect.X + (rect.Width - textWidth) * 0.5f, rect.Y + 2.0f * scale,
|
||||||
|
textWidth, rect.Height - 4.0f * scale);
|
||||||
|
UiFillRoundRect(g, highlight, 2.0f * scale, theme.accent);
|
||||||
|
UiText(g, shown, font, highlight, theme.textOnAccent, TextAlign::CENTER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UiText(g, shown, font, rect, theme.textStrong, TextAlign::CENTER);
|
||||||
|
|
||||||
|
// 已开始键入:在文本末尾显示插入符。
|
||||||
|
const float textWidth = UiTextWidth(g, shown, font);
|
||||||
|
const float caretX = rect.X + (rect.Width + textWidth) * 0.5f + 1.0f * scale;
|
||||||
|
if (caretX < rect.X + rect.Width - 2.0f * scale)
|
||||||
|
{
|
||||||
|
UiDrawLine(g, caretX, rect.Y + 4.0f * scale, caretX, rect.Y + rect.Height - 4.0f * scale,
|
||||||
|
theme.accent, 1.4f * scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool UiSwatchNeedsOutline(float opacity)
|
bool UiSwatchNeedsOutline(float opacity)
|
||||||
{
|
{
|
||||||
return !(opacity >= kSwatchOutlineOpacity);
|
return !(opacity >= kSwatchOutlineOpacity);
|
||||||
|
|||||||
@@ -367,4 +367,48 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CopyTextToClipboard(HWND owner, const std::wstring& text)
|
||||||
|
{
|
||||||
|
if (text.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OpenClipboard(owner) == FALSE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
if (EmptyClipboard() != FALSE)
|
||||||
|
{
|
||||||
|
const SIZE_T bytes = (text.size() + 1) * sizeof(wchar_t);
|
||||||
|
HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, bytes);
|
||||||
|
if (handle != nullptr)
|
||||||
|
{
|
||||||
|
void* target = GlobalLock(handle);
|
||||||
|
if (target != nullptr)
|
||||||
|
{
|
||||||
|
std::memcpy(target, text.c_str(), bytes);
|
||||||
|
GlobalUnlock(handle);
|
||||||
|
if (SetClipboardData(CF_UNICODETEXT, handle) != nullptr)
|
||||||
|
{
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GlobalFree(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GlobalFree(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseClipboard();
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
Reference in new issue
Block a user