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