Files
Just-DataInspector/Components/SignalPanel/Src/SignalPanel.cpp
T
DeepSeek V4.1 Flash eb868b2679 feat: 重构信号样式区并统一主题化弹出菜单
样式编辑区:
- 移除顶部「样式 + 信号名 + 文件名」标题行,选中信号即代表编辑目标。
- 绘制方式取消阶梯选项,仅保留线段/散点/点线,标签由「方式」改为「样式」。
- 线宽改为「最细预设 + 数值旋钮 + 最粗预设」:中间为带左右增减箭头的输入框,
  步进 0.2px,取值范围 0.5-6.0px,并标注单位为 px。
- 颜色在 10 个预设色块后新增带加号图标的方格,点击展开 RGB 三通道滑块面板,
  可实时预览并显示十六进制值;点击面板外任意位置收起。
- 移除网格归属下拉框,改由每条信号行末尾的 G 徽标承载,点击弹出网格归属菜单。

信号面板与菜单:
- 文件分组行新增关闭按钮,可直接从工作区移除该文件,与整体风格保持一致。
- 新增 PopupMenu 组件:绘制在窗口后备缓冲区内的自绘菜单,含圆角、投影、悬停高亮、
  单选对勾、色块行与分组标题,彻底替换系统默认弹出菜单;
  菜单打开期间接管鼠标与键盘(Esc / 上下键 / 回车),失焦或在菜单外释放时自动关闭。
- 信号、文件、网格、标签页四类右键菜单全部改用该组件。

其他修复:
- 信号名称不再保留尾部单位括号,避免名称后再重复显示单位。
- 数据模型移除 DrawMode::STEP 枚举。
- 标签栏与信号面板弹出菜单后补充重绘请求,修复菜单不显示的问题。
2026-09-29 16:30:48 +08:00

1737 lines
54 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 kStyleBaseHeight = 124.0f; /**< 样式编辑区基础高度。 */
const float kColorPickerHeight = 132.0f;/**< 自定义颜色面板高度。 */
const float kRowHeight = 26.0f; /**< 样式区单行高度。 */
const float kRowGap = 10.0f; /**< 样式区行间距。 */
const float kStylePadTop = 12.0f; /**< 样式区上内边距。 */
const float kStylePadH = 12.0f; /**< 面板左右内边距。 */
const float kPadding = 12.0f; /**< 通用内边距。 */
const float kCheckSize = 15.0f; /**< 复选框尺寸。 */
const float kSwatchSize = 12.0f; /**< 信号行颜色小方块尺寸。 */
const float kBadgeWidth = 30.0f; /**< 网格徽标宽度。 */
const float kBadgeHeight = 17.0f; /**< 网格徽标高度。 */
const float kCorner = 4.0f; /**< 通用圆角半径。 */
const float kLineLabelWidth = 40.0f; /**< 样式行标签列宽度。 */
const float kDisclosureWidth = 16.0f; /**< 折叠三角占位宽度。 */
const float kIndent = 20.0f; /**< 信号行相对文件行的缩进。 */
const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */
const float kCloseButtonSize = 18.0f; /**< 文件行关闭按钮尺寸。 */
const float kSliderLabelWidth = 18.0f; /**< 滑块行标签列宽度。 */
const float kSliderValueWidth = 42.0f; /**< 滑块数值显示宽度。 */
const float kSliderRowHeight = 24.0f; /**< 滑块行高。 */
const float kSliderGap = 4.0f; /**< 滑块行间距。 */
const float 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<int>(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 - 4.0f * scale, track.Y - 9.0f * scale, track.Width + 8.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] hovered 是否处于悬停状态。
* @param [IN] scale 缩放比例。
*/
void DrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
bool hovered, float scale)
{
const float radius = track.Height * 0.5f;
UiFillRoundRect(g, track, radius, 0x2A657B83u);
ratio = std::max(0.0, std::min(1.0, ratio));
const float fillWidth = std::max(track.Height, static_cast<float>(ratio) * track.Width);
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
const float knobRadius = radius + (hovered ? 2.5f * scale : 1.0f * 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 颜色值。
* @return 形如 #RRGGBB 的文本。
*/
std::wstring ColorToHex(std::uint32_t color)
{
wchar_t buffer[16];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu,
(color >> 8) & 0xFFu, color & 0xFFu);
return std::wstring(buffer);
}
}
SignalPanel::SignalPanel()
{
m_colorRects.fill(gd::RectF());
m_modeRects.fill(gd::RectF());
m_sliderRects.fill(gd::RectF());
}
void SignalPanel::Setup(HWND host, const UiFonts* fonts, const Theme* theme, PopupMenu* menu)
{
m_host = host;
m_fonts = fonts;
m_theme = theme;
m_menu = menu;
}
void SignalPanel::SetContext(const DataSet* dataSet, TabConfig* tab)
{
m_dataSet = dataSet;
m_tab = tab;
m_rowsDirty = true;
m_scrollOffset = 0.0f;
m_colorPickerOpen = false;
m_draggingSlider = -1;
}
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());
}
float SignalPanel::DesiredStyleHeight() const
{
return kStyleBaseHeight + (m_colorPickerOpen ? kColorPickerHeight : 0.0f);
}
bool SignalPanel::TakeLayoutDirty()
{
const bool dirty = m_layoutDirty;
m_layoutDirty = false;
return dirty;
}
bool SignalPanel::CloseTransientUi()
{
if (!m_colorPickerOpen)
{
return false;
}
m_colorPickerOpen = false;
m_draggingSlider = -1;
m_layoutDirty = true;
return true;
}
void SignalPanel::BuildRows()
{
m_rows.clear();
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);
}
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<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 int rowIndex = static_cast<int>(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 = kLineLabelWidth * m_scale;
const float contentLeft = m_styleRect.X + kStylePadH * m_scale + labelWidth;
const float contentWidth = std::max(40.0f, m_styleRect.Width - kStylePadH * 2.0f * m_scale - labelWidth);
const float row0Top = m_styleRect.Y + kStylePadTop * m_scale;
const float row1Top = row0Top + (kRowHeight + kRowGap) * m_scale;
const float row2Top = row1Top + (kRowHeight + kRowGap) * m_scale;
// ---- 第一行:颜色(预设色块 + 自定义颜色入口)----
int paletteCount = 0;
SignalPalette(paletteCount);
const float chipSize = 22.0f * m_scale;
const float chipGap = 8.0f * m_scale;
const float swatchGap = 4.0f * m_scale;
const float available = std::max(swatchGap, contentWidth - chipGap - chipSize);
int count = std::min(paletteCount, static_cast<int>(m_colorRects.size()));
float swatchSize = 18.0f * m_scale;
while (count > 1)
{
const float fitted =
(available - swatchGap * static_cast<float>(count - 1)) / static_cast<float>(count);
if (fitted >= 12.0f * m_scale)
{
swatchSize = std::min(18.0f * m_scale, fitted);
break;
}
--count;
}
if (count <= 1)
{
count = std::max(1, std::min(count, 1));
swatchSize = std::min(18.0f * m_scale, available);
}
m_colorCount = count;
const float swatchTop = row0Top + (kRowHeight * m_scale - swatchSize) * 0.5f;
for (int index = 0; index < m_colorCount; ++index)
{
const float x = contentLeft + static_cast<float>(index) * (swatchSize + swatchGap);
m_colorRects[static_cast<std::size_t>(index)] = gd::RectF(x, swatchTop, swatchSize, swatchSize);
}
const float chipLeft = contentLeft + static_cast<float>(m_colorCount) * (swatchSize + swatchGap) -
swatchGap + chipGap;
m_customChipRect = gd::RectF(std::min(chipLeft, contentLeft + contentWidth - chipSize),
row0Top + (kRowHeight * m_scale - chipSize) * 0.5f, chipSize, chipSize);
// ---- 第二行:线宽(两个预设 + 数值旋钮)----
float presetWidth = 46.0f * m_scale;
float buttonWidth = 22.0f * m_scale;
float valueWidth = 62.0f * m_scale;
float gap = 6.0f * m_scale;
const float natural = presetWidth * 2.0f + buttonWidth * 2.0f + valueWidth + gap * 2.0f;
if (natural > contentWidth)
{
const float shrink = contentWidth / natural;
presetWidth = std::max(28.0f * m_scale, presetWidth * shrink);
buttonWidth = std::max(16.0f * m_scale, buttonWidth * shrink);
valueWidth = std::max(40.0f * m_scale, valueWidth * shrink);
gap = std::max(3.0f * m_scale, gap * shrink);
}
const float spinnerWidth = buttonWidth * 2.0f + valueWidth;
const float groupWidth = presetWidth * 2.0f + spinnerWidth + gap * 2.0f;
const float groupLeft = contentLeft + std::max(0.0f, (contentWidth - groupWidth) * 0.5f);
m_widthThinRect = gd::RectF(groupLeft, row1Top, presetWidth, kRowHeight * m_scale);
m_widthSpinnerRect = gd::RectF(groupLeft + presetWidth + gap, row1Top, spinnerWidth,
kRowHeight * m_scale);
m_widthMinusRect = gd::RectF(m_widthSpinnerRect.X, row1Top, buttonWidth, kRowHeight * m_scale);
m_widthValueRect = gd::RectF(m_widthMinusRect.X + buttonWidth, row1Top, valueWidth,
kRowHeight * m_scale);
m_widthPlusRect = gd::RectF(m_widthValueRect.X + valueWidth, row1Top, buttonWidth,
kRowHeight * m_scale);
m_widthThickRect = gd::RectF(m_widthSpinnerRect.X + spinnerWidth + gap, row1Top, presetWidth,
kRowHeight * m_scale);
// ---- 第三行:样式(线段 / 散点 / 点线)----
const float modeGap = 6.0f * m_scale;
const float modeWidth = (contentWidth - modeGap * static_cast<float>(kModeOptionCount - 1)) /
static_cast<float>(kModeOptionCount);
for (int index = 0; index < kModeOptionCount; ++index)
{
const float x = contentLeft + static_cast<float>(index) * (modeWidth + modeGap);
m_modeRects[static_cast<std::size_t>(index)] = gd::RectF(x, row2Top, modeWidth, kRowHeight * m_scale);
}
// ---- 自定义颜色面板 ----
if (!m_colorPickerOpen)
{
m_pickerRect = gd::RectF();
return;
}
const float pickerTop = row2Top + (kRowHeight + kRowGap) * m_scale;
m_pickerRect = gd::RectF(m_styleRect.X + kStylePadH * m_scale, pickerTop,
std::max(40.0f, m_styleRect.Width - kStylePadH * 2.0f * m_scale),
kColorPickerHeight * m_scale - kRowGap * m_scale);
const float sliderLabelX = m_pickerRect.X + 6.0f * m_scale;
const float trackLeft = sliderLabelX + kSliderLabelWidth * m_scale;
const float trackWidth =
std::max(24.0f, m_pickerRect.Width - (trackLeft - m_pickerRect.X) - 6.0f * m_scale -
kSliderValueWidth * m_scale);
const float trackHeight = 6.0f * m_scale;
const float firstTop = m_pickerRect.Y + 10.0f * m_scale;
for (int index = 0; index < 3; ++index)
{
const float rowTop = firstTop + static_cast<float>(index) * (kSliderRowHeight + kSliderGap) * m_scale;
m_sliderRects[static_cast<std::size_t>(index)] =
gd::RectF(trackLeft, rowTop + (kSliderRowHeight * m_scale - trackHeight) * 0.5f, trackWidth,
trackHeight);
}
const float previewTop = firstTop + 3.0f * (kSliderRowHeight + kSliderGap) * m_scale;
m_pickerPreviewRect = gd::RectF(trackLeft, previewTop, 54.0f * m_scale, 20.0f * m_scale);
}
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<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 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<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);
UiDrawCheckBox(g, checkRect, style->visible ? 1 : 0, 3.0f * m_scale);
// 颜色块。
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 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;
}
// 名称与单位。
const float unitWidth = signal->unit.empty()
? 0.0f
: UiMeasureTextWidth(m_fonts->uiSmall, L" " + signal->unit) + 4.0f * m_scale;
const float nameLeft = swatchRect.X + swatchRect.Width + 8.0f * m_scale;
const float nameWidth = std::max(16.0f, nameRightLimit - unitWidth - nameLeft);
const std::uint32_t nameColor = style->visible ? m_theme->text : m_theme->textMuted;
UiText(g, UiEllipsize(g, signal->name, m_fonts->ui, nameWidth), m_fonts->ui,
gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), nameColor, TextAlign::LEFT);
if (!signal->unit.empty())
{
const std::wstring unitText = L" " + signal->unit;
UiText(g, unitText, m_fonts->uiSmall,
gd::RectF(nameLeft + nameWidth, rect.Y, unitWidth, rect.Height), m_theme->textMuted,
TextAlign::LEFT);
}
if (badgeVisible)
{
const bool badgeHot = hovered && (m_hoverPart == RowPart::LANE_BADGE);
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)
{
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 = kLineLabelWidth * m_scale;
// ---- 颜色 ----
{
const float rowTop = m_styleRect.Y + kStylePadTop * m_scale;
UiText(g, L"颜色", m_fonts->uiSmall, gd::RectF(labelX, rowTop, labelWidth, kRowHeight * m_scale),
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 bool chipHot = (m_hoverStyle == StyleHit::CUSTOM_CHIP);
const bool chipActive = m_colorPickerOpen;
UiFillRoundRect(g, m_customChipRect, 4.0f * m_scale,
chipActive ? m_theme->accentSoft : (chipHot ? m_theme->hover : m_theme->cardBg));
UiDrawRoundRect(g, m_customChipRect, 4.0f * m_scale,
chipActive ? m_theme->accent : m_theme->borderStrong, 1.0f);
UiDrawPlusIcon(g, m_customChipRect, chipActive ? m_theme->accent : m_theme->text, 1.6f * m_scale);
}
// ---- 线宽:两个预设 + 数值旋钮 ----
{
UiText(g, L"线宽", m_fonts->uiSmall,
gd::RectF(labelX, m_widthThinRect.Y, labelWidth, m_widthThinRect.Height),
m_theme->textMuted, TextAlign::LEFT);
struct PresetInfo
{
gd::RectF rect;
float width;
int index;
};
const PresetInfo presets[] = {
{m_widthThinRect, kWidthThinPreset, 0},
{m_widthThickRect, kWidthThickPreset, 1},
};
for (const PresetInfo& preset : presets)
{
const bool active = std::fabs(style->lineWidth - preset.width) < 0.01f;
const bool hovered = (m_hoverStyle == StyleHit::WIDTH_PRESET && m_hoverStyleIndex == preset.index);
UiFillRoundRect(g, preset.rect, kCorner * m_scale,
active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg));
UiDrawRoundRect(g, preset.rect, kCorner * m_scale,
active ? m_theme->accent : m_theme->border, 1.0f);
const float centerY = preset.rect.Y + preset.rect.Height * 0.5f;
UiDrawLine(g, preset.rect.X + 9.0f * m_scale, centerY, preset.rect.X + preset.rect.Width - 9.0f * m_scale,
centerY, active ? m_theme->accent : m_theme->text, preset.width * m_scale);
}
// 数值输入框。
UiFillRoundRect(g, m_widthSpinnerRect, kCorner * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, m_widthSpinnerRect, kCorner * m_scale, m_theme->border, 1.0f);
const std::wstring valueText = FormatNumber(style->lineWidth, 1) + L" px";
UiText(g, valueText, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, TextAlign::CENTER);
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);
}
UiDrawLine(g, m_widthMinusRect.X + m_widthMinusRect.Width, m_widthMinusRect.Y + 3.0f * m_scale,
m_widthMinusRect.X + m_widthMinusRect.Width, m_widthMinusRect.Y + m_widthMinusRect.Height -
3.0f * m_scale,
m_theme->border, 1.0f);
UiDrawLine(g, m_widthPlusRect.X, m_widthPlusRect.Y + 3.0f * m_scale, m_widthPlusRect.X,
m_widthPlusRect.Y + m_widthPlusRect.Height - 3.0f * m_scale, m_theme->border, 1.0f);
UiDrawArrowIcon(g, m_widthMinusRect,
minusEnabled ? m_theme->text : m_theme->textMuted, 1.6f * m_scale,
IconDirection::LEFT);
UiDrawArrowIcon(g, m_widthPlusRect, plusEnabled ? m_theme->text : m_theme->textMuted,
1.6f * m_scale, IconDirection::RIGHT);
}
// ---- 样式 ----
{
const gd::RectF& first = m_modeRects[0];
UiText(g, L"样式", m_fonts->uiSmall, gd::RectF(labelX, first.Y, labelWidth, first.Height),
m_theme->textMuted, TextAlign::LEFT);
for (int index = 0; index < kModeOptionCount; ++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);
}
}
if (m_colorPickerOpen)
{
PaintColorPicker(g, *style);
}
}
void SignalPanel::PaintColorPicker(gd::Graphics& g, const SignalStyle& style)
{
UiFillRoundRect(g, m_pickerRect, kCorner * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, m_pickerRect, kCorner * m_scale, m_theme->borderStrong, 1.0f);
static const wchar_t* kChannelNames[3] = {L"R", L"G", L"B"};
static const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u};
for (int index = 0; index < 3; ++index)
{
const gd::RectF& track = m_sliderRects[static_cast<std::size_t>(index)];
const gd::RectF rowRect(m_pickerRect.X, track.Y - 9.0f * m_scale,
m_pickerRect.Width, kSliderRowHeight * m_scale);
UiText(g, kChannelNames[index], m_fonts->uiSmall,
gd::RectF(m_pickerRect.X + 6.0f * m_scale, rowRect.Y, kSliderLabelWidth * m_scale,
rowRect.Height),
m_theme->textMuted, TextAlign::LEFT);
DrawSlider(g, track, m_pickerChannels[static_cast<std::size_t>(index)], kChannelColors[index],
m_hoverStyle == StyleHit::SLIDER && m_hoverStyleIndex == index, m_scale);
const int value = static_cast<int>(m_pickerChannels[static_cast<std::size_t>(index)] * 255.0 + 0.5);
UiText(g, std::to_wstring(value), m_fonts->monoSmall,
gd::RectF(track.X + track.Width + 6.0f * m_scale, rowRect.Y, kSliderValueWidth * m_scale,
rowRect.Height),
m_theme->text, TextAlign::RIGHT);
}
// 预览与十六进制值。
UiText(g, L"预览", m_fonts->uiSmall,
gd::RectF(m_pickerRect.X + 6.0f * m_scale, m_pickerPreviewRect.Y,
kSliderLabelWidth * m_scale + 20.0f * m_scale, m_pickerPreviewRect.Height),
m_theme->textMuted, TextAlign::LEFT);
UiFillRoundRect(g, m_pickerPreviewRect, 3.0f * m_scale, style.color);
UiDrawRoundRect(g, m_pickerPreviewRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
UiText(g, ColorToHex(style.color), m_fonts->monoSmall,
gd::RectF(m_pickerPreviewRect.X + m_pickerPreviewRect.Width + 8.0f * m_scale,
m_pickerPreviewRect.Y, 96.0f * m_scale, m_pickerPreviewRect.Height),
m_theme->text, TextAlign::LEFT);
}
bool SignalPanel::AreAllSignalsVisible() const
{
if (m_dataSet == nullptr || m_tab == nullptr || m_dataSet->Empty())
{
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 = style->lineWidth + delta;
value = std::max(kWidthMin, std::min(kWidthMax, value));
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::OpenColorPicker(const SignalStyle& style)
{
m_pickerChannels[0] = static_cast<double>((style.color >> 16) & 0xFFu) / 255.0;
m_pickerChannels[1] = static_cast<double>((style.color >> 8) & 0xFFu) / 255.0;
m_pickerChannels[2] = static_cast<double>(style.color & 0xFFu) / 255.0;
if (m_colorPickerOpen)
{
return true;
}
m_colorPickerOpen = true;
m_layoutDirty = true;
return true;
}
bool SignalPanel::OnMouseMove(int x, int y)
{
// 滑块拖拽优先级最高。
if (m_draggingSlider >= 0 && m_draggingSlider < 3)
{
const gd::RectF& track = m_sliderRects[static_cast<std::size_t>(m_draggingSlider)];
m_pickerChannels[static_cast<std::size_t>(m_draggingSlider)] =
UiSliderRatioFromX(track, static_cast<float>(x));
if (m_tab != nullptr)
{
if (SignalStyle* style = m_tab->Style(m_tab->Selected()))
{
const unsigned int r =
static_cast<unsigned int>(m_pickerChannels[0] * 255.0 + 0.5);
const unsigned int g =
static_cast<unsigned int>(m_pickerChannels[1] * 255.0 + 0.5);
const unsigned int b =
static_cast<unsigned int>(m_pickerChannels[2] * 255.0 + 0.5);
style->color = 0xFF000000u | (r << 16) | (g << 8) | b;
}
}
return true;
}
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_colorPickerOpen)
{
for (int index = 0; index < 3; ++index)
{
if (UiPointInRect(SliderHitRect(m_sliderRects[static_cast<std::size_t>(index)], m_scale), fx, fy))
{
hoverStyle = StyleHit::SLIDER;
hoverStyleIndex = index;
break;
}
}
}
if (hoverStyle == StyleHit::NONE && m_tab != nullptr && m_tab->Style(m_tab->Selected()) != nullptr)
{
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 && 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)
{
for (int index = 0; index < kModeOptionCount; ++index)
{
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
{
hoverStyle = StyleHit::MODE;
hoverStyleIndex = index;
break;
}
}
}
}
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 (m_colorPickerOpen)
{
for (int index = 0; index < 3; ++index)
{
if (UiPointInRect(SliderHitRect(m_sliderRects[static_cast<std::size_t>(index)], m_scale), fx, fy))
{
m_draggingSlider = index;
m_pickerChannels[static_cast<std::size_t>(index)] =
UiSliderRatioFromX(m_sliderRects[static_cast<std::size_t>(index)], fx);
if (SignalStyle* style = m_tab->Style(m_tab->Selected()))
{
const unsigned int r = static_cast<unsigned int>(m_pickerChannels[0] * 255.0 + 0.5);
const unsigned int g = static_cast<unsigned int>(m_pickerChannels[1] * 255.0 + 0.5);
const unsigned int b = static_cast<unsigned int>(m_pickerChannels[2] * 255.0 + 0.5);
style->color = 0xFF000000u | (r << 16) | (g << 8) | b;
}
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()))
{
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;
}
}
if (UiPointInRect(m_customChipRect, fx, fy))
{
return OpenColorPicker(*style);
}
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);
}
for (int index = 0; index < kModeOptionCount; ++index)
{
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
{
style->mode = kModeOptions[index];
return true;
}
}
// 点击样式区内其它位置(颜色面板之外)时收起自定义颜色面板。
if (m_colorPickerOpen && !UiPointInRect(m_pickerRect, fx, fy))
{
return CloseTransientUi();
}
}
// 列表行。
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;
}
if (part == RowPart::CLOSE)
{
// 关闭文件属于数据层操作,交由宿主窗口处理。
if (m_host != nullptr)
{
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(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)
{
const gd::RectF badge = SignalBadgeRect(row);
OpenLaneMenu(ref, static_cast<int>(badge.X), static_cast<int>(badge.Y + badge.Height));
}
return true;
}
bool SignalPanel::OnLButtonUp(int /*x*/, int /*y*/)
{
if (m_draggingSlider >= 0)
{
m_draggingSlider = -1;
return true;
}
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;
}
const ListRow& item = m_rows[static_cast<std::size_t>(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<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::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<MenuItem> 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<MenuItem> 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<MenuItem> items;
MenuItem header;
header.kind = MenuItemKind::HEADER;
header.label = L"所属网格";
items.push_back(header);
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)
{
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<int>(index);
lane.checked = (currentLane == static_cast<int>(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<WPARAM>(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