fix: 修正滑块填充残留、数值内联输入与信号/分组重命名,拖拽虚影跟随鼠标

- 滑块最小值时不再有超出圆钮的填充残留,三处滑块统一为共享实现
- 透明度/RGB/色号支持点击或双击直接输入,整段高亮、首次键入替换、回车提交、Ctrl+C 复制
- 双击信号行或分组名进入重命名编辑;分组竖线改为绘制而非字符
- 拖拽信号时曲线虚影跟随鼠标纵向位移
- 数据层新增展示名覆盖,面板改为持有可写数据集合
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-30 09:38:22 +08:00
1 parent 542aa51e40
commit 261b8ac07d
13 files changed
+1074 -274

No files matched your search

+9 -4
View File
@@ -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)))
{ {
+1 -2
View File
@@ -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; /**< 输入框是否处于“首次输入整体替换”状态。 */
+65 -44
View File
@@ -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;
+60 -23
View File
@@ -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; /**< 待弹出菜单的目标信号。 */
+359 -175
View File
@@ -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;
+50 -1
View File
@@ -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
+33 -2
View File
@@ -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 信号引用。
+28
View File
@@ -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 绘图上下文。
+10
View File
@@ -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__
+297 -23
View File
@@ -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
+41
View File
@@ -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);
+77
View File
@@ -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);
+44
View File
@@ -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