diff --git a/Components/Application/Inc/Application.hpp b/Components/Application/Inc/Application.hpp index 7c2b8cb..fe523b2 100644 --- a/Components/Application/Inc/Application.hpp +++ b/Components/Application/Inc/Application.hpp @@ -8,6 +8,7 @@ #include #include +#include "ColorPickerPopup.hpp" #include "DataModel.hpp" #include "PaintUtil.hpp" #include "PlotView.hpp" @@ -146,6 +147,12 @@ namespace jdi */ void NewTab(); + /** + * @brief 生成当前未被占用的最小标签页名称(关闭标签页后编号可被复用)。 + * @return 形如「视图 N」的名称。 + */ + std::wstring NextTabName() const; + /** * @brief 关闭指定标签页。 * @param [IN] index 标签页下标。 @@ -257,10 +264,12 @@ namespace jdi bool m_gdiplusReady = false; /**< GDI+ 是否初始化成功。 */ Theme m_theme; /**< 主题配色。 */ - UiFonts m_fonts; /**< 界面字体。 */ PopupMenu m_menu; /**< 与主题一致的自绘弹出菜单。 */ DataSet m_dataSet; /**< 全局共享的数据集合。 */ + UiFonts m_fonts; /**< 界面字体。 */ + PopupMenu m_menu; /**< 与主题一致的自绘弹出菜单。 */ + ColorPickerPopup m_colorPicker; /**< 与主题一致的浮动拾色器。 */ + DataSet m_dataSet; /**< 全局共享的数据集合。 */ std::vector m_tabs; /**< 标签页配置集合。 */ int m_activeTab = -1; /**< 当前激活标签页下标。 */ - int m_tabNameCounter = 0; /**< 标签页命名计数器。 */ TabBar m_tabBar; /**< 顶部标签栏。 */ Toolbar m_toolbar; /**< 顶部工具栏。 */ @@ -274,7 +283,7 @@ namespace jdi gd::RectF m_plotRect; /**< 绘图区区域。 */ gd::RectF m_statusRect; /**< 状态栏区域。 */ - float m_panelWidth = 300.0f; /**< 面板宽度(逻辑像素)。 */ + float m_panelWidth = 366.0f; /**< 面板宽度(逻辑像素)。 */ bool m_draggingSplitter = false; /**< 是否正在拖动分隔条。 */ int m_splitterGrabOffset = 0; /**< 分隔条拖动的抓取偏移。 */ diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index 1fc1fea..1e89588 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -28,7 +28,7 @@ namespace jdi const float kStatusBarHeight = 26.0f; /**< 状态栏高度(逻辑像素)。 */ const float kSplitterWidth = 4.0f; /**< 分隔条宽度(逻辑像素)。 */ const float kMinPanelWidth = 220.0f; /**< 面板最小宽度(逻辑像素)。 */ - const float kMaxPanelWidth = 480.0f; /**< 面板最大宽度(逻辑像素)。 */ + const float kMaxPanelWidth = 560.0f; /**< 面板最大宽度(逻辑像素)。 */ const int kMinClientWidth = 860; /**< 窗口最小客户区宽度。 */ const int kMinClientHeight = 540; /**< 窗口最小客户区高度。 */ const UINT kMessageRemoveFile = WM_APP + 1; /**< 面板请求移除文件的自定义消息。 */ @@ -197,7 +197,9 @@ namespace jdi m_toolbar.SetScale(m_scale); m_menu.Setup(&m_fonts, &m_theme); m_menu.SetScale(m_scale); - m_panel.Setup(m_hwnd, &m_fonts, &m_theme, &m_menu); + m_colorPicker.Setup(&m_fonts, &m_theme); + m_colorPicker.SetScale(m_scale); + m_panel.Setup(m_hwnd, &m_fonts, &m_theme, &m_menu, &m_colorPicker); m_panel.SetScale(m_scale); m_plot.Setup(&m_fonts, &m_theme); m_plot.SetScale(m_scale); @@ -242,6 +244,7 @@ namespace jdi m_tabBar.SetScale(m_scale); m_toolbar.SetScale(m_scale); m_menu.SetScale(m_scale); + m_colorPicker.SetScale(m_scale); m_panel.SetScale(m_scale); m_plot.SetScale(m_scale); ReleaseBackBuffer(); @@ -270,16 +273,37 @@ namespace jdi void Application::NewTab() { - ++m_tabNameCounter; - TabConfig tab; - tab.SetName(L"视图 " + std::to_wstring(m_tabNameCounter)); + tab.SetName(NextTabName()); tab.SyncToDataSet(m_dataSet); m_tabs.push_back(std::move(tab)); SetActiveTab(static_cast(m_tabs.size()) - 1); } + std::wstring Application::NextTabName() const + { + // 取当前未被占用的最小编号,使关闭标签页后编号可以被复用(3 开 2 关再新建回到「视图 2」)。 + for (int number = 1; number <= static_cast(m_tabs.size()) + 1; ++number) + { + const std::wstring candidate = L"视图 " + std::to_wstring(number); + bool used = false; + for (const TabConfig& tab : m_tabs) + { + if (tab.Name() == candidate) + { + used = true; + break; + } + } + if (!used) + { + return candidate; + } + } + return L"视图 " + std::to_wstring(m_tabs.size() + 1); + } + void Application::CloseTab(int index) { if (index < 0 || index >= static_cast(m_tabs.size())) @@ -570,8 +594,9 @@ namespace jdi PaintStatusBar(g); - // 弹出菜单始终绘制在最上层,保证与主界面风格一致。 + // 弹出菜单与浮动拾色器始终绘制在最上层,保证与主界面风格一致。 m_menu.Paint(g); + m_colorPicker.Paint(g); } HDC windowDc = GetDC(m_hwnd); @@ -1140,6 +1165,72 @@ namespace jdi } } + // 拾色器打开时接管鼠标与键盘(优先级低于弹出菜单、高于常规组件)。 + if (m_colorPicker.IsOpen()) + { + bool repaint = false; + bool consume = false; + + switch (message) + { + case WM_MOUSEMOVE: + repaint = m_colorPicker.OnMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + consume = true; + break; + case WM_LBUTTONDOWN: + // 拾色器接管输入后,取消底层组件可能仍在进行的拖拽。 + m_panel.OnLButtonUp(0, 0); + m_plot.OnLButtonUp(0, 0); + m_activeTarget = Target::NONE; + m_draggingSplitter = false; + m_colorPicker.OnLButtonDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + if (m_colorPicker.IsOpen()) + { + SetCapture(hwnd); + } + repaint = true; + consume = true; + break; + case WM_LBUTTONUP: + m_colorPicker.OnLButtonUp(); + ReleaseCapture(); + repaint = true; + consume = true; + break; + case WM_KEYDOWN: + m_colorPicker.OnKeyDown(wParam); + repaint = true; + consume = true; + break; + case WM_RBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_MOUSEWHEEL: + m_colorPicker.Close(); + repaint = true; + consume = true; + break; + default: + break; + } + + // 取色变化实时应用到当前选中信号。 + std::uint32_t pickedColor = 0; + if (m_colorPicker.TakeChanged(pickedColor)) + { + m_panel.ApplyColor(pickedColor); + repaint = true; + } + + if (repaint) + { + InvalidateRect(hwnd, nullptr, FALSE); + } + if (consume) + { + return 0; + } + } + switch (message) { case WM_SIZE: @@ -1461,12 +1552,41 @@ namespace jdi break; } case WM_KEYDOWN: + // 线宽数值输入状态优先接管按键(回车提交、Esc 取消、退格删除)。 + if (m_panel.IsEditingWidth()) + { + if (m_panel.OnEditKey(wParam)) + { + InvalidateRect(hwnd, nullptr, FALSE); + } + return 0; + } if (HandleKeyDown(wParam)) { InvalidateRect(hwnd, nullptr, TRUE); return 0; } break; + case WM_CHAR: + { + // 线宽数值仅接受数字与小数点,其余字符一律吞掉。 + if (m_panel.IsEditingWidth()) + { + if (m_panel.OnEditChar(static_cast(wParam))) + { + InvalidateRect(hwnd, nullptr, FALSE); + } + return 0; + } + break; + } + case WM_KILLFOCUS: + // 失焦时提交输入并收起临时界面,避免失去鼠标前景后控件仍处于编辑态。 + m_panel.CloseTransientUi(); + m_colorPicker.Close(); + m_menu.Close(); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; case kMessageRemoveFile: { RemoveFile(static_cast(wParam)); diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index d388c23..9289676 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -43,23 +43,32 @@ namespace jdi } /** - * @brief 组装图例文本:信号名已在括号中携带单位时不再重复追加。 + * @brief 组装图例文本:直接使用列名原文(已含序号与括号描述)。 * @param [IN] data 信号数据。 * @return 图例文本。 */ std::wstring BuildLegendText(const SignalData& data) { - if (data.unit.empty()) - { - return data.name; - } + return data.name; + } - const std::wstring bracketed = L"(" + data.unit + L")"; - if (ContainsInsensitive(data.name, bracketed)) + /** + * @brief 将信号样式颜色按不透明度换算为实际绘制颜色。 + * @param [IN] style 信号样式。 + * @return 叠加不透明度后的 0xAARRGGBB 颜色值。 + */ + std::uint32_t StyleColor(const SignalStyle& style) + { + double opacity = static_cast(style.opacity); + if (opacity < 0.0) { - return data.name; + opacity = 0.0; } - return data.name + L" [" + data.unit + L"]"; + if (opacity > 1.0) + { + opacity = 1.0; + } + return WithAlpha(style.color, static_cast(opacity * 255.0 + 0.5)); } } @@ -1096,7 +1105,8 @@ namespace jdi if (drawLine && !breaks.empty()) { - gd::Pen pen(ToGdiColor(resolved.style->color), resolved.style->lineWidth * m_scale); + gd::Pen pen(ToGdiColor(StyleColor(*resolved.style)), + resolved.style->lineWidth * m_scale); pen.SetLineJoin(gd::LineJoinRound); pen.SetStartCap(gd::LineCapRound); pen.SetEndCap(gd::LineCapRound); @@ -1121,7 +1131,7 @@ namespace jdi m_scale); const std::size_t scatterStride = std::max(1, points.size() / kMaxScatterPoints); - gd::SolidBrush brush(ToGdiColor(resolved.style->color)); + gd::SolidBrush brush(ToGdiColor(StyleColor(*resolved.style))); for (std::size_t k = 0; k < points.size(); k += scatterStride) { g.FillEllipse(&brush, points[k].X - radius, points[k].Y - radius, radius * 2.0f, @@ -1228,7 +1238,7 @@ namespace jdi const float swatchSize = 8.0f * m_scale; UiFillRoundRect(g, gd::RectF(cursorX, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize), - 1.5f * m_scale, resolved.style->color); + 1.5f * m_scale, StyleColor(*resolved.style)); cursorX += swatchSize + 5.0f * m_scale; UiText(g, label, m_fonts->uiSmall, gd::RectF(cursorX, top, textWidth + 2.0f, height), @@ -1425,7 +1435,7 @@ namespace jdi const float radius = 3.4f * m_scale; gd::SolidBrush ring(ToGdiColor(m_theme->cardBg)); - gd::SolidBrush fill(ToGdiColor(resolved.style->color)); + gd::SolidBrush fill(ToGdiColor(StyleColor(*resolved.style))); g.FillEllipse(&ring, x - radius - 1.2f * m_scale, y - radius - 1.2f * m_scale, (radius + 1.2f * m_scale) * 2.0f, (radius + 1.2f * m_scale) * 2.0f); g.FillEllipse(&fill, x - radius, y - radius, radius * 2.0f, radius * 2.0f); diff --git a/Components/SignalPanel/Inc/SignalPanel.hpp b/Components/SignalPanel/Inc/SignalPanel.hpp index 961d428..29be62b 100644 --- a/Components/SignalPanel/Inc/SignalPanel.hpp +++ b/Components/SignalPanel/Inc/SignalPanel.hpp @@ -10,6 +10,7 @@ #include #include +#include "ColorPickerPopup.hpp" #include "DataModel.hpp" #include "PaintUtil.hpp" #include "PopupMenu.hpp" @@ -40,8 +41,8 @@ namespace jdi * @brief 左侧信号面板:按文件分组的信号树、可见性勾选与样式编辑。 * * 面板只读取数据层(DataSet),全部显示配置读写均落在当前标签页(TabConfig)上, - * 因此不同标签页的信号勾选与样式互不影响。所有弹出菜单均使用主题化自绘菜单, - * 不出现系统默认菜单样式。 + * 因此不同标签页的信号勾选与样式互不影响。所有弹出菜单与拾色器均使用主题化自绘 + * 浮动面板,不出现系统默认菜单样式。 */ class SignalPanel { @@ -49,13 +50,15 @@ namespace jdi SignalPanel(); /** - * @brief 绑定宿主窗口、字体、主题与共用弹出菜单。 + * @brief 绑定宿主窗口、字体、主题与共用浮动控件。 * @param [IN] host 宿主窗口句柄。 * @param [IN] fonts 字体集合,需在面板生命周期内保持有效。 * @param [IN] theme 主题配色,需在面板生命周期内保持有效。 * @param [IN] menu 共用的主题化弹出菜单,需在面板生命周期内保持有效。 + * @param [IN] colorPicker 共用的浮动拾色器,需在面板生命周期内保持有效。 */ - void Setup(HWND host, const UiFonts* fonts, const Theme* theme, PopupMenu* menu); + void Setup(HWND host, const UiFonts* fonts, const Theme* theme, PopupMenu* menu, + ColorPickerPopup* colorPicker); /** * @brief 绑定数据层与当前标签页配置。 @@ -65,7 +68,7 @@ namespace jdi void SetContext(const DataSet* dataSet, TabConfig* tab); /** - * @brief 设置宿主客户区矩形(用于约束弹出菜单位置)。 + * @brief 设置宿主客户区矩形(用于约束浮动面板位置)。 * @param [IN] bounds 客户区矩形。 */ void SetClientBounds(const gd::RectF& bounds) { m_clientBounds = bounds; } @@ -73,19 +76,13 @@ namespace jdi /** * @brief 标记列表结构失效(数据或折叠状态变化后调用)。 */ - void InvalidateRows() - { - m_rowsDirty = true; - } + void InvalidateRows() { m_rowsDirty = true; } /** * @brief 判断列表内容是否需要纵向滚动。 * @return 内容超出可视区域返回 true。 */ - bool NeedsScrollBar() const - { - return m_contentHeight > m_listRect.Height + 0.5f; - } + bool NeedsScrollBar() const { return m_contentHeight > m_listRect.Height + 0.5f; } /** * @brief 设置 DPI 缩放比例。 @@ -122,7 +119,7 @@ namespace jdi bool OnLButtonDown(int x, int y); /** - * @brief 处理鼠标左键抬起。 + * @brief 处理鼠标左键抬起(网格徽标与自定义颜色入口在此刻触发展开)。 * @param [IN] x 客户区横坐标。 * @param [IN] y 客户区纵坐标。 * @return 视觉状态发生变化返回 true。 @@ -130,7 +127,7 @@ namespace jdi bool OnLButtonUp(int x, int y); /** - * @brief 处理鼠标左键双击(切换信号可见性)。 + * @brief 处理鼠标左键双击(线宽数值进入输入状态,其余行切换可见性)。 * @param [IN] x 客户区横坐标。 * @param [IN] y 客户区纵坐标。 * @return 需要重绘返回 true。 @@ -166,7 +163,34 @@ namespace jdi bool OnMenuCommand(const MenuResult& result); /** - * @brief 关闭自定义颜色面板等临时展开的界面。 + * @brief 判断线宽数值框是否处于输入状态。 + * @return 正在输入返回 true。 + */ + bool IsEditingWidth() const { return m_widthEditing; } + + /** + * @brief 向线宽输入框送入一个字符。 + * @param [IN] character 输入字符('\r' 提交、'\b' 退格)。 + * @return 界面状态发生变化返回 true。 + */ + bool OnEditChar(wchar_t character); + + /** + * @brief 向线宽输入框送入一个按键。 + * @param [IN] key 虚拟键码。 + * @return 界面状态发生变化返回 true。 + */ + bool OnEditKey(WPARAM key); + + /** + * @brief 将浮动拾色器的取色结果应用到当前选中信号。 + * @param [IN] color 0xAARRGGBB 颜色值。 + * @return 应用成功返回 true。 + */ + bool ApplyColor(std::uint32_t color); + + /** + * @brief 关闭自定义颜色面板与线宽输入状态。 * @return 界面状态发生变化返回 true。 */ bool CloseTransientUi(); @@ -215,18 +239,19 @@ namespace jdi }; /** - * @brief 样式编辑区中的可点击条目类型。 + * @brief 样式编辑区中的可交互条目类型。 */ enum class StyleHit : int { NONE = 0, /**< 未命中。 */ COLOR, /**< 预设颜色色块。 */ CUSTOM_CHIP, /**< 自定义颜色入口。 */ - WIDTH_PRESET, /**< 线宽预设按钮。 */ + WIDTH_PRESET, /**< 线宽预设按钮(细/粗)。 */ WIDTH_MINUS, /**< 线宽减号。 */ WIDTH_PLUS, /**< 线宽加号。 */ + WIDTH_VALUE, /**< 线宽数值框。 */ MODE, /**< 绘制样式按钮。 */ - SLIDER, /**< 自定义颜色滑块。 */ + OPACITY, /**< 不透明度滑块。 */ TOGGLE_ALL /**< 全部显示/隐藏按钮。 */ }; @@ -312,18 +337,11 @@ namespace jdi void PaintSignalRow(gd::Graphics& g, int row); /** - * @brief 绘制样式编辑区。 + * @brief 绘制样式编辑区(固定四行:颜色 / 线宽 / 样式 / 透明度)。 * @param [IN,OUT] g 绘图上下文。 */ void PaintStyleSection(gd::Graphics& g); - /** - * @brief 绘制自定义颜色面板。 - * @param [IN,OUT] g 绘图上下文。 - * @param [IN] style 当前编辑的信号样式。 - */ - void PaintColorPicker(gd::Graphics& g, const SignalStyle& style); - /** * @brief 绘制列表滚动条。 * @param [IN,OUT] g 绘图上下文。 @@ -342,7 +360,7 @@ namespace jdi void LayoutStyleControls(); /** - * @brief 取得当前样式编辑区所需高度。 + * @brief 取得样式编辑区固定所需高度。 * @return 逻辑高度(未乘缩放比例)。 */ float DesiredStyleHeight() const; @@ -371,6 +389,13 @@ namespace jdi */ void OpenLaneMenu(const SignalRef& ref, int x, int y); + /** + * @brief 打开浮动拾色器(由自定义颜色入口触发)。 + * @param [IN] style 当前信号样式。 + * @return 打开成功返回 true。 + */ + bool OpenColorPickerPopup(const SignalStyle& style); + /** * @brief 判断当前标签页中全部信号是否均为可见状态。 * @return 全部可见且至少有一条信号返回 true。 @@ -391,20 +416,34 @@ namespace jdi bool StepLineWidth(float delta); /** - * @brief 展开自定义颜色面板并同步初始取色。 - * @param [IN] style 当前信号样式。 - * @return 界面状态发生变化返回 true。 + * @brief 进入线宽数值输入状态。 + * @return 状态发生变化返回 true。 */ - bool OpenColorPicker(const SignalStyle& style); + bool BeginWidthEdit(); - HWND m_host = nullptr; /**< 宿主窗口句柄。 */ - const UiFonts* m_fonts = nullptr; /**< 字体资源。 */ - const Theme* m_theme = nullptr; /**< 主题资源。 */ - PopupMenu* m_menu = nullptr; /**< 共用弹出菜单。 */ - const DataSet* m_dataSet = nullptr; /**< 数据集合。 */ - TabConfig* m_tab = nullptr; /**< 当前标签页配置。 */ - float m_scale = 1.0f; /**< DPI 缩放比例。 */ - gd::RectF m_clientBounds; /**< 宿主客户区矩形。 */ + /** + * @brief 结束线宽数值输入状态。 + * @param [IN] commit 是否提交输入值。 + * @return 状态或取值发生变化返回 true。 + */ + bool EndWidthEdit(bool commit); + + /** + * @brief 调整当前信号不透明度。 + * @param [IN] x 客户区横坐标。 + * @return 取值发生变化返回 true。 + */ + bool ApplyOpacityFromX(float x); + + HWND m_host = nullptr; /**< 宿主窗口句柄。 */ + const UiFonts* m_fonts = nullptr; /**< 字体资源。 */ + const Theme* m_theme = nullptr; /**< 主题资源。 */ + PopupMenu* m_menu = nullptr; /**< 共用弹出菜单。 */ + ColorPickerPopup* m_colorPicker = nullptr; /**< 共用浮动拾色器。 */ + const DataSet* m_dataSet = nullptr; /**< 数据集合。 */ + TabConfig* m_tab = nullptr; /**< 当前标签页配置。 */ + float m_scale = 1.0f; /**< DPI 缩放比例。 */ + gd::RectF m_clientBounds; /**< 宿主客户区矩形。 */ gd::RectF m_area; /**< 面板整体区域。 */ gd::RectF m_headerRect; /**< 标题区域。 */ @@ -415,7 +454,8 @@ namespace jdi std::array m_colorRects; /**< 预设颜色色块矩形。 */ int m_colorCount = 0; /**< 实际可用的预设颜色数量。 */ - gd::RectF m_customChipRect; /**< 自定义颜色入口矩形。 */ + gd::RectF m_customChipRect; /**< 自定义颜色入口矩形(与预设色块同尺寸)。 */ + float m_swatchSize = 18.0f; /**< 颜色色块边长(逻辑像素)。 */ gd::RectF m_widthThinRect; /**< 最细线宽预设矩形。 */ gd::RectF m_widthThickRect; /**< 最粗线宽预设矩形。 */ @@ -426,10 +466,8 @@ namespace jdi std::array m_modeRects; /**< 绘制样式按钮矩形。 */ - gd::RectF m_pickerRect; /**< 自定义颜色面板整体矩形。 */ - std::array m_sliderRects; /**< RGB 滑块轨道矩形。 */ - gd::RectF m_pickerPreviewRect; /**< 自定义颜色预览矩形。 */ - std::array m_pickerChannels = {0.0, 0.0, 0.0}; /**< 自定义颜色的 RGB 分量(0-1)。 */ + gd::RectF m_opacityTrackRect; /**< 不透明度滑块轨道矩形。 */ + gd::RectF m_opacityValueRect; /**< 不透明度数值文本矩形。 */ std::vector m_rows; /**< 列表行集合(惰性缓存)。 */ bool m_rowsDirty = true; /**< 列表行集合是否需要重建。 */ @@ -440,13 +478,20 @@ namespace jdi float m_scrollOffset = 0.0f; /**< 列表滚动偏移量。 */ bool m_draggingScrollBar = false; /**< 是否正在拖拽滚动条。 */ int m_scrollGrabOffset = 0; /**< 拖拽时鼠标相对滑块顶部的偏移。 */ + bool m_draggingOpacity = false; /**< 是否正在拖拽不透明度滑块。 */ int m_hoverRow = -1; /**< 当前悬停行。 */ RowPart m_hoverPart = RowPart::NONE; /**< 当前悬停的行内区域。 */ StyleHit m_hoverStyle = StyleHit::NONE; /**< 当前悬停的样式条目类型。 */ int m_hoverStyleIndex = -1; /**< 当前悬停的样式条目下标。 */ - bool m_colorPickerOpen = false; /**< 自定义颜色面板是否展开。 */ - int m_draggingSlider = -1; /**< 正在拖拽的滑块下标,-1 表示未拖拽。 */ + bool m_widthEditing = false; /**< 线宽数值是否处于输入状态。 */ + bool m_widthEditFresh = false; /**< 输入缓冲是否仍为初始值(首次键入应整体替换)。 */ + std::wstring m_widthEditText; /**< 线宽输入缓冲。 */ + + bool m_pendingLaneMenu = false; /**< 左键抬起时是否弹出网格归属菜单。 */ + SignalRef m_pendingLaneRef; /**< 待弹出菜单的目标信号。 */ + bool m_pendingColorPicker = false; /**< 左键抬起时是否弹出拾色器。 */ + bool m_layoutDirty = false; /**< 是否需要重新布局。 */ SignalRef m_menuTargetSignal; /**< 菜单操作的目标信号。 */ diff --git a/Components/SignalPanel/Src/SignalPanel.cpp b/Components/SignalPanel/Src/SignalPanel.cpp index 3beeaa2..943521e 100644 --- a/Components/SignalPanel/Src/SignalPanel.cpp +++ b/Components/SignalPanel/Src/SignalPanel.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "Utf8Util.hpp" @@ -13,27 +14,31 @@ namespace jdi const float kHeaderHeight = 38.0f; /**< 标题区高度(逻辑像素)。 */ const float kFileRowHeight = 30.0f; /**< 文件分组行高。 */ const float kSignalRowHeight = 27.0f; /**< 信号行高。 */ - const float kStyleBaseHeight = 124.0f; /**< 样式编辑区基础高度。 */ - const float kColorPickerHeight = 132.0f;/**< 自定义颜色面板高度。 */ - const float kRowHeight = 26.0f; /**< 样式区单行高度。 */ - const float kRowGap = 10.0f; /**< 样式区行间距。 */ - const float kStylePadTop = 12.0f; /**< 样式区上内边距。 */ - const float kStylePadH = 12.0f; /**< 面板左右内边距。 */ const float kPadding = 12.0f; /**< 通用内边距。 */ const float kCheckSize = 15.0f; /**< 复选框尺寸。 */ const float kSwatchSize = 12.0f; /**< 信号行颜色小方块尺寸。 */ const float kBadgeWidth = 30.0f; /**< 网格徽标宽度。 */ const float kBadgeHeight = 17.0f; /**< 网格徽标高度。 */ const float kCorner = 4.0f; /**< 通用圆角半径。 */ - const float kLineLabelWidth = 40.0f; /**< 样式行标签列宽度。 */ const float kDisclosureWidth = 16.0f; /**< 折叠三角占位宽度。 */ - const float kIndent = 20.0f; /**< 信号行相对文件行的缩进。 */ + const float kIndent = 14.0f; /**< 信号行相对文件行的缩进。 */ const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */ const float kCloseButtonSize = 18.0f; /**< 文件行关闭按钮尺寸。 */ - const float kSliderLabelWidth = 18.0f; /**< 滑块行标签列宽度。 */ - const float kSliderValueWidth = 42.0f; /**< 滑块数值显示宽度。 */ - const float kSliderRowHeight = 24.0f; /**< 滑块行高。 */ - const float kSliderGap = 4.0f; /**< 滑块行间距。 */ + + // 样式编辑区:固定四行网格,行高与间距恒定,保证面板宽度变化时控件不散架。 + const float kStylePadH = 12.0f; /**< 样式区左右内边距。 */ + const float kStylePadV = 12.0f; /**< 样式区上下内边距。 */ + const float kStyleRowHeight = 26.0f; /**< 样式区单行高度。 */ + const float kStyleRowGap = 10.0f; /**< 样式区行间距。 */ + const float kStyleLabelWidth = 44.0f; /**< 样式行标签列宽度。 */ + const float kStyleRowCount = 4.0f; /**< 样式区固定行数。 */ + const float kSwatchEdge = 18.0f; /**< 颜色色块边长(含自定义颜色入口)。 */ + const float kSwatchGap = 5.0f; /**< 颜色色块间距。 */ + const float kWidthPresetWidth = 46.0f; /**< 线宽预设按钮宽度。 */ + const float kWidthSpinButton = 22.0f; /**< 线宽旋钮按钮宽度。 */ + const float kWidthValueWidth = 62.0f; /**< 线宽数值框宽度。 */ + const float kOpacityValueWidth = 52.0f; /**< 不透明度数值宽度。 */ + const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */ const float kWidthMin = 0.5f; /**< 允许的最小线宽。 */ const float kWidthMax = 6.0f; /**< 允许的最大线宽。 */ @@ -41,7 +46,7 @@ namespace jdi const float kWidthThinPreset = 1.0f; /**< 最细线宽预设值。 */ const float kWidthThickPreset = 3.0f; /**< 最粗线宽预设值。 */ - /** @brief 可选绘制样式顺序(已取消阶梯方式)。 */ + /** @brief 可选绘制样式顺序(不含阶梯方式)。 */ const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER}; const int kModeOptionCount = static_cast(sizeof(kModeOptions) / sizeof(kModeOptions[0])); @@ -53,7 +58,7 @@ namespace jdi */ gd::RectF SliderHitRect(const gd::RectF& track, float scale) { - return gd::RectF(track.X - 4.0f * scale, track.Y - 9.0f * scale, track.Width + 8.0f * scale, + return gd::RectF(track.X - 6.0f * scale, track.Y - 9.0f * scale, track.Width + 12.0f * scale, track.Height + 18.0f * scale); } @@ -63,11 +68,11 @@ namespace jdi * @param [IN] track 轨道矩形。 * @param [IN] ratio 当前值比例(0-1)。 * @param [IN] fillColor 已填充部分颜色。 - * @param [IN] hovered 是否处于悬停状态。 + * @param [IN] active 是否处于悬停或拖拽状态。 * @param [IN] scale 缩放比例。 */ void DrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor, - bool hovered, float scale) + bool active, float scale) { const float radius = track.Height * 0.5f; UiFillRoundRect(g, track, radius, 0x2A657B83u); @@ -77,7 +82,7 @@ namespace jdi UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor); const float knobX = track.X + static_cast(ratio) * track.Width; - const float knobRadius = radius + (hovered ? 2.5f * scale : 1.0f * scale); + const float knobRadius = radius + (active ? 2.5f * scale : 0.8f * scale); gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu)); gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale); @@ -88,16 +93,28 @@ namespace jdi } /** - * @brief 生成十六进制颜色文本。 + * @brief 将颜色按不透明度叠加为实际显示颜色。 * @param [IN] color 0xAARRGGBB 颜色值。 - * @return 形如 #RRGGBB 的文本。 + * @param [IN] opacity 不透明度(0-1)。 + * @return 叠加后的颜色值。 */ - std::wstring ColorToHex(std::uint32_t color) + std::uint32_t ApplyOpacity(std::uint32_t color, float opacity) { - wchar_t buffer[16]; - swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu, - (color >> 8) & 0xFFu, color & 0xFFu); - return std::wstring(buffer); + double alpha = static_cast(opacity); + alpha = std::max(0.0, std::min(1.0, alpha)); + return WithAlpha(color, static_cast(alpha * 255.0 + 0.5)); + } + + /** + * @brief 将数值限制到合法区间。 + * @param [IN] value 输入值。 + * @param [IN] low 下限。 + * @param [IN] high 上限。 + * @return 限制后的数值。 + */ + float ClampFloat(float value, float low, float high) + { + return (value < low) ? low : ((value > high) ? high : value); } } @@ -105,15 +122,16 @@ namespace jdi { m_colorRects.fill(gd::RectF()); m_modeRects.fill(gd::RectF()); - m_sliderRects.fill(gd::RectF()); } - void SignalPanel::Setup(HWND host, const UiFonts* fonts, const Theme* theme, PopupMenu* menu) + void SignalPanel::Setup(HWND host, const UiFonts* fonts, const Theme* theme, PopupMenu* menu, + ColorPickerPopup* colorPicker) { m_host = host; m_fonts = fonts; m_theme = theme; m_menu = menu; + m_colorPicker = colorPicker; } void SignalPanel::SetContext(const DataSet* dataSet, TabConfig* tab) @@ -122,8 +140,11 @@ namespace jdi m_tab = tab; m_rowsDirty = true; m_scrollOffset = 0.0f; - m_colorPickerOpen = false; - m_draggingSlider = -1; + m_draggingOpacity = false; + m_widthEditing = false; + m_widthEditText.clear(); + m_pendingLaneMenu = false; + m_pendingColorPicker = false; } void SignalPanel::SetScale(float scale) @@ -147,7 +168,8 @@ namespace jdi float SignalPanel::DesiredStyleHeight() const { - return kStyleBaseHeight + (m_colorPickerOpen ? kColorPickerHeight : 0.0f); + return kStylePadV * 2.0f + kStyleRowCount * kStyleRowHeight + + (kStyleRowCount - 1.0f) * kStyleRowGap; } bool SignalPanel::TakeLayoutDirty() @@ -157,18 +179,6 @@ namespace jdi return dirty; } - bool SignalPanel::CloseTransientUi() - { - if (!m_colorPickerOpen) - { - return false; - } - m_colorPickerOpen = false; - m_draggingSlider = -1; - m_layoutDirty = true; - return true; - } - void SignalPanel::BuildRows() { m_rows.clear(); @@ -412,125 +422,100 @@ namespace jdi void SignalPanel::LayoutStyleControls() { - const float labelWidth = kLineLabelWidth * m_scale; + const float labelWidth = kStyleLabelWidth * m_scale; const float contentLeft = m_styleRect.X + kStylePadH * m_scale + labelWidth; - const float contentWidth = std::max(40.0f, m_styleRect.Width - kStylePadH * 2.0f * m_scale - labelWidth); + const float contentWidth = std::max(60.0f, m_styleRect.Width - kStylePadH * 2.0f * m_scale - labelWidth); - const float row0Top = m_styleRect.Y + kStylePadTop * m_scale; - const float row1Top = row0Top + (kRowHeight + kRowGap) * m_scale; - const float row2Top = row1Top + (kRowHeight + kRowGap) * m_scale; + const float rowHeight = kStyleRowHeight * m_scale; + const float row0Top = m_styleRect.Y + kStylePadV * m_scale; - // ---- 第一行:颜色(预设色块 + 自定义颜色入口)---- + // ---- 第一行:颜色(预设色块 + 同尺寸自定义颜色入口)---- int paletteCount = 0; SignalPalette(paletteCount); - const float chipSize = 22.0f * m_scale; - const float chipGap = 8.0f * m_scale; - const float swatchGap = 4.0f * m_scale; - const float available = std::max(swatchGap, contentWidth - chipGap - chipSize); - + float swatchSize = kSwatchEdge * m_scale; + const float swatchGap = kSwatchGap * m_scale; int count = std::min(paletteCount, static_cast(m_colorRects.size())); - float swatchSize = 18.0f * m_scale; - while (count > 1) - { - const float fitted = - (available - swatchGap * static_cast(count - 1)) / static_cast(count); - if (fitted >= 12.0f * m_scale) - { - swatchSize = std::min(18.0f * m_scale, fitted); - break; - } - --count; - } - if (count <= 1) - { - count = std::max(1, std::min(count, 1)); - swatchSize = std::min(18.0f * m_scale, available); - } - m_colorCount = count; - const float swatchTop = row0Top + (kRowHeight * m_scale - swatchSize) * 0.5f; + // 为自定义颜色入口预留一个同尺寸格位,优先整体缩小色块以保留全部预设颜色。 + if (count > 0) + { + const float fit = (contentWidth - swatchGap * static_cast(count)) / + static_cast(count + 1); + if (fit < swatchSize) + { + swatchSize = fit; + } + // 色块小于可用下限时再从末尾裁剪预设,避免尺寸不可用。 + const float minSwatch = 14.0f * m_scale; + while (count > 1 && swatchSize < minSwatch) + { + --count; + swatchSize = (contentWidth - swatchGap * static_cast(count)) / + static_cast(count + 1); + } + } + m_colorCount = std::max(0, count); + m_swatchSize = swatchSize / m_scale; + + const float swatchTop = row0Top + (rowHeight - swatchSize) * 0.5f; for (int index = 0; index < m_colorCount; ++index) { const float x = contentLeft + static_cast(index) * (swatchSize + swatchGap); m_colorRects[static_cast(index)] = gd::RectF(x, swatchTop, swatchSize, swatchSize); } + m_customChipRect = + gd::RectF(contentLeft + static_cast(m_colorCount) * (swatchSize + swatchGap), swatchTop, + swatchSize, swatchSize); - const float chipLeft = contentLeft + static_cast(m_colorCount) * (swatchSize + swatchGap) - - swatchGap + chipGap; - m_customChipRect = gd::RectF(std::min(chipLeft, contentLeft + contentWidth - chipSize), - row0Top + (kRowHeight * m_scale - chipSize) * 0.5f, chipSize, chipSize); + // ---- 第二行:线宽(最细 / 数值旋钮 / 最粗)---- + const float presetWidth = kWidthPresetWidth * m_scale; + const float spinButton = kWidthSpinButton * m_scale; + const float valueWidth = kWidthValueWidth * m_scale; + float groupGap = 8.0f * m_scale; - // ---- 第二行:线宽(两个预设 + 数值旋钮)---- - float presetWidth = 46.0f * m_scale; - float buttonWidth = 22.0f * m_scale; - float valueWidth = 62.0f * m_scale; - float gap = 6.0f * m_scale; - const float natural = presetWidth * 2.0f + buttonWidth * 2.0f + valueWidth + gap * 2.0f; - if (natural > contentWidth) + float spinnerWidth = spinButton * 2.0f + valueWidth; + float groupWidth = presetWidth * 2.0f + spinnerWidth + groupGap * 2.0f; + if (groupWidth > contentWidth) { - const float shrink = contentWidth / natural; - presetWidth = std::max(28.0f * m_scale, presetWidth * shrink); - buttonWidth = std::max(16.0f * m_scale, buttonWidth * shrink); - valueWidth = std::max(40.0f * m_scale, valueWidth * shrink); - gap = std::max(3.0f * m_scale, gap * shrink); + const float fit = contentWidth / groupWidth; + groupGap = std::max(3.0f * m_scale, groupGap * fit); + spinnerWidth = std::max(spinButton * 2.0f + 36.0f * m_scale, spinnerWidth * fit); + groupWidth = std::min(contentWidth, presetWidth * 2.0f + spinnerWidth + groupGap * 2.0f); } - const float spinnerWidth = buttonWidth * 2.0f + valueWidth; - const float groupWidth = presetWidth * 2.0f + spinnerWidth + gap * 2.0f; + const float row1Top = row0Top + (kStyleRowHeight + kStyleRowGap) * m_scale; const float groupLeft = contentLeft + std::max(0.0f, (contentWidth - groupWidth) * 0.5f); + const float innerSpinButton = std::min(spinButton, spinnerWidth * 0.25f); + const float innerValueWidth = std::max(24.0f * m_scale, spinnerWidth - innerSpinButton * 2.0f); - m_widthThinRect = gd::RectF(groupLeft, row1Top, presetWidth, kRowHeight * m_scale); - m_widthSpinnerRect = gd::RectF(groupLeft + presetWidth + gap, row1Top, spinnerWidth, - kRowHeight * m_scale); - m_widthMinusRect = gd::RectF(m_widthSpinnerRect.X, row1Top, buttonWidth, kRowHeight * m_scale); - m_widthValueRect = gd::RectF(m_widthMinusRect.X + buttonWidth, row1Top, valueWidth, - kRowHeight * m_scale); - m_widthPlusRect = gd::RectF(m_widthValueRect.X + valueWidth, row1Top, buttonWidth, - kRowHeight * m_scale); - m_widthThickRect = gd::RectF(m_widthSpinnerRect.X + spinnerWidth + gap, row1Top, presetWidth, - kRowHeight * m_scale); + m_widthThinRect = gd::RectF(groupLeft, row1Top, presetWidth, rowHeight); + m_widthSpinnerRect = gd::RectF(groupLeft + presetWidth + groupGap, row1Top, spinnerWidth, rowHeight); + m_widthMinusRect = gd::RectF(m_widthSpinnerRect.X, row1Top, innerSpinButton, rowHeight); + m_widthValueRect = gd::RectF(m_widthMinusRect.X + innerSpinButton, row1Top, innerValueWidth, rowHeight); + m_widthPlusRect = gd::RectF(m_widthValueRect.X + innerValueWidth, row1Top, innerSpinButton, rowHeight); + m_widthThickRect = + gd::RectF(m_widthSpinnerRect.X + spinnerWidth + groupGap, row1Top, presetWidth, rowHeight); // ---- 第三行:样式(线段 / 散点 / 点线)---- + const float row2Top = row1Top + (kStyleRowHeight + kStyleRowGap) * m_scale; const float modeGap = 6.0f * m_scale; const float modeWidth = (contentWidth - modeGap * static_cast(kModeOptionCount - 1)) / static_cast(kModeOptionCount); for (int index = 0; index < kModeOptionCount; ++index) { const float x = contentLeft + static_cast(index) * (modeWidth + modeGap); - m_modeRects[static_cast(index)] = gd::RectF(x, row2Top, modeWidth, kRowHeight * m_scale); + m_modeRects[static_cast(index)] = gd::RectF(x, row2Top, modeWidth, rowHeight); } - // ---- 自定义颜色面板 ---- - if (!m_colorPickerOpen) - { - m_pickerRect = gd::RectF(); - return; - } - - const float pickerTop = row2Top + (kRowHeight + kRowGap) * m_scale; - m_pickerRect = gd::RectF(m_styleRect.X + kStylePadH * m_scale, pickerTop, - std::max(40.0f, m_styleRect.Width - kStylePadH * 2.0f * m_scale), - kColorPickerHeight * m_scale - kRowGap * m_scale); - - const float sliderLabelX = m_pickerRect.X + 6.0f * m_scale; - const float trackLeft = sliderLabelX + kSliderLabelWidth * m_scale; - const float trackWidth = - std::max(24.0f, m_pickerRect.Width - (trackLeft - m_pickerRect.X) - 6.0f * m_scale - - kSliderValueWidth * m_scale); - const float trackHeight = 6.0f * m_scale; - const float firstTop = m_pickerRect.Y + 10.0f * m_scale; - - for (int index = 0; index < 3; ++index) - { - const float rowTop = firstTop + static_cast(index) * (kSliderRowHeight + kSliderGap) * m_scale; - m_sliderRects[static_cast(index)] = - gd::RectF(trackLeft, rowTop + (kSliderRowHeight * m_scale - trackHeight) * 0.5f, trackWidth, - trackHeight); - } - - const float previewTop = firstTop + 3.0f * (kSliderRowHeight + kSliderGap) * m_scale; - m_pickerPreviewRect = gd::RectF(trackLeft, previewTop, 54.0f * m_scale, 20.0f * m_scale); + // ---- 第四行:不透明度 ---- + const float row3Top = row2Top + (kStyleRowHeight + kStyleRowGap) * m_scale; + const float opacityValueWidth = kOpacityValueWidth * m_scale; + const float trackWidth = std::max(40.0f, contentWidth - opacityValueWidth - 8.0f * m_scale); + m_opacityTrackRect = gd::RectF(contentLeft, row3Top + (rowHeight - kTrackHeight * m_scale) * 0.5f, + trackWidth, kTrackHeight * m_scale); + m_opacityValueRect = gd::RectF(contentLeft + trackWidth + 8.0f * m_scale, row3Top, + opacityValueWidth - 8.0f * m_scale, rowHeight); } void SignalPanel::Layout(const gd::RectF& area) @@ -712,8 +697,7 @@ namespace jdi { UiFillRoundRect(g, closeRect, 3.0f * m_scale, m_theme->pressed); } - UiDrawCrossIcon(g, closeRect, - closeHot ? m_theme->danger : m_theme->textMuted, 1.5f * m_scale); + UiDrawCrossIcon(g, closeRect, closeHot ? m_theme->danger : m_theme->textMuted, 1.5f * m_scale); } void SignalPanel::PaintSignalRow(gd::Graphics& g, int row) @@ -754,12 +738,13 @@ namespace jdi kCheckSize * m_scale, kCheckSize * m_scale); UiDrawCheckBox(g, checkRect, style->visible ? 1 : 0, 3.0f * m_scale); - // 颜色块。 - const gd::RectF swatchRect(checkRect.X + checkRect.Width + 8.0f * m_scale, + // 颜色块(叠加不透明度,直观反映显示效果)。 + const std::uint32_t swatchColor = ApplyOpacity(style->color, style->opacity); + const gd::RectF swatchRect(checkRect.X + checkRect.Width + 6.0f * m_scale, centerY - kSwatchSize * m_scale * 0.5f, kSwatchSize * m_scale, kSwatchSize * m_scale); UiFillRoundRect(g, swatchRect, 2.5f * m_scale, - style->visible ? style->color : WithAlpha(style->color, 0x55)); + style->visible ? swatchColor : WithAlpha(style->color, 0x50)); // 网格徽标(可点击,用于调整所属网格)。 const int laneIndex = m_tab->LaneOfSignal(ref); @@ -772,24 +757,13 @@ namespace jdi nameRightLimit = badgeRect.X - 6.0f * m_scale; } - // 名称与单位。 - const float unitWidth = signal->unit.empty() - ? 0.0f - : UiMeasureTextWidth(m_fonts->uiSmall, L" " + signal->unit) + 4.0f * m_scale; - const float nameLeft = swatchRect.X + swatchRect.Width + 8.0f * m_scale; - const float nameWidth = std::max(16.0f, nameRightLimit - unitWidth - nameLeft); + // 名称:完全保留文件内的列名(含 "[n]" 序号与括号描述)。 + const float nameLeft = swatchRect.X + swatchRect.Width + 6.0f * m_scale; + 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->name, m_fonts->ui, nameWidth), m_fonts->ui, gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), nameColor, TextAlign::LEFT); - if (!signal->unit.empty()) - { - const std::wstring unitText = L" " + signal->unit; - UiText(g, unitText, m_fonts->uiSmall, - gd::RectF(nameLeft + nameWidth, rect.Y, unitWidth, rect.Height), m_theme->textMuted, - TextAlign::LEFT); - } - if (badgeVisible) { const bool badgeHot = hovered && (m_hoverPart == RowPart::LANE_BADGE); @@ -839,7 +813,7 @@ namespace jdi UiFillRect(g, m_styleRect, m_theme->panelBg); const SignalStyle* style = (m_tab == nullptr) ? nullptr : m_tab->Style(m_tab->Selected()); - if (style == nullptr) + if (style == nullptr || m_tab->Selected().IsValid() == false) { UiText(g, L"请选择信号以编辑样式", m_fonts->uiSmall, m_styleRect, m_theme->textMuted, TextAlign::CENTER); @@ -847,24 +821,26 @@ namespace jdi } const float labelX = m_styleRect.X + kStylePadH * m_scale; - const float labelWidth = kLineLabelWidth * m_scale; + const float labelWidth = kStyleLabelWidth * m_scale; + const float rowHeight = kStyleRowHeight * m_scale; - // ---- 颜色 ---- + // ---- 第一行:颜色 ---- { - const float rowTop = m_styleRect.Y + kStylePadTop * m_scale; - UiText(g, L"颜色", m_fonts->uiSmall, gd::RectF(labelX, rowTop, labelWidth, kRowHeight * m_scale), + UiText(g, L"颜色", m_fonts->uiSmall, gd::RectF(labelX, m_customChipRect.Y, labelWidth, rowHeight), m_theme->textMuted, TextAlign::LEFT); int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); + bool presetActive = false; for (int index = 0; index < m_colorCount; ++index) { const gd::RectF& rect = m_colorRects[static_cast(index)]; const std::uint32_t color = palette[index % paletteCount]; const bool hovered = (m_hoverStyle == StyleHit::COLOR && m_hoverStyleIndex == index); const bool active = (style->color == color); + presetActive = presetActive || active; - UiFillRoundRect(g, rect, 4.0f * m_scale, color); + UiFillRoundRect(g, rect, kCorner * m_scale, color); if (active || hovered) { const gd::RectF ring(rect.X - 2.0f * m_scale, rect.Y - 2.0f * m_scale, @@ -875,54 +851,81 @@ namespace jdi } } - // 自定义颜色入口:带加号图标的小方格。 + // 自定义颜色入口:与预设色块完全同尺寸、同外框样式的小方格,仅多一个加号图标。 const bool chipHot = (m_hoverStyle == StyleHit::CUSTOM_CHIP); - const bool chipActive = m_colorPickerOpen; - UiFillRoundRect(g, m_customChipRect, 4.0f * m_scale, - chipActive ? m_theme->accentSoft : (chipHot ? m_theme->hover : m_theme->cardBg)); - UiDrawRoundRect(g, m_customChipRect, 4.0f * m_scale, - chipActive ? m_theme->accent : m_theme->borderStrong, 1.0f); - UiDrawPlusIcon(g, m_customChipRect, chipActive ? m_theme->accent : m_theme->text, 1.6f * m_scale); + const bool chipActive = !presetActive && (m_colorPicker != nullptr) && m_colorPicker->IsOpen(); + UiFillRoundRect(g, m_customChipRect, kCorner * m_scale, m_theme->cardBg); + UiDrawRoundRect(g, m_customChipRect, kCorner * m_scale, m_theme->borderStrong, 1.0f); + UiDrawPlusIcon(g, m_customChipRect, chipHot ? m_theme->accent : m_theme->text, + 1.6f * m_scale); + + // 当前颜色不在预设中时,用与预设一致的外框指示“正在使用自定义颜色”。 + if (!presetActive || chipHot) + { + const gd::RectF& chip = m_customChipRect; + const gd::RectF ring(chip.X - 2.0f * m_scale, chip.Y - 2.0f * m_scale, + chip.Width + 4.0f * m_scale, chip.Height + 4.0f * m_scale); + UiDrawRoundRect(g, ring, 5.0f * m_scale, + !presetActive ? m_theme->textStrong : m_theme->borderStrong, + !presetActive ? 1.6f : 1.0f); + } + (void)chipActive; } - // ---- 线宽:两个预设 + 数值旋钮 ---- + // ---- 第二行:线宽 ---- { UiText(g, L"线宽", m_fonts->uiSmall, - gd::RectF(labelX, m_widthThinRect.Y, labelWidth, m_widthThinRect.Height), - m_theme->textMuted, TextAlign::LEFT); + gd::RectF(labelX, m_widthThinRect.Y, labelWidth, rowHeight), m_theme->textMuted, + TextAlign::LEFT); - struct PresetInfo + const gd::RectF presetRects[2] = {m_widthThinRect, m_widthThickRect}; + const float presetWidths[2] = {kWidthThinPreset, kWidthThickPreset}; + for (int index = 0; index < 2; ++index) { - gd::RectF rect; - float width; - int index; - }; - const PresetInfo presets[] = { - {m_widthThinRect, kWidthThinPreset, 0}, - {m_widthThickRect, kWidthThickPreset, 1}, - }; + const gd::RectF& rect = presetRects[index]; + const bool active = std::fabs(style->lineWidth - presetWidths[index]) < 0.01f; + const bool hovered = + (m_hoverStyle == StyleHit::WIDTH_PRESET && m_hoverStyleIndex == index); - for (const PresetInfo& preset : presets) - { - const bool active = std::fabs(style->lineWidth - preset.width) < 0.01f; - const bool hovered = (m_hoverStyle == StyleHit::WIDTH_PRESET && m_hoverStyleIndex == preset.index); - - UiFillRoundRect(g, preset.rect, kCorner * m_scale, + UiFillRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg)); - UiDrawRoundRect(g, preset.rect, kCorner * m_scale, - active ? m_theme->accent : m_theme->border, 1.0f); + UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f); - const float centerY = preset.rect.Y + preset.rect.Height * 0.5f; - UiDrawLine(g, preset.rect.X + 9.0f * m_scale, centerY, preset.rect.X + preset.rect.Width - 9.0f * m_scale, - centerY, active ? m_theme->accent : m_theme->text, preset.width * m_scale); + const float centerY = rect.Y + rect.Height * 0.5f; + UiDrawLine(g, rect.X + 9.0f * m_scale, centerY, rect.X + rect.Width - 9.0f * m_scale, + centerY, active ? m_theme->accent : m_theme->text, + presetWidths[index] * m_scale); } - // 数值输入框。 + // 数值旋钮(线宽输入框)。 + const bool valueHot = (m_hoverStyle == StyleHit::WIDTH_VALUE); UiFillRoundRect(g, m_widthSpinnerRect, kCorner * m_scale, m_theme->cardBg); - UiDrawRoundRect(g, m_widthSpinnerRect, kCorner * m_scale, m_theme->border, 1.0f); + UiDrawRoundRect(g, m_widthSpinnerRect, kCorner * m_scale, + m_widthEditing ? m_theme->accent + : (valueHot ? m_theme->borderStrong : m_theme->border), + m_widthEditing ? 1.6f : 1.0f); - const std::wstring valueText = FormatNumber(style->lineWidth, 1) + L" px"; - UiText(g, valueText, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, TextAlign::CENTER); + if (m_widthEditing) + { + const std::wstring editText = + m_widthEditText.empty() ? std::wstring(L" ") : m_widthEditText; + const float textWidth = UiTextWidth(g, editText, m_fonts->monoSmall); + const float caretX = + m_widthValueRect.X + m_widthValueRect.Width - 6.0f * m_scale - textWidth; + UiText(g, editText + L" px", m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, + TextAlign::RIGHT); + if (caretX > m_widthValueRect.X) + { + const float caretY = m_widthValueRect.Y + 5.0f * m_scale; + UiDrawLine(g, caretX, caretY, caretX, m_widthValueRect.Y + rowHeight - 5.0f * m_scale, + m_theme->accent, 1.4f * m_scale); + } + } + else + { + UiText(g, FormatNumber(style->lineWidth, 1) + L" px", m_fonts->monoSmall, m_widthValueRect, + m_theme->textStrong, TextAlign::CENTER); + } const bool minusHot = (m_hoverStyle == StyleHit::WIDTH_MINUS); const bool plusHot = (m_hoverStyle == StyleHit::WIDTH_PLUS); @@ -937,24 +940,16 @@ namespace jdi { UiFillRoundRect(g, m_widthPlusRect, kCorner * m_scale, m_theme->hover); } - UiDrawLine(g, m_widthMinusRect.X + m_widthMinusRect.Width, m_widthMinusRect.Y + 3.0f * m_scale, - m_widthMinusRect.X + m_widthMinusRect.Width, m_widthMinusRect.Y + m_widthMinusRect.Height - - 3.0f * m_scale, - m_theme->border, 1.0f); - UiDrawLine(g, m_widthPlusRect.X, m_widthPlusRect.Y + 3.0f * m_scale, m_widthPlusRect.X, - m_widthPlusRect.Y + m_widthPlusRect.Height - 3.0f * m_scale, m_theme->border, 1.0f); - UiDrawArrowIcon(g, m_widthMinusRect, - minusEnabled ? m_theme->text : m_theme->textMuted, 1.6f * m_scale, - IconDirection::LEFT); + UiDrawArrowIcon(g, m_widthMinusRect, minusEnabled ? m_theme->text : m_theme->textMuted, + 1.6f * m_scale, IconDirection::LEFT); UiDrawArrowIcon(g, m_widthPlusRect, plusEnabled ? m_theme->text : m_theme->textMuted, 1.6f * m_scale, IconDirection::RIGHT); } - // ---- 样式 ---- + // ---- 第三行:样式 ---- { - const gd::RectF& first = m_modeRects[0]; - UiText(g, L"样式", m_fonts->uiSmall, gd::RectF(labelX, first.Y, labelWidth, first.Height), + UiText(g, L"样式", m_fonts->uiSmall, gd::RectF(labelX, m_modeRects[0].Y, labelWidth, rowHeight), m_theme->textMuted, TextAlign::LEFT); for (int index = 0; index < kModeOptionCount; ++index) @@ -971,56 +966,22 @@ namespace jdi } } - if (m_colorPickerOpen) + // ---- 第四行:不透明度 ---- { - PaintColorPicker(g, *style); + UiText(g, L"透明度", m_fonts->uiSmall, + gd::RectF(labelX, m_opacityValueRect.Y, labelWidth, rowHeight), m_theme->textMuted, + TextAlign::LEFT); + + const bool active = m_draggingOpacity || (m_hoverStyle == StyleHit::OPACITY); + DrawSlider(g, m_opacityTrackRect, static_cast(style->opacity), m_theme->accent, active, + m_scale); + + const int percent = static_cast(style->opacity * 100.0f + 0.5f); + UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect, + m_theme->textStrong, TextAlign::RIGHT); } } - void SignalPanel::PaintColorPicker(gd::Graphics& g, const SignalStyle& style) - { - UiFillRoundRect(g, m_pickerRect, kCorner * m_scale, m_theme->cardBg); - UiDrawRoundRect(g, m_pickerRect, kCorner * m_scale, m_theme->borderStrong, 1.0f); - - static const wchar_t* kChannelNames[3] = {L"R", L"G", L"B"}; - static const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u}; - - for (int index = 0; index < 3; ++index) - { - const gd::RectF& track = m_sliderRects[static_cast(index)]; - const gd::RectF rowRect(m_pickerRect.X, track.Y - 9.0f * m_scale, - m_pickerRect.Width, kSliderRowHeight * m_scale); - - UiText(g, kChannelNames[index], m_fonts->uiSmall, - gd::RectF(m_pickerRect.X + 6.0f * m_scale, rowRect.Y, kSliderLabelWidth * m_scale, - rowRect.Height), - m_theme->textMuted, TextAlign::LEFT); - - DrawSlider(g, track, m_pickerChannels[static_cast(index)], kChannelColors[index], - m_hoverStyle == StyleHit::SLIDER && m_hoverStyleIndex == index, m_scale); - - const int value = static_cast(m_pickerChannels[static_cast(index)] * 255.0 + 0.5); - UiText(g, std::to_wstring(value), m_fonts->monoSmall, - gd::RectF(track.X + track.Width + 6.0f * m_scale, rowRect.Y, kSliderValueWidth * m_scale, - rowRect.Height), - m_theme->text, TextAlign::RIGHT); - } - - // 预览与十六进制值。 - UiText(g, L"预览", m_fonts->uiSmall, - gd::RectF(m_pickerRect.X + 6.0f * m_scale, m_pickerPreviewRect.Y, - kSliderLabelWidth * m_scale + 20.0f * m_scale, m_pickerPreviewRect.Height), - m_theme->textMuted, TextAlign::LEFT); - - UiFillRoundRect(g, m_pickerPreviewRect, 3.0f * m_scale, style.color); - UiDrawRoundRect(g, m_pickerPreviewRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f); - - UiText(g, ColorToHex(style.color), m_fonts->monoSmall, - gd::RectF(m_pickerPreviewRect.X + m_pickerPreviewRect.Width + 8.0f * m_scale, - m_pickerPreviewRect.Y, 96.0f * m_scale, m_pickerPreviewRect.Height), - m_theme->text, TextAlign::LEFT); - } - bool SignalPanel::AreAllSignalsVisible() const { if (m_dataSet == nullptr || m_tab == nullptr || m_dataSet->Empty()) @@ -1042,8 +1003,7 @@ namespace jdi return false; } - float value = style->lineWidth + delta; - value = std::max(kWidthMin, std::min(kWidthMax, value)); + float value = ClampFloat(style->lineWidth + delta, kWidthMin, kWidthMax); value = std::round(value * 10.0f) / 10.0f; if (std::fabs(value - style->lineWidth) < 0.001f) { @@ -1053,45 +1013,213 @@ namespace jdi return true; } - bool SignalPanel::OpenColorPicker(const SignalStyle& style) + bool SignalPanel::BeginWidthEdit() { - m_pickerChannels[0] = static_cast((style.color >> 16) & 0xFFu) / 255.0; - m_pickerChannels[1] = static_cast((style.color >> 8) & 0xFFu) / 255.0; - m_pickerChannels[2] = static_cast(style.color & 0xFFu) / 255.0; - - if (m_colorPickerOpen) + if (m_tab == nullptr) { - return true; + return false; } - m_colorPickerOpen = true; - m_layoutDirty = true; + const SignalStyle* style = m_tab->Style(m_tab->Selected()); + if (style == nullptr) + { + return false; + } + if (m_widthEditing) + { + return false; + } + + m_widthEditText = FormatNumber(style->lineWidth, 1); + m_widthEditing = true; + m_widthEditFresh = true; return true; } - bool SignalPanel::OnMouseMove(int x, int y) + bool SignalPanel::EndWidthEdit(bool commit) { - // 滑块拖拽优先级最高。 - if (m_draggingSlider >= 0 && m_draggingSlider < 3) + if (!m_widthEditing) { - const gd::RectF& track = m_sliderRects[static_cast(m_draggingSlider)]; - m_pickerChannels[static_cast(m_draggingSlider)] = - UiSliderRatioFromX(track, static_cast(x)); - if (m_tab != nullptr) + return false; + } + + bool changed = true; + if (commit && m_tab != nullptr) + { + const std::wstring trimmed = Trim(m_widthEditText); + if (!trimmed.empty()) { - if (SignalStyle* style = m_tab->Style(m_tab->Selected())) + wchar_t* stop = nullptr; + const double parsed = std::wcstod(trimmed.c_str(), &stop); + if (stop != trimmed.c_str() && parsed == parsed) { - const unsigned int r = - static_cast(m_pickerChannels[0] * 255.0 + 0.5); - const unsigned int g = - static_cast(m_pickerChannels[1] * 255.0 + 0.5); - const unsigned int b = - static_cast(m_pickerChannels[2] * 255.0 + 0.5); - style->color = 0xFF000000u | (r << 16) | (g << 8) | b; + if (SignalStyle* style = m_tab->Style(m_tab->Selected())) + { + const float value = ClampFloat(static_cast(parsed), kWidthMin, kWidthMax); + style->lineWidth = std::round(value * 10.0f) / 10.0f; + } } } + } + + m_widthEditing = false; + m_widthEditFresh = false; + m_widthEditText.clear(); + return changed; + } + + bool SignalPanel::OnEditChar(wchar_t character) + { + if (!m_widthEditing) + { + return false; + } + + if (character == L'\r' || character == L'\n') + { + return EndWidthEdit(true); + } + if (character == L'\b') + { + m_widthEditFresh = false; + if (!m_widthEditText.empty()) + { + m_widthEditText.pop_back(); + } return true; } + if (character >= L'0' && character <= L'9') + { + // 首次键入直接替换原有数值,无需先手动删除。 + if (m_widthEditFresh) + { + m_widthEditText.clear(); + m_widthEditFresh = false; + } + if (m_widthEditText.size() < 5) + { + m_widthEditText.push_back(character); + } + return true; + } + if (character == L'.') + { + 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; + } + + bool SignalPanel::OnEditKey(WPARAM key) + { + if (!m_widthEditing) + { + return false; + } + + switch (key) + { + case VK_RETURN: + return EndWidthEdit(true); + case VK_ESCAPE: + return EndWidthEdit(false); + case VK_BACK: + m_widthEditFresh = false; + if (!m_widthEditText.empty()) + { + m_widthEditText.pop_back(); + } + return true; + default: + break; + } + return true; + } + + bool SignalPanel::ApplyColor(std::uint32_t color) + { + if (m_tab == nullptr) + { + return false; + } + SignalStyle* style = m_tab->Style(m_tab->Selected()); + if (style == nullptr) + { + return false; + } + if (style->color == color) + { + return false; + } + style->color = color; + return true; + } + + bool SignalPanel::ApplyOpacityFromX(float x) + { + if (m_tab == nullptr) + { + return false; + } + SignalStyle* style = m_tab->Style(m_tab->Selected()); + if (style == nullptr) + { + return false; + } + + const float value = static_cast(UiSliderRatioFromX(m_opacityTrackRect, x)); + if (std::fabs(value - style->opacity) < 0.005f) + { + return false; + } + style->opacity = value; + return true; + } + + bool SignalPanel::OpenColorPickerPopup(const SignalStyle& style) + { + if (m_colorPicker == nullptr) + { + return false; + } + + const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area; + m_colorPicker->Open(bounds, static_cast(m_customChipRect.X), + static_cast(m_customChipRect.Y + m_customChipRect.Height), style.color); + return true; + } + + bool SignalPanel::CloseTransientUi() + { + bool changed = false; + if (m_widthEditing) + { + changed = EndWidthEdit(true) || changed; + } + if (m_colorPicker != nullptr && m_colorPicker->IsOpen()) + { + m_colorPicker->Close(); + changed = true; + } + return changed; + } + + bool SignalPanel::OnMouseMove(int x, int y) + { + // 拖拽优先级最高。 + if (m_draggingOpacity) + { + return ApplyOpacityFromX(static_cast(x)); + } if (m_draggingScrollBar) { const gd::RectF thumb = ScrollThumbRect(); @@ -1119,20 +1247,7 @@ namespace jdi { hoverStyle = StyleHit::TOGGLE_ALL; } - else if (m_colorPickerOpen) - { - for (int index = 0; index < 3; ++index) - { - if (UiPointInRect(SliderHitRect(m_sliderRects[static_cast(index)], m_scale), fx, fy)) - { - hoverStyle = StyleHit::SLIDER; - hoverStyleIndex = index; - break; - } - } - } - - if (hoverStyle == StyleHit::NONE && m_tab != nullptr && m_tab->Style(m_tab->Selected()) != nullptr) + else if (m_tab != nullptr && m_tab->Style(m_tab->Selected()) != nullptr) { for (int index = 0; index < m_colorCount; ++index) { @@ -1165,6 +1280,10 @@ namespace jdi { hoverStyle = StyleHit::WIDTH_PLUS; } + if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthValueRect, fx, fy)) + { + hoverStyle = StyleHit::WIDTH_VALUE; + } if (hoverStyle == StyleHit::NONE) { for (int index = 0; index < kModeOptionCount; ++index) @@ -1177,6 +1296,11 @@ namespace jdi } } } + if (hoverStyle == StyleHit::NONE && + UiPointInRect(SliderHitRect(m_opacityTrackRect, m_scale), fx, fy)) + { + hoverStyle = StyleHit::OPACITY; + } } if (hoverRow == m_hoverRow && hoverStyle == m_hoverStyle && hoverStyleIndex == m_hoverStyleIndex && @@ -1213,7 +1337,6 @@ namespace jdi } else { - // 点击空白区域时滑块中心对齐鼠标位置。 m_scrollGrabOffset = static_cast(thumb.Height * 0.5f); const float travel = fy - m_scrollBarRect.Y - static_cast(m_scrollGrabOffset); const float usable = m_scrollBarRect.Height - thumb.Height; @@ -1227,26 +1350,13 @@ namespace jdi return true; } - // 自定义颜色滑块:开始拖拽(点击轨道任意处即跳转并进入拖拽)。 - if (m_colorPickerOpen) + m_pendingLaneMenu = false; + m_pendingColorPicker = false; + + // 点击样式区以外的位置时结束线宽输入。 + if (m_widthEditing && !UiPointInRect(m_widthValueRect, fx, fy)) { - for (int index = 0; index < 3; ++index) - { - if (UiPointInRect(SliderHitRect(m_sliderRects[static_cast(index)], m_scale), fx, fy)) - { - m_draggingSlider = index; - m_pickerChannels[static_cast(index)] = - UiSliderRatioFromX(m_sliderRects[static_cast(index)], fx); - if (SignalStyle* style = m_tab->Style(m_tab->Selected())) - { - const unsigned int r = static_cast(m_pickerChannels[0] * 255.0 + 0.5); - const unsigned int g = static_cast(m_pickerChannels[1] * 255.0 + 0.5); - const unsigned int b = static_cast(m_pickerChannels[2] * 255.0 + 0.5); - style->color = 0xFF000000u | (r << 16) | (g << 8) | b; - } - return true; - } - } + EndWidthEdit(true); } // 全部显示/隐藏。 @@ -1272,7 +1382,9 @@ namespace jdi } if (UiPointInRect(m_customChipRect, fx, fy)) { - return OpenColorPicker(*style); + // 在左键抬起时展开,避免按下即被后续消息立即收起。 + m_pendingColorPicker = true; + return true; } if (UiPointInRect(m_widthThinRect, fx, fy)) { @@ -1292,6 +1404,11 @@ namespace jdi { return StepLineWidth(kWidthStep); } + if (UiPointInRect(m_widthValueRect, fx, fy)) + { + // 单击即进入输入状态,双击亦等效。 + return BeginWidthEdit(); + } for (int index = 0; index < kModeOptionCount; ++index) { if (UiPointInRect(m_modeRects[static_cast(index)], fx, fy)) @@ -1300,11 +1417,10 @@ namespace jdi return true; } } - - // 点击样式区内其它位置(颜色面板之外)时收起自定义颜色面板。 - if (m_colorPickerOpen && !UiPointInRect(m_pickerRect, fx, fy)) + if (UiPointInRect(SliderHitRect(m_opacityTrackRect, m_scale), fx, fy)) { - return CloseTransientUi(); + m_draggingOpacity = true; + return ApplyOpacityFromX(fx) || true; } } @@ -1322,7 +1438,6 @@ namespace jdi { if (part == RowPart::CHECK) { - // 分组勾选:全选则清空,否则全选。 const bool allVisible = m_tab->FileAllVisible(*m_dataSet, item.fileIndex); m_tab->SetFileVisible(*m_dataSet, item.fileIndex, !allVisible); m_rowsDirty = true; @@ -1337,7 +1452,6 @@ namespace jdi } return true; } - // 折叠三角与行主体均视为切换折叠。 m_tab->ToggleCollapsed(item.fileIndex); m_rowsDirty = true; return true; @@ -1354,25 +1468,62 @@ namespace jdi } else if (part == RowPart::LANE_BADGE) { - const gd::RectF badge = SignalBadgeRect(row); - OpenLaneMenu(ref, static_cast(badge.X), static_cast(badge.Y + badge.Height)); + // 在左键抬起时弹出,保证菜单不会随着按键松开而消失。 + m_pendingLaneMenu = true; + m_pendingLaneRef = ref; } return true; } - bool SignalPanel::OnLButtonUp(int /*x*/, int /*y*/) + bool SignalPanel::OnLButtonUp(int x, int y) { - if (m_draggingSlider >= 0) + bool changed = false; + + if (m_draggingOpacity) { - m_draggingSlider = -1; - return true; + m_draggingOpacity = false; + changed = true; } if (m_draggingScrollBar) { m_draggingScrollBar = false; - return true; + changed = true; } - return false; + + if (m_pendingColorPicker) + { + m_pendingColorPicker = false; + const float fx = static_cast(x); + const float fy = static_cast(y); + if (UiPointInRect(m_customChipRect, fx, fy) && m_tab != nullptr) + { + if (const SignalStyle* style = m_tab->Style(m_tab->Selected())) + { + changed = OpenColorPickerPopup(*style) || changed; + } + } + } + + if (m_pendingLaneMenu) + { + m_pendingLaneMenu = false; + RowPart part = RowPart::NONE; + const int row = HitRow(x, y, part); + if (row >= 0 && part == RowPart::LANE_BADGE) + { + const ListRow& item = m_rows[static_cast(row)]; + const SignalRef ref{item.fileIndex, item.signalIndex}; + if (ref == m_pendingLaneRef) + { + const gd::RectF badge = SignalBadgeRect(row); + OpenLaneMenu(ref, static_cast(badge.X), + static_cast(badge.Y + badge.Height)); + changed = true; + } + } + } + + return changed; } bool SignalPanel::OnDoubleClick(int x, int y) @@ -1382,6 +1533,15 @@ namespace jdi return false; } + const float fx = static_cast(x); + const float fy = static_cast(y); + + // 线宽数值框:双击进入(或保持)输入状态。 + if (UiPointInRect(m_widthValueRect, fx, fy)) + { + return BeginWidthEdit(); + } + RowPart part = RowPart::NONE; const int row = HitRow(x, y, part); if (row < 0) @@ -1397,6 +1557,12 @@ namespace jdi return true; } + // 网格徽标单击已可打开菜单,双击不再切换可见性。 + if (part == RowPart::LANE_BADGE) + { + return false; + } + const SignalRef ref{item.fileIndex, item.signalIndex}; m_tab->SetSelected(ref); m_tab->ToggleVisible(*m_dataSet, ref); diff --git a/Utilities/Inc/ColorPickerPopup.hpp b/Utilities/Inc/ColorPickerPopup.hpp new file mode 100644 index 0000000..0729d97 --- /dev/null +++ b/Utilities/Inc/ColorPickerPopup.hpp @@ -0,0 +1,161 @@ +#ifndef __COLOR_PICKER_POPUP_HPP__ +#define __COLOR_PICKER_POPUP_HPP__ + +#include +#include +#include + +#include +#include + +#include "PaintUtil.hpp" +#include "Theme.hpp" + +namespace jdi +{ + + /** + * @brief 浮动式 RGB 自定义颜色拾取器。 + * + * 与弹出菜单一致,不创建独立窗口,而是作为宿主绘制的最后一步直接画到后备缓冲区, + * 以浮动面板的形式出现在触发控件附近;取值变化实时生效,点击面板外部即收起。 + */ + class ColorPickerPopup + { + public: + ColorPickerPopup(); + + /** + * @brief 绑定字体与主题资源。 + * @param [IN] fonts 字体集合,需在弹出面板生命周期内保持有效。 + * @param [IN] theme 主题配色,需在弹出面板生命周期内保持有效。 + */ + void Setup(const UiFonts* fonts, const Theme* theme); + + /** + * @brief 设置 DPI 缩放比例。 + * @param [IN] scale 缩放比例(1.0 对应 96 DPI)。 + */ + void SetScale(float scale); + + /** + * @brief 在指定位置打开拾色面板。 + * @param [IN] bounds 宿主客户区矩形,用于把面板限制在窗口内。 + * @param [IN] x 锚点横坐标(客户区坐标)。 + * @param [IN] y 锚点纵坐标(客户区坐标)。 + * @param [IN] color 初始颜色(0xAARRGGBB,仅取 RGB 分量)。 + */ + void Open(const gd::RectF& bounds, int x, int y, std::uint32_t color); + + /** + * @brief 关闭拾色面板。 + */ + void Close(); + + /** + * @brief 判断拾色面板是否处于打开状态。 + * @return 打开返回 true。 + */ + bool IsOpen() const { return m_open; } + + /** + * @brief 获取当前取色结果。 + * @return 0xAARRGGBB 颜色值(Alpha 固定为不透明)。 + */ + std::uint32_t CurrentColor() const; + + /** + * @brief 取出并清除“取值已变化”标记。 + * @param [OUT] color 变化后的颜色。 + * @return 自上次调用以来取值发生过变化返回 true。 + */ + bool TakeChanged(std::uint32_t& color); + + /** + * @brief 绘制拾色面板(应在宿主绘制的最后一步调用)。 + * @param [IN,OUT] g 绘图上下文。 + */ + void Paint(gd::Graphics& g); + + /** + * @brief 处理鼠标移动(含滑块拖拽)。 + * @param [IN] x 客户区横坐标。 + * @param [IN] y 客户区纵坐标。 + * @return 需要重绘返回 true。 + */ + bool OnMouseMove(int x, int y); + + /** + * @brief 处理鼠标左键按下。 + * @param [IN] x 客户区横坐标。 + * @param [IN] y 客户区纵坐标。 + * @return 事件已被消费返回 true。 + */ + bool OnLButtonDown(int x, int y); + + /** + * @brief 处理鼠标左键抬起。 + * @return 需要重绘返回 true。 + */ + bool OnLButtonUp(); + + /** + * @brief 处理键盘按键(Esc 收起)。 + * @param [IN] key 虚拟键码。 + * @return 事件已被消费返回 true。 + */ + bool OnKeyDown(WPARAM key); + + /** + * @brief 判断坐标是否落在拾色面板内部。 + * @param [IN] x 客户区横坐标。 + * @param [IN] y 客户区纵坐标。 + * @return 落在内部返回 true。 + */ + bool ContainsPoint(int x, int y) const; + + private: + /** + * @brief 依据锚点与客户区计算面板矩形与内部控件矩形。 + * @param [IN] bounds 宿主客户区矩形。 + * @param [IN] x 锚点横坐标。 + * @param [IN] y 锚点纵坐标。 + */ + void BuildLayout(const gd::RectF& bounds, int x, int y); + + /** + * @brief 查询坐标命中的滑块下标。 + * @param [IN] x 客户区横坐标。 + * @param [IN] y 客户区纵坐标。 + * @return 滑块下标(0=R,1=G,2=B),未命中返回 -1。 + */ + int SliderAtPoint(int x, int y) const; + + /** + * @brief 应用滑块拖拽位置到对应通道。 + * @param [IN] index 滑块下标。 + * @param [IN] x 客户区横坐标。 + */ + void ApplySlider(int index, float x); + + const UiFonts* m_fonts = nullptr; /**< 字体资源。 */ + const Theme* m_theme = nullptr; /**< 主题资源。 */ + float m_scale = 1.0f; /**< DPI 缩放比例。 */ + + bool m_open = false; /**< 是否处于打开状态。 */ + bool m_changed = false; /**< 取值是否发生变化。 */ + int m_draggingSlider = -1; /**< 正在拖拽的滑块下标,-1 表示未拖拽。 */ + int m_hoverSlider = -1; /**< 当前悬停的滑块下标。 */ + std::array m_channels = {0.0, 0.0, 0.0}; /**< RGB 分量(0-1)。 */ + + gd::RectF m_rect; /**< 面板整体矩形。 */ + gd::RectF m_titleRect; /**< 标题区域。 */ + std::array m_tracks; /**< 滑块轨道矩形。 */ + std::array m_values; /**< 数值文本矩形。 */ + gd::RectF m_previewRect; /**< 预览色块矩形。 */ + gd::RectF m_hexRect; /**< 十六进制文本矩形。 */ + }; + +} // namespace jdi + +#endif // __COLOR_PICKER_POPUP_HPP__ diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index d45ff46..b706e84 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -195,6 +195,7 @@ namespace jdi { bool visible = false; /**< 是否参与绘制。默认关闭,由用户自行勾选。 */ std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */ + float opacity = 1.0f; /**< 不透明度,取值 0.0-1.0,默认 1.0。 */ float lineWidth = 1.6f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */ DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */ }; diff --git a/Utilities/Src/ColorPickerPopup.cpp b/Utilities/Src/ColorPickerPopup.cpp new file mode 100644 index 0000000..ae9e86f --- /dev/null +++ b/Utilities/Src/ColorPickerPopup.cpp @@ -0,0 +1,347 @@ +#include "ColorPickerPopup.hpp" + +#include +#include + +namespace jdi +{ + + namespace + { + const float kPanelWidth = 214.0f; /**< 面板宽度(逻辑像素)。 */ + const float kPadH = 12.0f; /**< 面板左右内边距。 */ + const float kPadV = 10.0f; /**< 面板上下内边距。 */ + const float kTitleHeight = 20.0f; /**< 标题行高度。 */ + const float kTitleGap = 8.0f; /**< 标题与滑块区间距。 */ + const float kRowHeight = 26.0f; /**< 单个滑块行高度。 */ + const float kRowGap = 2.0f; /**< 滑块行间距。 */ + const float kPreviewGap = 10.0f; /**< 滑块区与预览区间距。 */ + const float kPreviewHeight = 24.0f; /**< 预览行高度。 */ + const float kLabelWidth = 16.0f; /**< 通道标签宽度。 */ + const float kLabelGap = 6.0f; /**< 标签与滑块间距。 */ + const float kValueWidth = 40.0f; /**< 数值文本宽度。 */ + const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */ + const float kCorner = 6.0f; /**< 面板圆角半径。 */ + + /** @brief 通道显示名称。 */ + const wchar_t* kChannelNames[3] = {L"R", L"G", L"B"}; + + /** @brief 通道滑块填充色。 */ + const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u}; + + /** + * @brief 绘制水平滑块(轨道 + 已填充部分 + 圆钮)。 + * @param [IN,OUT] g 绘图上下文。 + * @param [IN] track 轨道矩形。 + * @param [IN] ratio 当前值比例(0-1)。 + * @param [IN] fillColor 已填充部分颜色。 + * @param [IN] hovered 是否处于悬停或拖拽状态。 + * @param [IN] scale 缩放比例。 + */ + void DrawChannelSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor, + bool hovered, 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(ratio) * track.Width); + UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor); + + const float knobX = track.X + static_cast(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); + } + + /** + * @brief 将 0-1 分量换算为 0-255 整数。 + * @param [IN] value 分量值。 + * @return 整数分量。 + */ + unsigned int ToByte(double value) + { + const double scaled = value * 255.0 + 0.5; + if (scaled < 0.0) + { + return 0u; + } + if (scaled > 255.0) + { + return 255u; + } + return static_cast(scaled); + } + } + + ColorPickerPopup::ColorPickerPopup() = default; + + void ColorPickerPopup::Setup(const UiFonts* fonts, const Theme* theme) + { + m_fonts = fonts; + m_theme = theme; + } + + void ColorPickerPopup::SetScale(float scale) + { + m_scale = (scale > 0.1f) ? scale : 1.0f; + } + + std::uint32_t ColorPickerPopup::CurrentColor() const + { + const unsigned int r = ToByte(m_channels[0]); + const unsigned int g = ToByte(m_channels[1]); + const unsigned int b = ToByte(m_channels[2]); + return 0xFF000000u | (r << 16) | (g << 8) | b; + } + + bool ColorPickerPopup::TakeChanged(std::uint32_t& color) + { + if (!m_changed) + { + return false; + } + m_changed = false; + color = CurrentColor(); + return true; + } + + void ColorPickerPopup::BuildLayout(const gd::RectF& bounds, int x, int y) + { + const float width = kPanelWidth * m_scale; + const float height = (kPadV * 2.0f + kTitleHeight + kTitleGap + kRowHeight * 3.0f + kRowGap * 2.0f + + kPreviewGap + kPreviewHeight) * + m_scale; + + float left = static_cast(x); + float top = static_cast(y); + + // 优先向右下展开;越界时翻转,最后做边界收敛。 + if (left + width > bounds.X + bounds.Width) + { + left = static_cast(x) - width; + } + if (top + height > bounds.Y + bounds.Height) + { + top = static_cast(y) - height; + } + left = std::max(bounds.X + 2.0f, std::min(left, bounds.X + bounds.Width - width - 2.0f)); + top = std::max(bounds.Y + 2.0f, std::min(top, bounds.Y + bounds.Height - height - 2.0f)); + + m_rect = gd::RectF(left, top, width, height); + + m_titleRect = gd::RectF(left + kPadH * m_scale, top + kPadV * m_scale, + width - kPadH * 2.0f * m_scale, kTitleHeight * m_scale); + + const float trackLeft = left + (kPadH + kLabelWidth + kLabelGap) * m_scale; + const float trackWidth = + std::max(40.0f, width - (trackLeft - left) - (kPadH + kValueWidth) * m_scale); + const float firstTop = m_titleRect.Y + m_titleRect.Height + kTitleGap * m_scale; + + for (int index = 0; index < 3; ++index) + { + const float rowTop = firstTop + static_cast(index) * (kRowHeight + kRowGap) * m_scale; + m_tracks[static_cast(index)] = + gd::RectF(trackLeft, rowTop + (kRowHeight * m_scale - kTrackHeight * m_scale) * 0.5f, + trackWidth, kTrackHeight * m_scale); + m_values[static_cast(index)] = + gd::RectF(trackLeft + trackWidth + 6.0f * m_scale, rowTop, + kValueWidth * m_scale - 6.0f * m_scale, kRowHeight * m_scale); + } + + const float previewTop = firstTop + 3.0f * (kRowHeight + kRowGap) * m_scale + kPreviewGap * m_scale; + m_previewRect = gd::RectF(trackLeft, previewTop + 2.0f * m_scale, 52.0f * m_scale, 20.0f * m_scale); + m_hexRect = gd::RectF(m_previewRect.X + m_previewRect.Width + 8.0f * m_scale, previewTop + 2.0f * m_scale, + 96.0f * m_scale, 20.0f * m_scale); + } + + void ColorPickerPopup::Open(const gd::RectF& bounds, int x, int y, std::uint32_t color) + { + m_channels[0] = static_cast((color >> 16) & 0xFFu) / 255.0; + m_channels[1] = static_cast((color >> 8) & 0xFFu) / 255.0; + m_channels[2] = static_cast(color & 0xFFu) / 255.0; + + m_open = true; + m_changed = false; + m_draggingSlider = -1; + m_hoverSlider = -1; + + BuildLayout(bounds, x, y); + } + + void ColorPickerPopup::Close() + { + m_open = false; + m_draggingSlider = -1; + m_hoverSlider = -1; + } + + bool ColorPickerPopup::ContainsPoint(int x, int y) const + { + if (!m_open) + { + return false; + } + return UiPointInRect(m_rect, static_cast(x), static_cast(y)); + } + + int ColorPickerPopup::SliderAtPoint(int x, int y) const + { + if (!m_open) + { + return -1; + } + + const float fx = static_cast(x); + const float fy = static_cast(y); + for (int index = 0; index < 3; ++index) + { + const gd::RectF& track = m_tracks[static_cast(index)]; + // 命中范围在纵向适当放宽,便于拖拽。 + const gd::RectF hit(track.X - 4.0f * m_scale, track.Y - 9.0f * m_scale, + track.Width + 8.0f * m_scale, track.Height + 18.0f * m_scale); + if (UiPointInRect(hit, fx, fy)) + { + return index; + } + } + return -1; + } + + void ColorPickerPopup::ApplySlider(int index, float x) + { + if (index < 0 || index >= 3) + { + return; + } + const std::size_t slot = static_cast(index); + const double ratio = UiSliderRatioFromX(m_tracks[slot], x); + if (std::fabs(ratio - m_channels[slot]) < 1.0e-6) + { + return; + } + m_channels[slot] = ratio; + m_changed = true; + } + + bool ColorPickerPopup::OnMouseMove(int x, int y) + { + if (!m_open) + { + return false; + } + + if (m_draggingSlider >= 0) + { + ApplySlider(m_draggingSlider, static_cast(x)); + return true; + } + + const int hover = SliderAtPoint(x, y); + if (hover == m_hoverSlider) + { + return false; + } + m_hoverSlider = hover; + return true; + } + + bool ColorPickerPopup::OnLButtonDown(int x, int y) + { + if (!m_open) + { + return false; + } + + if (!ContainsPoint(x, y)) + { + // 点击面板外部:收起面板(外部点击不再下传,避免误触其他控件)。 + Close(); + return true; + } + + m_draggingSlider = SliderAtPoint(x, y); + if (m_draggingSlider >= 0) + { + ApplySlider(m_draggingSlider, static_cast(x)); + return true; + } + return true; + } + + bool ColorPickerPopup::OnLButtonUp() + { + if (!m_open || m_draggingSlider < 0) + { + return false; + } + m_draggingSlider = -1; + return true; + } + + bool ColorPickerPopup::OnKeyDown(WPARAM key) + { + if (!m_open) + { + return false; + } + if (key == VK_ESCAPE) + { + Close(); + } + return true; + } + + void ColorPickerPopup::Paint(gd::Graphics& g) + { + if (!m_open || m_theme == nullptr || m_fonts == nullptr || m_rect.Width <= 0.0f) + { + return; + } + + // 投影。 + UiFillRoundRect(g, gd::RectF(m_rect.X + 1.0f * m_scale, m_rect.Y + 2.0f * m_scale, m_rect.Width, + m_rect.Height), + kCorner * m_scale, 0x14000000u); + UiFillRoundRect(g, gd::RectF(m_rect.X + 2.0f * m_scale, m_rect.Y + 4.0f * m_scale, m_rect.Width, + m_rect.Height), + kCorner * m_scale, 0x10000000u); + + UiFillRoundRect(g, m_rect, kCorner * m_scale, m_theme->cardBg); + UiDrawRoundRect(g, m_rect, kCorner * m_scale, m_theme->borderStrong, 1.0f); + + UiText(g, L"自定义颜色", m_fonts->uiSmallBold, m_titleRect, m_theme->textMuted, TextAlign::LEFT); + + for (int index = 0; index < 3; ++index) + { + const std::size_t slot = static_cast(index); + const gd::RectF& track = m_tracks[slot]; + + const gd::RectF labelRect(m_rect.X + kPadH * m_scale, track.Y - 9.0f * m_scale, + kLabelWidth * m_scale, kRowHeight * m_scale); + UiText(g, kChannelNames[index], m_fonts->uiSmall, labelRect, m_theme->textMuted, TextAlign::LEFT); + + const bool active = (m_draggingSlider == index) || (m_hoverSlider == index); + 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], + m_theme->text, TextAlign::RIGHT); + } + + // 预览与十六进制值。 + const std::uint32_t color = CurrentColor(); + UiFillRoundRect(g, m_previewRect, 3.0f * m_scale, color); + UiDrawRoundRect(g, m_previewRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f); + + 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 diff --git a/Utilities/Src/DataModel.cpp b/Utilities/Src/DataModel.cpp index 810e249..bef5622 100644 --- a/Utilities/Src/DataModel.cpp +++ b/Utilities/Src/DataModel.cpp @@ -37,44 +37,9 @@ namespace jdi return ContainsInsensitive(text, L"time") || ContainsInsensitive(text, L"时间"); } - /** - * @brief 剥离列名中的 "[n]" 序号前缀,得到显示名称。 - * @param [IN] header 原始列名。 - * @return 显示名称。 - */ - std::wstring MakeDisplayName(const std::wstring& header) - { - const std::wstring text = Trim(header); - if (text.size() >= 3 && text[0] == L'[') - { - const std::size_t close = text.find(L']'); - if (close != std::wstring::npos && close > 1 && (close + 1) < text.size()) - { - bool numeric = true; - for (std::size_t i = 1; i < close; ++i) - { - if (!iswdigit(text[i])) - { - numeric = false; - break; - } - } - if (numeric) - { - const std::wstring rest = Trim(text.substr(close + 1)); - if (!rest.empty()) - { - return rest; - } - } - } - } - return text.empty() ? header : text; - } - /** * @brief 从名称末尾的括号中提取单位(仅接受形如 mA / V / deg/s 的短标记)。 - * @param [IN] text 已剥离 "[n]" 前缀的名称。 + * @param [IN] text 列名(已去除首尾空白)。 * @return 单位文本,无法识别时返回空字符串。 */ std::wstring ExtractUnit(const std::wstring& text) @@ -109,46 +74,6 @@ namespace jdi return inner; } - /** - * @brief 从名称末尾移除已被识别为单位(或描述)的括号片段。 - * @param [IN] name 已剥离 "[n]" 前缀的名称。 - * @return 移除尾部括号后的名称(括号片段非单位时原样返回)。 - */ - std::wstring StripBracketSuffix(const std::wstring& name) - { - if (name.size() < 3 || name.back() != L')') - { - return name; - } - - const std::size_t open = name.rfind(L'('); - if (open == std::wstring::npos || open == 0 || open + 2 >= name.size()) - { - return name; - } - - const std::wstring inner = Trim(name.substr(open + 1, name.size() - open - 2)); - if (inner.empty() || inner.size() > 10) - { - return name; - } - - // 仅当括号内容为短单位标记时才剥离,避免误伤 "(5 Points)" 这类描述。 - for (const wchar_t c : inner) - { - const bool allowed = iswalpha(c) || iswdigit(c) || c == L'%' || c == L'/' || c == L'^' || - c == L'-' || c == L'.' || c == L'\u00B0' || c == L'\u00B5' || - c == L'\u03A9' || c == L'\u00B2' || c == L'\u00B3' || c == L'\u00B7'; - if (!allowed) - { - return name; - } - } - - const std::wstring head = Trim(name.substr(0, open)); - return head.empty() ? name : head; - } - /** * @brief 依据全局信号序号从调色板取色,保证跨标签页配色一致。 * @param [IN] globalIndex 全局信号序号。 @@ -421,10 +346,9 @@ namespace jdi SignalData signal; signal.rawHeader = table.headers[column]; { - // 名称与单位分开保存:名称不再保留尾部单位括号,避免重复显示。 - const std::wstring baseName = MakeDisplayName(signal.rawHeader); - signal.unit = ExtractUnit(baseName); - signal.name = StripBracketSuffix(baseName); + // 名称完整保留文件内的列名(含 "[n]" 序号与括号描述),仅用于展示不做改写。 + signal.name = Trim(signal.rawHeader); + signal.unit = ExtractUnit(signal.name); } signal.values.resize(rowCount);