Files
Just-DataInspector/Components/SignalPanel/Src/SignalPanel.cpp
T
DeepSeek V4.1 Flash 9c6c374cce feat: 支持多视图标签页、按文件分组的多文件信号管理与网格随勾选自动收敛
数据层与视图配置解耦:新增 DataSet(多文件只读数据层)与 TabConfig(每标签页独立配置),
以 SignalRef{fileIndex,signalIndex} 标识全局信号,替换原有单文件 SignalSet。

- 标签页:新增 TabBar 组件,支持新建/切换/关闭标签页;每页独立保存信号勾选、
  颜色/线宽/显示方式、网格划分、视图范围与时间光标,互不影响。
- 网格收敛:可见性变更统一经 TabConfig 的 Set*Visible 接口并同步重算网格,
  取消勾选后对应的空网格立即回收(修复此前留下空轨道的问题)。
- 多文件分组:左侧面板改为「文件分组 + 信号」树形列表,分组可折叠、可整组勾选,
  分组复选框支持全选/部分/未选三态;新加载文件的信号默认未勾选,由用户自行选择。
- 来源标识:信号行、样式编辑区标题、波形图例与光标读数在跨文件时标注来源文件名。
- 交互补充:面板滚动条支持点击跳转与拖拽,切换标签页自动滚动到已选信号。
- 工具栏精简为「打开 CSV/适应视图/放大/缩小/合并网格/每信号一格/全选信号/清空勾选」,
  按钮宽度改为按实际文本像素测量;打开对话框支持多选文件,拖放支持批量载入。
- 新增 TestData/[2].CSV 作为跨文件对比测试数据。
2026-09-29 15:48:04 +08:00

1353 lines
43 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "SignalPanel.hpp"
#include <algorithm>
#include <cmath>
#include "Utf8Util.hpp"
namespace jdi
{
namespace
{
const float kHeaderHeight = 38.0f; /**< 标题区高度(逻辑像素)。 */
const float kFileRowHeight = 30.0f; /**< 文件分组行高。 */
const float kSignalRowHeight = 27.0f; /**< 信号行高。 */
const float kStyleHeight = 186.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 kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */
/** @brief 上下文菜单命令 ID 基准值。 */
const int kMenuToggleVisible = 1;
const int kMenuSolo = 2;
const int kMenuShowAll = 3;
const int kMenuHideAll = 4;
const int kMenuFileShowAll = 10;
const int kMenuFileHideAll = 11;
const int kMenuFileRemove = 12;
const int kMenuModeBase = 100;
const int kMenuWidthBase = 200;
const int kMenuColorBase = 300;
const int kMenuLaneBase = 400;
const int kMenuLaneNew = 500;
/** @brief 可选线宽档位。 */
const float kWidthOptions[] = {1.0f, 1.5f, 2.0f, 3.0f};
/** @brief 可选绘制方式顺序。 */
const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER,
DrawMode::STEP};
}
SignalPanel::SignalPanel()
{
m_colorRects.fill(gd::RectF());
m_widthRects.fill(gd::RectF());
m_modeRects.fill(gd::RectF());
}
void SignalPanel::Setup(HWND host, const UiFonts* fonts, const Theme* theme)
{
m_host = host;
m_fonts = fonts;
m_theme = theme;
}
void SignalPanel::SetContext(const DataSet* dataSet, TabConfig* tab)
{
m_dataSet = dataSet;
m_tab = tab;
m_rowsDirty = true;
m_scrollOffset = 0.0f;
}
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<float>(x), static_cast<float>(y));
}
int SignalPanel::TotalSignalCount() const
{
if (m_dataSet == nullptr)
{
return 0;
}
return static_cast<int>(m_dataSet->SignalCount());
}
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<DataFile>& files = m_dataSet->Files();
for (std::size_t fileIndex = 0; fileIndex < files.size(); ++fileIndex)
{
ListRow fileRow;
fileRow.kind = RowKind::FILE;
fileRow.fileIndex = static_cast<int>(fileIndex);
fileRow.height = fileHeight;
m_rows.push_back(fileRow);
if (m_tab->IsCollapsed(static_cast<int>(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<int>(fileIndex);
signalRow.signalIndex = static_cast<int>(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<int>(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<std::size_t>(index)].height;
}
return gd::RectF(m_listRect.X, top, m_listRect.Width, m_rows[static_cast<std::size_t>(row)].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<float>(x);
const float fy = static_cast<float>(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 gd::RectF rect = RowRect(static_cast<int>(index));
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 && UiPointInRect(disclosureRect, fx, fy))
{
part = RowPart::DISCLOSURE;
}
else if (UiPointInRect(checkRect, fx, fy))
{
part = RowPart::CHECK;
}
else
{
part = RowPart::BODY;
}
return static_cast<int>(index);
}
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::Layout(const gd::RectF& area)
{
m_area = area;
m_headerRect = gd::RectF(area.X, area.Y, area.Width, kHeaderHeight * m_scale);
const float styleHeight = kStyleHeight * 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();
// 样式编辑区布局。
const float labelWidth = kLineLabelWidth * m_scale;
const float contentWidth = m_styleRect.Width - kPadding * 2.0f * m_scale - labelWidth;
const float contentLeft = m_styleRect.X + kPadding * m_scale + labelWidth;
int paletteCount = 0;
SignalPalette(paletteCount);
m_colorCount = std::min(paletteCount, static_cast<int>(m_colorRects.size()));
// 色块尺寸按可用宽度自适应,避免超出面板。
const float colorGap = 4.0f * m_scale;
float swatchSize = 20.0f * m_scale;
if (m_colorCount > 1)
{
const float fitted = (contentWidth - colorGap * static_cast<float>(m_colorCount - 1)) /
static_cast<float>(m_colorCount);
if (fitted < swatchSize)
{
swatchSize = std::max(10.0f * m_scale, fitted);
}
}
const float colorRowTop = m_styleRect.Y + 30.0f * m_scale;
for (int index = 0; index < m_colorCount; ++index)
{
const float x = contentLeft + static_cast<float>(index) * (swatchSize + colorGap);
m_colorRects[static_cast<std::size_t>(index)] =
gd::RectF(x, colorRowTop, swatchSize, swatchSize);
}
const float widthRowTop = colorRowTop + swatchSize + 12.0f * m_scale;
const float widthGap = 6.0f * m_scale;
const float widthButtonWidth = std::min(56.0f * m_scale, (contentWidth - widthGap * 3.0f) / 4.0f);
for (int index = 0; index < 4; ++index)
{
const float x = contentLeft + static_cast<float>(index) * (widthButtonWidth + widthGap);
m_widthRects[static_cast<std::size_t>(index)] =
gd::RectF(x, widthRowTop, widthButtonWidth, 26.0f * m_scale);
}
const float modeRowTop = widthRowTop + 36.0f * m_scale;
const float modeGap = 6.0f * m_scale;
const float modeButtonWidth = (contentWidth - modeGap * 3.0f) / 4.0f;
for (int index = 0; index < 4; ++index)
{
const float x = contentLeft + static_cast<float>(index) * (modeButtonWidth + modeGap);
m_modeRects[static_cast<std::size_t>(index)] =
gd::RectF(x, modeRowTop, modeButtonWidth, 26.0f * m_scale);
}
const float laneRowTop = modeRowTop + 40.0f * m_scale;
m_laneButtonRect =
gd::RectF(contentLeft, laneRowTop, std::min(contentWidth, 150.0f * m_scale), 26.0f * m_scale);
}
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,
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<int>(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<std::size_t>(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<int>(m_rows.size()))
{
return;
}
const ListRow& item = m_rows[static_cast<std::size_t>(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 float arm = 3.0f * m_scale;
const std::uint32_t color =
(hovered && m_hoverPart == RowPart::DISCLOSURE) ? m_theme->accent : m_theme->text;
if (collapsed)
{
UiDrawLine(g, cx - arm * 0.5f, centerY - arm, cx + arm * 0.7f, centerY, color, 1.5f * m_scale);
UiDrawLine(g, cx + arm * 0.7f, centerY, cx - arm * 0.5f, centerY + arm, color, 1.5f * m_scale);
}
else
{
UiDrawLine(g, cx - arm, centerY - arm * 0.5f, cx, centerY + arm * 0.7f, color, 1.5f * m_scale);
UiDrawLine(g, cx, centerY + arm * 0.7f, cx + arm, centerY - arm * 0.5f, color, 1.5f * m_scale);
}
}
// 分组复选框(三态:全选 / 部分 / 未选)。
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);
if (allVisible)
{
UiFillRoundRect(g, checkRect, 3.0f * m_scale, m_theme->accent);
gd::Pen pen(ToGdiColor(m_theme->textOnAccent), 1.6f * m_scale);
pen.SetStartCap(gd::LineCapRound);
pen.SetEndCap(gd::LineCapRound);
g.DrawLine(&pen, checkRect.X + 3.0f * m_scale, centerY, checkRect.X + 6.0f * m_scale,
checkRect.Y + checkRect.Height - 4.0f * m_scale);
g.DrawLine(&pen, checkRect.X + 6.0f * m_scale, checkRect.Y + checkRect.Height - 4.0f * m_scale,
checkRect.X + checkRect.Width - 3.0f * m_scale, checkRect.Y + 4.0f * m_scale);
}
else if (anyVisible)
{
UiFillRoundRect(g, checkRect, 3.0f * m_scale, m_theme->accentSoft);
UiDrawRoundRect(g, checkRect, 3.0f * m_scale, m_theme->accent, 1.0f);
UiFillRect(g, gd::RectF(checkRect.X + 3.0f * m_scale, centerY - 1.0f * m_scale,
checkRect.Width - 6.0f * m_scale, 2.0f * m_scale),
m_theme->accent);
}
else
{
UiFillRoundRect(g, checkRect, 3.0f * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, checkRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
}
// 文件名称与统计。
const float nameLeft = checkRect.X + checkRect.Width + 8.0f * m_scale;
const std::wstring stats = L" " + std::to_wstring(file->signals.size()) + L" 项";
const float statsWidth = UiMeasureTextWidth(m_fonts->uiSmall, stats);
const float nameWidth = std::max(20.0f, rect.X + rect.Width - kPadding * m_scale - statsWidth - 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(rect.X + rect.Width - kPadding * m_scale - statsWidth, rect.Y, statsWidth,
rect.Height),
m_theme->textMuted, TextAlign::RIGHT);
}
void SignalPanel::PaintSignalRow(gd::Graphics& g, int row)
{
if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast<int>(m_rows.size()))
{
return;
}
const ListRow& item = m_rows[static_cast<std::size_t>(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);
if (style->visible)
{
UiFillRoundRect(g, checkRect, 3.0f * m_scale, m_theme->accent);
gd::Pen pen(ToGdiColor(m_theme->textOnAccent), 1.6f * m_scale);
pen.SetStartCap(gd::LineCapRound);
pen.SetEndCap(gd::LineCapRound);
g.DrawLine(&pen, checkRect.X + 3.0f * m_scale, centerY, checkRect.X + 6.0f * m_scale,
checkRect.Y + checkRect.Height - 4.0f * m_scale);
g.DrawLine(&pen, checkRect.X + 6.0f * m_scale, checkRect.Y + checkRect.Height - 4.0f * m_scale,
checkRect.X + checkRect.Width - 3.0f * m_scale, checkRect.Y + 4.0f * m_scale);
}
else
{
UiFillRoundRect(g, checkRect, 3.0f * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, checkRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
}
// 颜色块。
const gd::RectF swatchRect(checkRect.X + checkRect.Width + 8.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));
// 名称。
const float unitWidth = signal->unit.empty()
? 0.0f
: UiMeasureTextWidth(m_fonts->uiSmall, L" " + signal->unit) + 4.0f * m_scale;
const float badgeOffset = kBadgeWidth * m_scale + 8.0f * m_scale;
const float nameLeft = swatchRect.X + swatchRect.Width + 8.0f * m_scale;
const float nameWidth = rect.X + rect.Width - kPadding * m_scale - badgeOffset - unitWidth - nameLeft;
const std::uint32_t nameColor = style->visible ? m_theme->text : m_theme->textMuted;
UiText(g, UiEllipsize(g, signal->name, m_fonts->ui, std::max(16.0f, nameWidth)), m_fonts->ui,
gd::RectF(nameLeft, rect.Y, std::max(16.0f, 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 + std::max(16.0f, nameWidth), rect.Y, unitWidth, rect.Height),
m_theme->textMuted, TextAlign::LEFT);
}
// 网格标记。
const int laneIndex = m_tab->LaneOfSignal(ref);
if (laneIndex >= 0)
{
const gd::RectF badgeRect(rect.X + rect.Width - kPadding * m_scale - kBadgeWidth * m_scale,
centerY - kBadgeHeight * m_scale * 0.5f, kBadgeWidth * m_scale,
kBadgeHeight * m_scale);
const bool laneActive = (selected && m_hoverStyle == StyleHit::LANE);
UiFillRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
laneActive ? m_theme->accentSoft : m_theme->cardBg);
UiDrawRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f, m_theme->border, 1.0f);
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect, 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());
const SignalData* signal = (m_dataSet == nullptr || m_tab == nullptr)
? nullptr
: m_dataSet->Resolve(m_tab->Selected());
if (style == nullptr || signal == nullptr)
{
UiText(g, L"请选择信号以编辑样式", m_fonts->uiSmall, m_styleRect, m_theme->textMuted,
TextAlign::CENTER);
return;
}
// 标题行(带上来源文件,便于多文件对比时区分同名信号)。
std::wstring styleTitle = L"样式 · " + signal->name;
if (const DataFile* file = m_dataSet->FileAt(m_tab->Selected().fileIndex); file != nullptr)
{
styleTitle += L" · " + file->fileName;
}
const gd::RectF titleRect(m_styleRect.X + kPadding * m_scale, m_styleRect.Y,
m_styleRect.Width - kPadding * 2.0f * m_scale, 28.0f * m_scale);
UiText(g, UiEllipsize(g, styleTitle, m_fonts->uiBold, titleRect.Width), m_fonts->uiBold, titleRect,
m_theme->textStrong, TextAlign::LEFT);
const float labelX = m_styleRect.X + kPadding * m_scale;
const float labelWidth = kLineLabelWidth * m_scale;
// 颜色。
{
const float rowTop = m_styleRect.Y + 30.0f * m_scale;
UiText(g, L"颜色", m_fonts->uiSmall, gd::RectF(labelX, rowTop, labelWidth, 20.0f * m_scale),
m_theme->textMuted, TextAlign::LEFT);
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
for (int index = 0; index < m_colorCount; ++index)
{
const gd::RectF& rect = m_colorRects[static_cast<std::size_t>(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);
UiFillRoundRect(g, rect, 4.0f * 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 gd::RectF& first = m_widthRects[0];
UiText(g, L"线宽", m_fonts->uiSmall, gd::RectF(labelX, first.Y, labelWidth, first.Height),
m_theme->textMuted, TextAlign::LEFT);
for (int index = 0; index < 4; ++index)
{
const gd::RectF& rect = m_widthRects[static_cast<std::size_t>(index)];
const bool active = std::fabs(style->lineWidth - kWidthOptions[index]) < 0.01f;
const bool hovered = (m_hoverStyle == StyleHit::WIDTH && 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 + 8.0f * m_scale, centerY, rect.X + rect.Width - 8.0f * m_scale, centerY,
active ? m_theme->accent : m_theme->text, kWidthOptions[index] * m_scale);
}
}
// 绘制方式。
{
const gd::RectF& first = m_modeRects[0];
UiText(g, L"方式", m_fonts->uiSmall, gd::RectF(labelX, first.Y, labelWidth, first.Height),
m_theme->textMuted, TextAlign::LEFT);
for (int index = 0; index < 4; ++index)
{
const gd::RectF& rect = m_modeRects[static_cast<std::size_t>(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_laneButtonRect.Y, labelWidth, m_laneButtonRect.Height),
m_theme->textMuted, TextAlign::LEFT);
const bool hovered = (m_hoverStyle == StyleHit::LANE);
UiFillRoundRect(g, m_laneButtonRect, kCorner * m_scale, hovered ? m_theme->hover : m_theme->cardBg);
UiDrawRoundRect(g, m_laneButtonRect, kCorner * m_scale, m_theme->border, 1.0f);
const int laneIndex = m_tab->LaneOfSignal(m_tab->Selected());
const std::wstring label = (laneIndex >= 0) ? (L"网格 " + std::to_wstring(laneIndex + 1))
: L"未分配(勾选后自动分配)";
const gd::RectF textRect(m_laneButtonRect.X + 8.0f * m_scale, m_laneButtonRect.Y,
m_laneButtonRect.Width - 26.0f * m_scale, m_laneButtonRect.Height);
UiText(g, UiEllipsize(g, label, m_fonts->uiSmall, textRect.Width), m_fonts->uiSmall, textRect,
m_theme->text, TextAlign::LEFT);
const float arrowX = m_laneButtonRect.X + m_laneButtonRect.Width - 16.0f * m_scale;
const float arrowY = m_laneButtonRect.Y + m_laneButtonRect.Height * 0.5f;
gd::PointF points[3];
points[0] = gd::PointF(arrowX - 4.0f * m_scale, arrowY - 2.0f * m_scale);
points[1] = gd::PointF(arrowX + 4.0f * m_scale, arrowY - 2.0f * m_scale);
points[2] = gd::PointF(arrowX, arrowY + 3.0f * m_scale);
gd::SolidBrush brush(ToGdiColor(m_theme->textMuted));
g.FillPolygon(&brush, points, 3);
}
}
bool SignalPanel::AreAllSignalsVisible() const
{
if (m_dataSet == nullptr || m_tab == nullptr || m_dataSet->Empty())
{
return false;
}
return m_tab->VisibleCount() == TotalSignalCount();
}
bool SignalPanel::OnMouseMove(int x, int y)
{
if (m_draggingScrollBar)
{
const gd::RectF thumb = ScrollThumbRect();
const float travel = static_cast<float>(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<float>(x);
const float fy = static_cast<float>(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<std::size_t>(index)], fx, fy))
{
hoverStyle = StyleHit::COLOR;
hoverStyleIndex = index;
break;
}
}
if (hoverStyle == StyleHit::NONE)
{
for (int index = 0; index < 4; ++index)
{
if (UiPointInRect(m_widthRects[static_cast<std::size_t>(index)], fx, fy))
{
hoverStyle = StyleHit::WIDTH;
hoverStyleIndex = index;
break;
}
}
}
if (hoverStyle == StyleHit::NONE)
{
for (int index = 0; index < 4; ++index)
{
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
{
hoverStyle = StyleHit::MODE;
hoverStyleIndex = index;
break;
}
}
}
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_laneButtonRect, fx, fy))
{
hoverStyle = StyleHit::LANE;
}
}
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<float>(x);
const float fy = static_cast<float>(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<int>(thumb.Y);
}
else
{
// 点击空白区域时滑块中心对齐鼠标位置。
m_scrollGrabOffset = static_cast<int>(thumb.Height * 0.5f);
const float travel = fy - m_scrollBarRect.Y - static_cast<float>(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;
}
// 全部显示/隐藏。
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()); style != nullptr)
{
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
for (int index = 0; index < m_colorCount; ++index)
{
if (UiPointInRect(m_colorRects[static_cast<std::size_t>(index)], fx, fy))
{
style->color = palette[index % paletteCount];
return true;
}
}
for (int index = 0; index < 4; ++index)
{
if (UiPointInRect(m_widthRects[static_cast<std::size_t>(index)], fx, fy))
{
style->lineWidth = kWidthOptions[index];
return true;
}
}
for (int index = 0; index < 4; ++index)
{
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
{
style->mode = kModeOptions[index];
return true;
}
}
if (UiPointInRect(m_laneButtonRect, fx, fy) && m_tab->Selected().IsValid())
{
ShowLaneMenu();
return true;
}
}
// 列表行。
RowPart part = RowPart::NONE;
const int row = HitRow(x, y, part);
if (row < 0)
{
return false;
}
const ListRow& item = m_rows[static_cast<std::size_t>(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;
}
// 折叠三角与行主体均视为切换折叠。
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;
}
return true;
}
bool SignalPanel::OnLButtonUp(int /*x*/, int /*y*/)
{
if (m_draggingScrollBar)
{
m_draggingScrollBar = false;
return true;
}
return false;
}
bool SignalPanel::OnDoubleClick(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<std::size_t>(row)];
if (item.kind == RowKind::FILE)
{
m_tab->ToggleCollapsed(item.fileIndex);
m_rowsDirty = true;
return true;
}
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;
}
POINT cursor = {x, y};
if (m_host != nullptr)
{
ClientToScreen(m_host, &cursor);
}
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
if (item.kind == RowKind::FILE)
{
ShowFileMenu(item.fileIndex, cursor.x, cursor.y);
return true;
}
const SignalRef ref{item.fileIndex, item.signalIndex};
m_tab->SetSelected(ref);
ShowSignalMenu(ref, cursor.x, cursor.y);
return true;
}
bool SignalPanel::OnMouseWheel(int delta)
{
if (m_dataSet == nullptr)
{
return false;
}
const float previous = m_scrollOffset;
m_scrollOffset -= static_cast<float>(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::ShowLaneMenu()
{
if (m_dataSet == nullptr || m_tab == nullptr || m_host == nullptr || !m_tab->Selected().IsValid())
{
return;
}
const SignalRef ref = m_tab->Selected();
const int currentLane = m_tab->LaneOfSignal(ref);
HMENU menu = CreatePopupMenu();
if (menu == nullptr)
{
return;
}
const std::vector<Lane>& lanes = m_tab->Lanes();
for (std::size_t index = 0; index < lanes.size(); ++index)
{
const std::wstring label = L"网格 " + std::to_wstring(index + 1) + L"(" +
std::to_wstring(lanes[index].signals.size()) + L" 条信号)";
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneBase + index), label.c_str());
}
if (!lanes.empty())
{
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
}
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneNew), L"新建网格");
if (currentLane >= 0 && currentLane < static_cast<int>(lanes.size()))
{
CheckMenuRadioItem(menu, static_cast<UINT>(kMenuLaneBase),
static_cast<UINT>(kMenuLaneBase + lanes.size() - 1),
static_cast<UINT>(kMenuLaneBase + currentLane), MF_BYCOMMAND);
}
POINT anchor = {static_cast<LONG>(m_laneButtonRect.X), static_cast<LONG>(m_laneButtonRect.Y)};
ClientToScreen(m_host, &anchor);
SetForegroundWindow(m_host);
const int command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN, anchor.x,
anchor.y + static_cast<int>(m_laneButtonRect.Height), 0, m_host,
nullptr);
DestroyMenu(menu);
if (command >= kMenuLaneBase && command < kMenuLaneNew)
{
m_tab->MoveSignalToLane(*m_dataSet, ref, command - kMenuLaneBase);
}
else if (command == kMenuLaneNew)
{
m_tab->MoveSignalToLane(*m_dataSet, ref, -1);
}
}
void SignalPanel::ShowSignalMenu(const SignalRef& ref, int screenX, int screenY)
{
if (m_dataSet == nullptr || m_tab == nullptr || m_host == nullptr)
{
return;
}
SignalStyle* style = m_tab->Style(ref);
const SignalData* signal = m_dataSet->Resolve(ref);
if (style == nullptr || signal == nullptr)
{
return;
}
HMENU menu = CreatePopupMenu();
if (menu == nullptr)
{
return;
}
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuToggleVisible),
style->visible ? L"隐藏该信号" : L"显示该信号");
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuSolo), L"仅显示该信号");
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuHideAll), L"隐藏全部信号");
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
HMENU colorMenu = CreatePopupMenu();
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
const wchar_t* colorNames[] = {L"蓝", L"绿", L"红", L"紫", L"黄",
L"青", L"品红", L"橙", L"深灰", L"灰"};
for (int index = 0; index < SignalColorCount(); ++index)
{
const std::uint32_t color = palette[index % paletteCount];
const std::wstring label =
(index < 10) ? std::wstring(colorNames[index]) : (L"颜色 " + std::to_wstring(index + 1));
AppendMenuW(colorMenu, MF_STRING, static_cast<UINT_PTR>(kMenuColorBase + index), label.c_str());
if (style->color == color)
{
CheckMenuRadioItem(colorMenu, static_cast<UINT>(kMenuColorBase),
static_cast<UINT>(kMenuColorBase + SignalColorCount() - 1),
static_cast<UINT>(kMenuColorBase + index), MF_BYCOMMAND);
}
}
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(colorMenu), L"颜色");
HMENU widthMenu = CreatePopupMenu();
for (int index = 0; index < 4; ++index)
{
const std::wstring label = FormatNumber(kWidthOptions[index], 1) + L" px";
AppendMenuW(widthMenu, MF_STRING, static_cast<UINT_PTR>(kMenuWidthBase + index), label.c_str());
if (std::fabs(style->lineWidth - kWidthOptions[index]) < 0.01f)
{
CheckMenuRadioItem(widthMenu, static_cast<UINT>(kMenuWidthBase),
static_cast<UINT>(kMenuWidthBase + 3),
static_cast<UINT>(kMenuWidthBase + index), MF_BYCOMMAND);
}
}
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(widthMenu), L"线宽");
HMENU modeMenu = CreatePopupMenu();
for (int index = 0; index < 4; ++index)
{
AppendMenuW(modeMenu, MF_STRING, static_cast<UINT_PTR>(kMenuModeBase + index),
DrawModeName(kModeOptions[index]));
if (style->mode == kModeOptions[index])
{
CheckMenuRadioItem(modeMenu, static_cast<UINT>(kMenuModeBase),
static_cast<UINT>(kMenuModeBase + 3),
static_cast<UINT>(kMenuModeBase + index), MF_BYCOMMAND);
}
}
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(modeMenu), L"显示方式");
HMENU laneMenu = CreatePopupMenu();
const std::vector<Lane>& lanes = m_tab->Lanes();
const int currentLane = m_tab->LaneOfSignal(ref);
for (std::size_t index = 0; index < lanes.size(); ++index)
{
const std::wstring laneLabel = L"网格 " + std::to_wstring(index + 1);
AppendMenuW(laneMenu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneBase + index), laneLabel.c_str());
}
if (!lanes.empty())
{
AppendMenuW(laneMenu, MF_SEPARATOR, 0, nullptr);
}
AppendMenuW(laneMenu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneNew), L"新建网格");
if (currentLane >= 0 && !lanes.empty())
{
CheckMenuRadioItem(laneMenu, static_cast<UINT>(kMenuLaneBase),
static_cast<UINT>(kMenuLaneBase + lanes.size() - 1),
static_cast<UINT>(kMenuLaneBase + currentLane), MF_BYCOMMAND);
}
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(laneMenu), L"所属网格");
SetForegroundWindow(m_host);
const int command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN, screenX, screenY,
0, m_host, nullptr);
DestroyMenu(menu);
if (command == kMenuToggleVisible)
{
m_tab->ToggleVisible(*m_dataSet, ref);
m_rowsDirty = true;
}
else if (command == kMenuSolo)
{
m_tab->SoloSignal(*m_dataSet, ref);
m_rowsDirty = true;
}
else if (command == kMenuHideAll)
{
m_tab->SetAllVisible(*m_dataSet, false);
m_rowsDirty = true;
}
else if (command >= kMenuModeBase && command < kMenuModeBase + 4)
{
style->mode = kModeOptions[command - kMenuModeBase];
}
else if (command >= kMenuWidthBase && command < kMenuWidthBase + 4)
{
style->lineWidth = kWidthOptions[command - kMenuWidthBase];
}
else if (command >= kMenuColorBase && command < kMenuColorBase + SignalColorCount())
{
style->color = palette[(command - kMenuColorBase) % paletteCount];
}
else if (command >= kMenuLaneBase && command < kMenuLaneNew)
{
m_tab->MoveSignalToLane(*m_dataSet, ref, command - kMenuLaneBase);
m_rowsDirty = true;
}
else if (command == kMenuLaneNew)
{
m_tab->MoveSignalToLane(*m_dataSet, ref, -1);
m_rowsDirty = true;
}
}
void SignalPanel::ShowFileMenu(int fileIndex, int screenX, int screenY)
{
if (m_dataSet == nullptr || m_tab == nullptr || m_host == nullptr)
{
return;
}
const DataFile* file = m_dataSet->FileAt(fileIndex);
if (file == nullptr)
{
return;
}
HMENU menu = CreatePopupMenu();
if (menu == nullptr)
{
return;
}
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuFileShowAll), L"选中该文件全部信号");
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuFileHideAll), L"取消该文件全部信号");
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuFileRemove), L"从工作区移除该文件");
SetForegroundWindow(m_host);
const int command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN, screenX,
screenY, 0, m_host, nullptr);
DestroyMenu(menu);
if (command == kMenuFileShowAll)
{
m_tab->SetFileVisible(*m_dataSet, fileIndex, true);
m_rowsDirty = true;
}
else if (command == kMenuFileHideAll)
{
m_tab->SetFileVisible(*m_dataSet, fileIndex, false);
m_rowsDirty = true;
}
else if (command == kMenuFileRemove)
{
// 移除文件属于数据层操作,交由宿主窗口处理。
if (m_host != nullptr)
{
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(fileIndex), 0);
}
}
}
} // namespace jdi