#include "SignalPanel.hpp" #include #include #include #include "Utf8Util.hpp" namespace jdi { namespace { const float kHeaderHeight = 38.0f; /**< 标题区高度(逻辑像素)。 */ const float kFileRowHeight = 30.0f; /**< 文件分组行高。 */ const float kSignalRowHeight = 27.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 kDisclosureWidth = 16.0f; /**< 折叠三角占位宽度。 */ const float kIndent = 14.0f; /**< 信号行相对文件行的缩进。 */ const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */ const float kCloseButtonSize = 18.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; /**< 允许的最大线宽。 */ const float kWidthStep = 0.2f; /**< 线宽增减步进。 */ const float kWidthThinPreset = 1.0f; /**< 最细线宽预设值。 */ const float kWidthThickPreset = 3.0f; /**< 最粗线宽预设值。 */ /** @brief 可选绘制样式顺序(不含阶梯方式)。 */ const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER}; const int kModeOptionCount = static_cast(sizeof(kModeOptions) / sizeof(kModeOptions[0])); /** * @brief 计算滑块的便捷命中矩形(比轨道略大,便于拖拽)。 * @param [IN] track 轨道矩形。 * @param [IN] scale 缩放比例。 * @return 命中矩形。 */ gd::RectF SliderHitRect(const gd::RectF& track, float scale) { return gd::RectF(track.X - 6.0f * scale, track.Y - 9.0f * scale, track.Width + 12.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(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 + (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 将颜色按不透明度叠加为实际显示颜色。 * @param [IN] color 0xAARRGGBB 颜色值。 * @param [IN] opacity 不透明度(0-1)。 * @return 叠加后的颜色值。 */ std::uint32_t ApplyOpacity(std::uint32_t color, float opacity) { 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); } } SignalPanel::SignalPanel() { m_colorRects.fill(gd::RectF()); m_modeRects.fill(gd::RectF()); } 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) { m_dataSet = dataSet; m_tab = tab; m_rowsDirty = true; m_scrollOffset = 0.0f; m_draggingOpacity = false; m_widthEditing = false; m_widthEditText.clear(); m_pendingLaneMenu = false; m_pendingColorPicker = false; } void SignalPanel::SetScale(float scale) { m_scale = (scale > 0.1f) ? scale : 1.0f; } bool SignalPanel::HitTest(int x, int y) const { return UiPointInRect(m_area, static_cast(x), static_cast(y)); } int SignalPanel::TotalSignalCount() const { if (m_dataSet == nullptr) { return 0; } return static_cast(m_dataSet->SignalCount()); } float SignalPanel::DesiredStyleHeight() const { return kStylePadV * 2.0f + kStyleRowCount * kStyleRowHeight + (kStyleRowCount - 1.0f) * kStyleRowGap; } bool SignalPanel::TakeLayoutDirty() { const bool dirty = m_layoutDirty; m_layoutDirty = false; return dirty; } void SignalPanel::BuildRows() { m_rows.clear(); m_rowsDirty = false; if (m_dataSet == nullptr || m_tab == nullptr) { m_contentHeight = 0.0f; return; } const float fileHeight = kFileRowHeight * m_scale; const float signalHeight = kSignalRowHeight * m_scale; const std::vector& files = m_dataSet->Files(); for (std::size_t fileIndex = 0; fileIndex < files.size(); ++fileIndex) { ListRow fileRow; fileRow.kind = RowKind::FILE; fileRow.fileIndex = static_cast(fileIndex); fileRow.height = fileHeight; m_rows.push_back(fileRow); if (m_tab->IsCollapsed(static_cast(fileIndex))) { continue; } const std::size_t signalCount = files[fileIndex].signals.size(); for (std::size_t signalIndex = 0; signalIndex < signalCount; ++signalIndex) { ListRow signalRow; signalRow.kind = RowKind::SIGNAL; signalRow.fileIndex = static_cast(fileIndex); signalRow.signalIndex = static_cast(signalIndex); signalRow.height = signalHeight; m_rows.push_back(signalRow); } } m_contentHeight = 0.0f; for (const ListRow& row : m_rows) { m_contentHeight += row.height; } } void SignalPanel::EnsureRows() { if (m_rowsDirty) { BuildRows(); RefreshScrollBar(); } } void SignalPanel::RefreshScrollBar() { if (m_listFullWidth <= 0.0f) { m_scrollBarVisible = false; return; } m_scrollBarVisible = (m_contentHeight > m_listRect.Height + 0.5f); if (m_scrollBarVisible) { const float thickness = kScrollBarThickness * m_scale; m_listRect.Width = std::max(0.0f, m_listFullWidth - thickness - 2.0f * m_scale); m_scrollBarRect = gd::RectF(m_area.X + m_area.Width - thickness - 3.0f * m_scale, m_listRect.Y, thickness, m_listRect.Height); } else { m_listRect.Width = m_listFullWidth; m_scrollBarRect = gd::RectF(); } ClampScroll(); } float SignalPanel::ContentHeight() const { return m_contentHeight; } void SignalPanel::ClampScroll() { const float maxOffset = std::max(0.0f, m_contentHeight - m_listRect.Height); m_scrollOffset = std::max(0.0f, std::min(m_scrollOffset, maxOffset)); } gd::RectF SignalPanel::RowRect(int row) const { if (row < 0 || row >= static_cast(m_rows.size())) { return gd::RectF(); } float top = m_listRect.Y - m_scrollOffset; for (int index = 0; index < row; ++index) { top += m_rows[static_cast(index)].height; } return gd::RectF(m_listRect.X, top, m_listRect.Width, m_rows[static_cast(row)].height); } gd::RectF SignalPanel::FileCloseRect(int row) const { const gd::RectF rect = RowRect(row); const float size = kCloseButtonSize * m_scale; return gd::RectF(rect.X + rect.Width - kPadding * m_scale - size, rect.Y + (rect.Height - size) * 0.5f, size, size); } gd::RectF SignalPanel::SignalBadgeRect(int row) const { const gd::RectF rect = RowRect(row); const float width = kBadgeWidth * m_scale; const float height = kBadgeHeight * m_scale; return gd::RectF(rect.X + rect.Width - kPadding * m_scale - width, rect.Y + (rect.Height - height) * 0.5f, width, height); } int SignalPanel::HitRow(int x, int y, RowPart& part) { part = RowPart::NONE; EnsureRows(); if (m_listRect.Height <= 0.0f) { return -1; } const float fx = static_cast(x); const float fy = static_cast(y); if (fy < m_listRect.Y || fy > m_listRect.Y + m_listRect.Height) { return -1; } if (fx < m_listRect.X || fx > m_listRect.X + m_listRect.Width) { return -1; } const float local = fy - m_listRect.Y + m_scrollOffset; if (local < 0.0f) { return -1; } float accumulated = 0.0f; for (std::size_t index = 0; index < m_rows.size(); ++index) { const ListRow& row = m_rows[index]; if (local >= accumulated && local < accumulated + row.height) { const int rowIndex = static_cast(index); const gd::RectF rect = RowRect(rowIndex); const bool isFile = (row.kind == RowKind::FILE); const float baseX = rect.X + kPadding * m_scale + (isFile ? 0.0f : kIndent * m_scale); const gd::RectF disclosureRect(baseX, rect.Y, kDisclosureWidth * m_scale, rect.Height); const gd::RectF checkRect(baseX + kDisclosureWidth * m_scale, rect.Y + (rect.Height - kCheckSize * m_scale) * 0.5f, kCheckSize * m_scale, kCheckSize * m_scale); if (isFile) { if (UiPointInRect(disclosureRect, fx, fy)) { part = RowPart::DISCLOSURE; } else if (UiPointInRect(checkRect, fx, fy)) { part = RowPart::CHECK; } else if (UiPointInRect(FileCloseRect(rowIndex), fx, fy)) { part = RowPart::CLOSE; } else { part = RowPart::BODY; } } else { const SignalRef ref{row.fileIndex, row.signalIndex}; const bool badgeVisible = (m_tab != nullptr) && m_tab->IsVisible(ref); if (UiPointInRect(checkRect, fx, fy)) { part = RowPart::CHECK; } else if (badgeVisible && UiPointInRect(SignalBadgeRect(rowIndex), fx, fy)) { part = RowPart::LANE_BADGE; } else { part = RowPart::BODY; } } return rowIndex; } accumulated += row.height; } return -1; } void SignalPanel::ScrollToSignal(const SignalRef& ref) { EnsureRows(); if (!ref.IsValid()) { return; } float top = 0.0f; for (std::size_t index = 0; index < m_rows.size(); ++index) { const ListRow& row = m_rows[index]; if (row.kind == RowKind::SIGNAL && row.fileIndex == ref.fileIndex && row.signalIndex == ref.signalIndex) { if (top < m_scrollOffset) { m_scrollOffset = top; } else if (top + row.height > m_scrollOffset + m_listRect.Height) { m_scrollOffset = top + row.height - m_listRect.Height; } ClampScroll(); return; } top += row.height; } } void SignalPanel::LayoutStyleControls() { const float labelWidth = kStyleLabelWidth * m_scale; const float contentLeft = m_styleRect.X + kStylePadH * m_scale + labelWidth; const float contentWidth = std::max(60.0f, m_styleRect.Width - kStylePadH * 2.0f * m_scale - labelWidth); const float rowHeight = kStyleRowHeight * m_scale; const float row0Top = m_styleRect.Y + kStylePadV * m_scale; // ---- 第一行:颜色(预设色块 + 同尺寸自定义颜色入口)---- int paletteCount = 0; SignalPalette(paletteCount); float swatchSize = kSwatchEdge * m_scale; const float swatchGap = kSwatchGap * m_scale; int count = std::min(paletteCount, static_cast(m_colorRects.size())); // 为自定义颜色入口预留一个同尺寸格位,优先整体缩小色块以保留全部预设颜色。 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 presetWidth = kWidthPresetWidth * m_scale; const float spinButton = kWidthSpinButton * m_scale; const float valueWidth = kWidthValueWidth * m_scale; float groupGap = 8.0f * m_scale; float spinnerWidth = spinButton * 2.0f + valueWidth; float groupWidth = presetWidth * 2.0f + spinnerWidth + groupGap * 2.0f; if (groupWidth > contentWidth) { 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 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, 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, rowHeight); } // ---- 第四行:不透明度 ---- 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) { m_area = area; m_headerRect = gd::RectF(area.X, area.Y, area.Width, kHeaderHeight * m_scale); const float styleHeight = DesiredStyleHeight() * m_scale; m_styleRect = gd::RectF(area.X, area.Y + area.Height - styleHeight, area.Width, styleHeight); m_listRect = gd::RectF(area.X, m_headerRect.Y + m_headerRect.Height, area.Width, std::max(0.0f, m_styleRect.Y - m_headerRect.Y - m_headerRect.Height)); // 标题区右侧的“全部显示/隐藏”按钮。 const float toggleWidth = 68.0f * m_scale; const float toggleHeight = 22.0f * m_scale; m_toggleAllRect = gd::RectF(area.X + area.Width - kPadding * m_scale - toggleWidth, m_headerRect.Y + (m_headerRect.Height - toggleHeight) * 0.5f, toggleWidth, toggleHeight); // 依据内容决定是否显示滚动条,并据此收缩列表可用宽度。 m_listFullWidth = m_listRect.Width; EnsureRows(); RefreshScrollBar(); LayoutStyleControls(); } void SignalPanel::Paint(gd::Graphics& g) { if (m_theme == nullptr || m_fonts == nullptr) { return; } EnsureRows(); UiFillRect(g, m_area, m_theme->panelBg); // 标题区。 { std::wstring title = L"信号"; if (m_dataSet != nullptr && m_tab != nullptr) { title += L" · " + std::to_wstring(m_tab->VisibleCount()) + L"/" + std::to_wstring(TotalSignalCount()) + L" 已选"; } gd::RectF titleRect(m_headerRect.X + kPadding * m_scale, m_headerRect.Y, std::max(20.0f, m_headerRect.Width - kPadding * 2.0f * m_scale - m_toggleAllRect.Width - 8.0f * m_scale), m_headerRect.Height); UiText(g, UiEllipsize(g, title, m_fonts->uiBold, titleRect.Width), m_fonts->uiBold, titleRect, m_theme->textStrong, TextAlign::LEFT); const bool allVisible = AreAllSignalsVisible(); const std::wstring toggleLabel = allVisible ? L"全部隐藏" : L"全部显示"; const bool toggleHover = (m_hoverStyle == StyleHit::TOGGLE_ALL); if (toggleHover) { UiFillRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->hover); } UiDrawRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->border, 1.0f); const bool enabled = (m_dataSet != nullptr && !m_dataSet->Empty()); UiText(g, toggleLabel, m_fonts->uiSmall, m_toggleAllRect, enabled ? m_theme->text : m_theme->textMuted, TextAlign::CENTER); } // 列表区。 { const int saveState = g.Save(); g.SetClip(gd::RectF(m_listRect.X, m_listRect.Y, m_listRect.Width, m_listRect.Height)); if (m_dataSet == nullptr || m_dataSet->Empty()) { UiText(g, L"尚未加载数据,请打开 CSV 文件", m_fonts->uiSmall, m_listRect, m_theme->textMuted, TextAlign::CENTER); } else { const int rowCount = static_cast(m_rows.size()); for (int row = 0; row < rowCount; ++row) { const gd::RectF rect = RowRect(row); if (rect.Y + rect.Height < m_listRect.Y || rect.Y > m_listRect.Y + m_listRect.Height) { continue; } if (m_rows[static_cast(row)].kind == RowKind::FILE) { PaintFileRow(g, row); } else { PaintSignalRow(g, row); } } } g.Restore(saveState); } PaintScrollBar(g); PaintStyleSection(g); // 分隔线。 UiDrawLine(g, m_area.X + m_area.Width - 1.0f, m_area.Y, m_area.X + m_area.Width - 1.0f, m_area.Y + m_area.Height, m_theme->border, 1.0f); UiDrawLine(g, m_headerRect.X, m_headerRect.Y + m_headerRect.Height - 1.0f, m_headerRect.X + m_headerRect.Width, m_headerRect.Y + m_headerRect.Height - 1.0f, m_theme->border, 1.0f); if (m_styleRect.Height > 0.0f) { UiDrawLine(g, m_styleRect.X, m_styleRect.Y, m_styleRect.X + m_styleRect.Width, m_styleRect.Y, m_theme->border, 1.0f); } } void SignalPanel::PaintFileRow(gd::Graphics& g, int row) { if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast(m_rows.size())) { return; } const ListRow& item = m_rows[static_cast(row)]; const DataFile* file = m_dataSet->FileAt(item.fileIndex); if (file == nullptr) { return; } const gd::RectF rect = RowRect(row); const bool hovered = (m_hoverRow == row); if (hovered) { UiFillRect(g, rect, m_theme->hover); } UiFillRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height), WithAlpha(m_theme->borderStrong, 0x90)); const float baseX = rect.X + kPadding * m_scale; const float centerY = rect.Y + rect.Height * 0.5f; const bool collapsed = m_tab->IsCollapsed(item.fileIndex); // 折叠三角。 { const float cx = baseX + kDisclosureWidth * m_scale * 0.5f; const std::uint32_t color = (hovered && m_hoverPart == RowPart::DISCLOSURE) ? m_theme->accent : m_theme->text; UiDrawTriangleIcon(g, gd::RectF(cx - 6.0f * m_scale, centerY - 6.0f * m_scale, 12.0f * m_scale, 12.0f * m_scale), color, collapsed ? IconDirection::RIGHT : IconDirection::DOWN); } // 分组复选框(三态:全选 / 部分 / 未选)。 const gd::RectF checkRect(baseX + kDisclosureWidth * m_scale, centerY - kCheckSize * m_scale * 0.5f, kCheckSize * m_scale, kCheckSize * m_scale); const bool allVisible = m_tab->FileAllVisible(*m_dataSet, item.fileIndex); const bool anyVisible = m_tab->FileHasVisible(*m_dataSet, item.fileIndex); UiDrawCheckBox(g, checkRect, allVisible ? 1 : (anyVisible ? 2 : 0), 3.0f * m_scale); const gd::RectF closeRect = FileCloseRect(row); // 文件名称与统计。 const float nameLeft = checkRect.X + checkRect.Width + 8.0f * m_scale; const std::wstring stats = std::to_wstring(file->signals.size()) + L" 项"; const float statsWidth = UiMeasureTextWidth(m_fonts->uiSmall, stats); const float statsRight = closeRect.X - 6.0f * m_scale; const float nameWidth = std::max(20.0f, statsRight - statsWidth - 6.0f * m_scale - nameLeft); UiText(g, UiEllipsize(g, file->fileName, m_fonts->uiBold, nameWidth), m_fonts->uiBold, gd::RectF(nameLeft, 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), m_theme->textMuted, TextAlign::RIGHT); // 关闭按钮(从工作区移除该文件)。 const bool closeHot = hovered && (m_hoverPart == RowPart::CLOSE); if (closeHot) { UiFillRoundRect(g, closeRect, 3.0f * m_scale, m_theme->pressed); } UiDrawCrossIcon(g, closeRect, closeHot ? m_theme->danger : m_theme->textMuted, 1.5f * m_scale); } void SignalPanel::PaintSignalRow(gd::Graphics& g, int row) { if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast(m_rows.size())) { return; } const ListRow& item = m_rows[static_cast(row)]; const SignalRef ref{item.fileIndex, item.signalIndex}; const SignalData* signal = m_dataSet->Resolve(ref); const SignalStyle* style = m_tab->Style(ref); if (signal == nullptr || style == nullptr) { return; } const gd::RectF rect = RowRect(row); const bool selected = (m_tab->Selected() == ref); const bool hovered = (m_hoverRow == row); if (selected) { UiFillRect(g, rect, m_theme->selection); UiFillRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height), m_theme->accent); } else if (hovered) { UiFillRect(g, rect, m_theme->hover); } const float baseX = rect.X + kPadding * m_scale + kIndent * m_scale; const float centerY = rect.Y + rect.Height * 0.5f; // 复选框。 const gd::RectF checkRect(baseX + kDisclosureWidth * m_scale, centerY - kCheckSize * m_scale * 0.5f, kCheckSize * m_scale, kCheckSize * m_scale); UiDrawCheckBox(g, checkRect, style->visible ? 1 : 0, 3.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 ? swatchColor : WithAlpha(style->color, 0x50)); // 网格徽标(可点击,用于调整所属网格)。 const int laneIndex = m_tab->LaneOfSignal(ref); const bool badgeVisible = (laneIndex >= 0) && style->visible; const gd::RectF badgeRect = SignalBadgeRect(row); float nameRightLimit = rect.X + rect.Width - kPadding * m_scale; if (badgeVisible) { nameRightLimit = badgeRect.X - 6.0f * m_scale; } // 名称:完全保留文件内的列名(含 "[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 (badgeVisible) { const bool badgeHot = hovered && (m_hoverPart == RowPart::LANE_BADGE); UiFillRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f, badgeHot ? m_theme->accentSoft : m_theme->cardBg); UiDrawRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f, badgeHot ? m_theme->accent : m_theme->border, 1.0f); UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect, badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER); } } gd::RectF SignalPanel::ScrollThumbRect() const { if (!m_scrollBarVisible || m_contentHeight <= m_listRect.Height || m_listRect.Height <= 0.0f) { return gd::RectF(); } const float ratio = std::min(1.0f, m_listRect.Height / m_contentHeight); const float thumbHeight = std::max(24.0f * m_scale, m_scrollBarRect.Height * ratio); const float maxScroll = m_contentHeight - m_listRect.Height; const float progress = (maxScroll > 0.0f) ? (m_scrollOffset / maxScroll) : 0.0f; const float thumbY = m_scrollBarRect.Y + progress * (m_scrollBarRect.Height - thumbHeight); return gd::RectF(m_scrollBarRect.X, thumbY, m_scrollBarRect.Width, thumbHeight); } void SignalPanel::PaintScrollBar(gd::Graphics& g) { if (!m_scrollBarVisible || m_contentHeight <= m_listRect.Height) { return; } UiFillRoundRect(g, m_scrollBarRect, kScrollBarThickness * m_scale * 0.5f, m_theme->gridWeak); UiFillRoundRect(g, ScrollThumbRect(), kScrollBarThickness * m_scale * 0.5f, m_theme->borderStrong); } void SignalPanel::PaintStyleSection(gd::Graphics& g) { if (m_styleRect.Height <= 0.0f) { return; } UiFillRect(g, m_styleRect, m_theme->panelBg); const SignalStyle* style = (m_tab == nullptr) ? nullptr : m_tab->Style(m_tab->Selected()); if (style == nullptr || m_tab->Selected().IsValid() == false) { UiText(g, L"请选择信号以编辑样式", m_fonts->uiSmall, m_styleRect, m_theme->textMuted, TextAlign::CENTER); return; } const float labelX = m_styleRect.X + kStylePadH * m_scale; const float labelWidth = kStyleLabelWidth * m_scale; const float rowHeight = kStyleRowHeight * 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, kCorner * m_scale, color); if (active || hovered) { const gd::RectF ring(rect.X - 2.0f * m_scale, rect.Y - 2.0f * m_scale, rect.Width + 4.0f * m_scale, rect.Height + 4.0f * m_scale); UiDrawRoundRect(g, ring, 5.0f * m_scale, active ? m_theme->textStrong : m_theme->borderStrong, active ? 1.6f : 1.0f); } } // 自定义颜色入口:与预设色块完全同尺寸、同外框样式的小方格,仅多一个加号图标。 const bool chipHot = (m_hoverStyle == StyleHit::CUSTOM_CHIP); 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, rowHeight), m_theme->textMuted, TextAlign::LEFT); const gd::RectF presetRects[2] = {m_widthThinRect, m_widthThickRect}; const float presetWidths[2] = {kWidthThinPreset, kWidthThickPreset}; for (int index = 0; index < 2; ++index) { 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); UiFillRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg)); UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f); 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_widthEditing ? m_theme->accent : (valueHot ? m_theme->borderStrong : m_theme->border), m_widthEditing ? 1.6f : 1.0f); 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); const bool minusEnabled = style->lineWidth > (kWidthMin + 1.0e-4f); const bool plusEnabled = style->lineWidth < (kWidthMax - 1.0e-4f); if (minusHot && minusEnabled) { UiFillRoundRect(g, m_widthMinusRect, kCorner * m_scale, m_theme->hover); } if (plusHot && plusEnabled) { UiFillRoundRect(g, m_widthPlusRect, kCorner * m_scale, m_theme->hover); } 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); } // ---- 第三行:样式 ---- { 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) { const gd::RectF& rect = m_modeRects[static_cast(index)]; const bool active = (style->mode == kModeOptions[index]); const bool hovered = (m_hoverStyle == StyleHit::MODE && m_hoverStyleIndex == index); UiFillRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg)); UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f); UiText(g, DrawModeName(kModeOptions[index]), m_fonts->uiSmall, rect, active ? m_theme->textStrong : m_theme->text, TextAlign::CENTER); } } // ---- 第四行:不透明度 ---- { 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); } } bool SignalPanel::AreAllSignalsVisible() const { if (m_dataSet == nullptr || m_tab == nullptr || m_dataSet->Empty()) { return false; } return m_tab->VisibleCount() == TotalSignalCount(); } bool SignalPanel::StepLineWidth(float delta) { if (m_tab == nullptr) { return false; } SignalStyle* style = m_tab->Style(m_tab->Selected()); if (style == nullptr) { return false; } float value = ClampFloat(style->lineWidth + delta, kWidthMin, kWidthMax); value = std::round(value * 10.0f) / 10.0f; if (std::fabs(value - style->lineWidth) < 0.001f) { return false; } style->lineWidth = value; return true; } bool SignalPanel::BeginWidthEdit() { if (m_tab == nullptr) { return false; } 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::EndWidthEdit(bool commit) { if (!m_widthEditing) { return false; } bool changed = true; if (commit && m_tab != nullptr) { const std::wstring trimmed = Trim(m_widthEditText); 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(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(); const float travel = static_cast(y - m_scrollGrabOffset) - m_scrollBarRect.Y; const float usable = m_scrollBarRect.Height - thumb.Height; if (usable > 0.0f) { const float progress = std::max(0.0f, std::min(1.0f, travel / usable)); m_scrollOffset = progress * (m_contentHeight - m_listRect.Height); ClampScroll(); } return true; } RowPart part = RowPart::NONE; const int hoverRow = HitRow(x, y, part); StyleHit hoverStyle = StyleHit::NONE; int hoverStyleIndex = -1; const float fx = static_cast(x); const float fy = static_cast(y); if (UiPointInRect(m_toggleAllRect, fx, fy)) { hoverStyle = StyleHit::TOGGLE_ALL; } else if (m_tab != nullptr && m_tab->Style(m_tab->Selected()) != nullptr) { for (int index = 0; index < m_colorCount; ++index) { if (UiPointInRect(m_colorRects[static_cast(index)], fx, fy)) { hoverStyle = StyleHit::COLOR; hoverStyleIndex = index; break; } } if (hoverStyle == StyleHit::NONE && UiPointInRect(m_customChipRect, fx, fy)) { hoverStyle = StyleHit::CUSTOM_CHIP; } if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthThinRect, fx, fy)) { hoverStyle = StyleHit::WIDTH_PRESET; hoverStyleIndex = 0; } if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthThickRect, fx, fy)) { hoverStyle = StyleHit::WIDTH_PRESET; hoverStyleIndex = 1; } if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthMinusRect, fx, fy)) { hoverStyle = StyleHit::WIDTH_MINUS; } if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthPlusRect, fx, fy)) { 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) { if (UiPointInRect(m_modeRects[static_cast(index)], fx, fy)) { hoverStyle = StyleHit::MODE; hoverStyleIndex = index; break; } } } 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 && part == m_hoverPart) { return false; } m_hoverRow = hoverRow; m_hoverPart = part; m_hoverStyle = hoverStyle; m_hoverStyleIndex = hoverStyleIndex; return true; } bool SignalPanel::OnLButtonDown(int x, int y) { if (m_dataSet == nullptr || m_tab == nullptr) { return false; } const float fx = static_cast(x); const float fy = static_cast(y); // 滚动条拖拽优先于其它命中测试。 if (m_scrollBarVisible && UiPointInRect(m_scrollBarRect, fx, fy)) { const gd::RectF thumb = ScrollThumbRect(); m_draggingScrollBar = true; if (UiPointInRect(thumb, fx, fy)) { m_scrollGrabOffset = y - static_cast(thumb.Y); } 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; if (usable > 0.0f) { const float progress = std::max(0.0f, std::min(1.0f, travel / usable)); m_scrollOffset = progress * (m_contentHeight - m_listRect.Height); ClampScroll(); } } return true; } m_pendingLaneMenu = false; m_pendingColorPicker = false; // 点击样式区以外的位置时结束线宽输入。 if (m_widthEditing && !UiPointInRect(m_widthValueRect, fx, fy)) { EndWidthEdit(true); } // 全部显示/隐藏。 if (UiPointInRect(m_toggleAllRect, fx, fy)) { m_tab->SetAllVisible(*m_dataSet, !AreAllSignalsVisible()); m_rowsDirty = true; return true; } // 样式编辑区。 if (SignalStyle* style = m_tab->Style(m_tab->Selected())) { int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); for (int index = 0; index < m_colorCount; ++index) { if (UiPointInRect(m_colorRects[static_cast(index)], fx, fy)) { style->color = palette[index % paletteCount]; return true; } } if (UiPointInRect(m_customChipRect, fx, fy)) { // 在左键抬起时展开,避免按下即被后续消息立即收起。 m_pendingColorPicker = true; return true; } if (UiPointInRect(m_widthThinRect, fx, fy)) { style->lineWidth = kWidthThinPreset; return true; } if (UiPointInRect(m_widthThickRect, fx, fy)) { style->lineWidth = kWidthThickPreset; return true; } if (UiPointInRect(m_widthMinusRect, fx, fy)) { return StepLineWidth(-kWidthStep); } if (UiPointInRect(m_widthPlusRect, fx, fy)) { 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)) { style->mode = kModeOptions[index]; return true; } } if (UiPointInRect(SliderHitRect(m_opacityTrackRect, m_scale), fx, fy)) { m_draggingOpacity = true; return ApplyOpacityFromX(fx) || true; } } // 列表行。 RowPart part = RowPart::NONE; const int row = HitRow(x, y, part); if (row < 0) { return false; } const ListRow& item = m_rows[static_cast(row)]; if (item.kind == RowKind::FILE) { 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; return true; } if (part == RowPart::CLOSE) { // 关闭文件属于数据层操作,交由宿主窗口处理。 if (m_host != nullptr) { PostMessageW(m_host, WM_APP + 1, static_cast(item.fileIndex), 0); } return true; } m_tab->ToggleCollapsed(item.fileIndex); m_rowsDirty = true; return true; } const SignalRef ref{item.fileIndex, item.signalIndex}; m_tab->SetSelected(ref); if (part == RowPart::CHECK) { // 勾选/取消勾选会立即触发网格重新划分(取消后空网格随之消失)。 m_tab->ToggleVisible(*m_dataSet, ref); m_rowsDirty = true; } else if (part == RowPart::LANE_BADGE) { // 在左键抬起时弹出,保证菜单不会随着按键松开而消失。 m_pendingLaneMenu = true; m_pendingLaneRef = ref; } return true; } bool SignalPanel::OnLButtonUp(int x, int y) { bool changed = false; if (m_draggingOpacity) { m_draggingOpacity = false; changed = true; } if (m_draggingScrollBar) { m_draggingScrollBar = false; changed = true; } 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) { if (m_dataSet == nullptr || m_tab == nullptr) { 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) { return false; } const ListRow& item = m_rows[static_cast(row)]; if (item.kind == RowKind::FILE) { m_tab->ToggleCollapsed(item.fileIndex); m_rowsDirty = true; 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); m_rowsDirty = true; return true; } bool SignalPanel::OnRButtonDown(int x, int y) { if (m_dataSet == nullptr || m_tab == nullptr) { return false; } RowPart part = RowPart::NONE; const int row = HitRow(x, y, part); if (row < 0) { return false; } const ListRow& item = m_rows[static_cast(row)]; if (item.kind == RowKind::FILE) { OpenFileMenu(item.fileIndex, x, y); return true; } const SignalRef ref{item.fileIndex, item.signalIndex}; m_tab->SetSelected(ref); OpenSignalMenu(ref, x, y); return true; } bool SignalPanel::OnMouseWheel(int delta) { if (m_dataSet == nullptr) { return false; } const float previous = m_scrollOffset; m_scrollOffset -= static_cast(delta) / 120.0f * 54.0f * m_scale; ClampScroll(); return std::fabs(previous - m_scrollOffset) > 0.01f; } bool SignalPanel::OnMouseLeave() { if (m_hoverRow < 0 && m_hoverStyle == StyleHit::NONE) { return false; } m_hoverRow = -1; m_hoverPart = RowPart::NONE; m_hoverStyle = StyleHit::NONE; m_hoverStyleIndex = -1; return true; } void SignalPanel::OpenSignalMenu(const SignalRef& ref, int x, int y) { if (m_menu == nullptr || m_tab == nullptr || m_dataSet == nullptr) { return; } const SignalStyle* style = m_tab->Style(ref); const SignalData* signal = m_dataSet->Resolve(ref); if (style == nullptr || signal == nullptr) { return; } m_menuTargetSignal = ref; m_menuTargetFile = -1; std::vector items; MenuItem header; header.kind = MenuItemKind::HEADER; header.label = signal->name; items.push_back(header); MenuItem toggle; toggle.label = style->visible ? L"隐藏该信号" : L"显示该信号"; toggle.id = CMD_SIGNAL_TOGGLE; items.push_back(toggle); MenuItem solo; solo.label = L"仅显示该信号"; solo.id = CMD_SIGNAL_SOLO; items.push_back(solo); MenuItem showAll; showAll.label = L"显示全部信号"; showAll.id = CMD_SIGNAL_SHOW_ALL; items.push_back(showAll); MenuItem hideAll; hideAll.label = L"隐藏全部信号"; hideAll.id = CMD_SIGNAL_HIDE_ALL; items.push_back(hideAll); MenuItem separator; separator.kind = MenuItemKind::SEPARATOR; items.push_back(separator); int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); MenuItem colors; colors.kind = MenuItemKind::SWATCHES; colors.id = CMD_COLOR_BASE; colors.swatchCount = std::min(paletteCount, 16); colors.checkedSwatch = -1; for (int index = 0; index < colors.swatchCount; ++index) { colors.swatches[index] = palette[index]; if (style->color == palette[index]) { colors.checkedSwatch = index; } } items.push_back(colors); MenuItem modeHeader; modeHeader.kind = MenuItemKind::HEADER; modeHeader.label = L"样式"; items.push_back(modeHeader); for (int index = 0; index < kModeOptionCount; ++index) { MenuItem mode; mode.label = DrawModeName(kModeOptions[index]); mode.id = CMD_MODE_BASE + index; mode.checked = (style->mode == kModeOptions[index]); items.push_back(mode); } const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area; m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items); } void SignalPanel::OpenFileMenu(int fileIndex, int x, int y) { if (m_menu == nullptr || m_tab == nullptr || m_dataSet == nullptr) { return; } const DataFile* file = m_dataSet->FileAt(fileIndex); if (file == nullptr) { return; } m_menuTargetFile = fileIndex; m_menuTargetSignal = SignalRef(); std::vector items; MenuItem header; header.kind = MenuItemKind::HEADER; header.label = file->fileName; items.push_back(header); MenuItem showAll; showAll.label = L"选中该文件全部信号"; showAll.id = CMD_FILE_SHOW_ALL; items.push_back(showAll); MenuItem hideAll; hideAll.label = L"取消该文件全部信号"; hideAll.id = CMD_FILE_HIDE_ALL; items.push_back(hideAll); MenuItem separator; separator.kind = MenuItemKind::SEPARATOR; items.push_back(separator); MenuItem remove; remove.label = L"关闭该文件(从工作区移除)"; remove.id = CMD_FILE_REMOVE; items.push_back(remove); const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area; m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items); } void SignalPanel::OpenLaneMenu(const SignalRef& ref, int x, int y) { if (m_menu == nullptr || m_tab == nullptr || m_dataSet == nullptr) { return; } const SignalData* signal = m_dataSet->Resolve(ref); if (signal == nullptr) { return; } m_menuTargetSignal = ref; m_menuTargetFile = -1; std::vector items; MenuItem header; header.kind = MenuItemKind::HEADER; header.label = L"所属网格"; items.push_back(header); const std::vector& lanes = m_tab->Lanes(); const int currentLane = m_tab->LaneOfSignal(ref); for (std::size_t index = 0; index < lanes.size(); ++index) { MenuItem lane; lane.label = L"网格 " + std::to_wstring(index + 1) + L"(" + std::to_wstring(lanes[index].signals.size()) + L" 条信号)"; lane.id = CMD_LANE_BASE + static_cast(index); lane.checked = (currentLane == static_cast(index)); items.push_back(lane); } MenuItem separator; separator.kind = MenuItemKind::SEPARATOR; items.push_back(separator); MenuItem create; create.label = L"新建网格"; create.id = CMD_LANE_NEW; items.push_back(create); const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area; m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items); } bool SignalPanel::OnMenuCommand(const MenuResult& result) { if (m_tab == nullptr || m_dataSet == nullptr || !result.valid) { return false; } bool needRelayout = false; switch (result.id) { case CMD_SIGNAL_TOGGLE: if (m_menuTargetSignal.IsValid()) { m_tab->ToggleVisible(*m_dataSet, m_menuTargetSignal); m_rowsDirty = true; needRelayout = true; } break; case CMD_SIGNAL_SOLO: if (m_menuTargetSignal.IsValid()) { m_tab->SoloSignal(*m_dataSet, m_menuTargetSignal); m_rowsDirty = true; needRelayout = true; } break; case CMD_SIGNAL_SHOW_ALL: m_tab->SetAllVisible(*m_dataSet, true); m_rowsDirty = true; needRelayout = true; break; case CMD_SIGNAL_HIDE_ALL: m_tab->SetAllVisible(*m_dataSet, false); m_rowsDirty = true; needRelayout = true; break; case CMD_FILE_SHOW_ALL: if (m_menuTargetFile >= 0) { m_tab->SetFileVisible(*m_dataSet, m_menuTargetFile, true); m_rowsDirty = true; needRelayout = true; } break; case CMD_FILE_HIDE_ALL: if (m_menuTargetFile >= 0) { m_tab->SetFileVisible(*m_dataSet, m_menuTargetFile, false); m_rowsDirty = true; needRelayout = true; } break; case CMD_FILE_REMOVE: if (m_menuTargetFile >= 0 && m_host != nullptr) { PostMessageW(m_host, WM_APP + 1, static_cast(m_menuTargetFile), 0); } break; case CMD_LANE_NEW: if (m_menuTargetSignal.IsValid()) { m_tab->MoveSignalToLane(*m_dataSet, m_menuTargetSignal, -1); m_rowsDirty = true; needRelayout = true; } break; default: if (result.id >= CMD_LANE_BASE && result.id < CMD_LANE_NEW) { if (m_menuTargetSignal.IsValid()) { m_tab->MoveSignalToLane(*m_dataSet, m_menuTargetSignal, result.id - CMD_LANE_BASE); m_rowsDirty = true; needRelayout = true; } } else if (result.id == CMD_COLOR_BASE && result.swatchIndex >= 0) { if (SignalStyle* style = m_tab->Style(m_menuTargetSignal)) { int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); style->color = palette[result.swatchIndex % std::max(1, paletteCount)]; } } else if (result.id >= CMD_MODE_BASE && result.id < CMD_MODE_BASE + kModeOptionCount) { if (SignalStyle* style = m_tab->Style(m_menuTargetSignal)) { style->mode = kModeOptions[result.id - CMD_MODE_BASE]; } } break; } return needRelayout; } } // namespace jdi