Files
Just-DataInspector/Components/SignalPanel/Src/SignalPanel.cpp
T
DeepSeek V4.1 Flash 729494d1ec fix: 修复网格徽标菜单与标签页编号,重构样式区并新增透明度与浮动拾色器
信号列表
- 网格徽标(G1/G2)改为在左键抬起时弹出菜单,修复此前「按住才显示、松手即消失」的问题。
- 文件分组行新增关闭按钮(✕),可直接从工作区移除文件,与右键菜单「关闭该文件」等价。
- 信号名改为完整保留文件内的列名原文,不再剥离 "[n]" 序号前缀与括号内容。

标签页
- 新建标签页改用「当前未被占用的最小编号」,关闭标签页后编号可被复用,
  修复「开 3 关 2 再新建会变成视图 4」的问题。

样式编辑区(统一为固定四行网格布局,面板缩放时不再错位)
- 移除顶部「样式 · 信号名 · 文件名」标题行,选中信号即为编辑对象。
- 绘制方式取消「阶梯」,标签由「方式」改为「样式」,仅保留线段 / 散点 / 点线。
- 线宽仅保留最细与最粗两个预设,中间改为带左右箭头的数值旋钮,单位为 px;
  支持单击或双击数值框直接键入(首次键入替换原值,回车提交、Esc 取消、越界钳制到 0.5-6.0)。
- 颜色预设后新增与预设色块完全同尺寸、同外框样式的「加号」小方格,
  点击后弹出浮动拾色面板(与 G1/G2 菜单同风格的浮动窗口),支持 RGB 三通道滑块、
  实时预览与十六进制显示;点击面板外部即收起,取色实时应用到当前信号。
- 移除样式区末尾的网格选择控件,该功能统一由信号行的 G 徽标承载。
- 新增透明度滑块,默认 100%,可在 0-100% 之间调节,并实时反映到波形、
  图例色块与信号行色块的显示效果。

其他
- 信号面板默认宽度与行内间距微调,使较长的列名可以完整显示。
- 失焦时提交线宽输入并收起浮动拾色器与弹出菜单。
2026-09-29 17:14:27 +08:00

1903 lines
57 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 <cstdlib>
#include "Utf8Util.hpp"
namespace jdi
{
namespace
{
const float kHeaderHeight = 38.0f; /**< 标题区高度(逻辑像素)。 */
const float kFileRowHeight = 30.0f; /**< 文件分组行高。 */
const float kSignalRowHeight = 27.0f; /**< 信号行高。 */
const float kPadding = 12.0f; /**< 通用内边距。 */
const float kCheckSize = 15.0f; /**< 复选框尺寸。 */
const float kSwatchSize = 12.0f; /**< 信号行颜色小方块尺寸。 */
const float kBadgeWidth = 30.0f; /**< 网格徽标宽度。 */
const float kBadgeHeight = 17.0f; /**< 网格徽标高度。 */
const float kCorner = 4.0f; /**< 通用圆角半径。 */
const float kDisclosureWidth = 16.0f; /**< 折叠三角占位宽度。 */
const float kIndent = 14.0f; /**< 信号行相对文件行的缩进。 */
const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */
const float kCloseButtonSize = 18.0f; /**< 文件行关闭按钮尺寸。 */
// 样式编辑区:固定四行网格,行高与间距恒定,保证面板宽度变化时控件不散架。
const float kStylePadH = 12.0f; /**< 样式区左右内边距。 */
const float kStylePadV = 12.0f; /**< 样式区上下内边距。 */
const float kStyleRowHeight = 26.0f; /**< 样式区单行高度。 */
const float kStyleRowGap = 10.0f; /**< 样式区行间距。 */
const float kStyleLabelWidth = 44.0f; /**< 样式行标签列宽度。 */
const float kStyleRowCount = 4.0f; /**< 样式区固定行数。 */
const float kSwatchEdge = 18.0f; /**< 颜色色块边长(含自定义颜色入口)。 */
const float kSwatchGap = 5.0f; /**< 颜色色块间距。 */
const float kWidthPresetWidth = 46.0f; /**< 线宽预设按钮宽度。 */
const float kWidthSpinButton = 22.0f; /**< 线宽旋钮按钮宽度。 */
const float kWidthValueWidth = 62.0f; /**< 线宽数值框宽度。 */
const float kOpacityValueWidth = 52.0f; /**< 不透明度数值宽度。 */
const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */
const float kWidthMin = 0.5f; /**< 允许的最小线宽。 */
const float kWidthMax = 6.0f; /**< 允许的最大线宽。 */
const float kWidthStep = 0.2f; /**< 线宽增减步进。 */
const float kWidthThinPreset = 1.0f; /**< 最细线宽预设值。 */
const float kWidthThickPreset = 3.0f; /**< 最粗线宽预设值。 */
/** @brief 可选绘制样式顺序(不含阶梯方式)。 */
const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER};
const int kModeOptionCount = static_cast<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 - 6.0f * scale, track.Y - 9.0f * scale, track.Width + 12.0f * scale,
track.Height + 18.0f * scale);
}
/**
* @brief 绘制水平滑块(轨道 + 已填充部分 + 圆钮)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] track 轨道矩形。
* @param [IN] ratio 当前值比例(0-1)。
* @param [IN] fillColor 已填充部分颜色。
* @param [IN] active 是否处于悬停或拖拽状态。
* @param [IN] scale 缩放比例。
*/
void DrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
bool active, float scale)
{
const float radius = track.Height * 0.5f;
UiFillRoundRect(g, track, radius, 0x2A657B83u);
ratio = std::max(0.0, std::min(1.0, ratio));
const float fillWidth = std::max(track.Height, static_cast<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 + (active ? 2.5f * scale : 0.8f * scale);
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
knobRadius * 2.0f);
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
knobRadius * 2.0f);
}
/**
* @brief 将颜色按不透明度叠加为实际显示颜色。
* @param [IN] color 0xAARRGGBB 颜色值。
* @param [IN] opacity 不透明度(0-1)。
* @return 叠加后的颜色值。
*/
std::uint32_t ApplyOpacity(std::uint32_t color, float opacity)
{
double alpha = static_cast<double>(opacity);
alpha = std::max(0.0, std::min(1.0, alpha));
return WithAlpha(color, static_cast<unsigned int>(alpha * 255.0 + 0.5));
}
/**
* @brief 将数值限制到合法区间。
* @param [IN] value 输入值。
* @param [IN] low 下限。
* @param [IN] high 上限。
* @return 限制后的数值。
*/
float ClampFloat(float value, float low, float high)
{
return (value < low) ? low : ((value > high) ? high : value);
}
}
SignalPanel::SignalPanel()
{
m_colorRects.fill(gd::RectF());
m_modeRects.fill(gd::RectF());
}
void SignalPanel::Setup(HWND host, const UiFonts* fonts, const Theme* theme, PopupMenu* menu,
ColorPickerPopup* colorPicker)
{
m_host = host;
m_fonts = fonts;
m_theme = theme;
m_menu = menu;
m_colorPicker = colorPicker;
}
void SignalPanel::SetContext(const DataSet* dataSet, TabConfig* tab)
{
m_dataSet = dataSet;
m_tab = tab;
m_rowsDirty = true;
m_scrollOffset = 0.0f;
m_draggingOpacity = false;
m_widthEditing = false;
m_widthEditText.clear();
m_pendingLaneMenu = false;
m_pendingColorPicker = false;
}
void SignalPanel::SetScale(float scale)
{
m_scale = (scale > 0.1f) ? scale : 1.0f;
}
bool SignalPanel::HitTest(int x, int y) const
{
return UiPointInRect(m_area, static_cast<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 kStylePadV * 2.0f + kStyleRowCount * kStyleRowHeight +
(kStyleRowCount - 1.0f) * kStyleRowGap;
}
bool SignalPanel::TakeLayoutDirty()
{
const bool dirty = m_layoutDirty;
m_layoutDirty = false;
return dirty;
}
void SignalPanel::BuildRows()
{
m_rows.clear();
m_rowsDirty = false;
if (m_dataSet == nullptr || m_tab == nullptr)
{
m_contentHeight = 0.0f;
return;
}
const float fileHeight = kFileRowHeight * m_scale;
const float signalHeight = kSignalRowHeight * m_scale;
const std::vector<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 = kStyleLabelWidth * m_scale;
const float contentLeft = m_styleRect.X + kStylePadH * m_scale + labelWidth;
const float contentWidth = std::max(60.0f, m_styleRect.Width - kStylePadH * 2.0f * m_scale - labelWidth);
const float rowHeight = kStyleRowHeight * m_scale;
const float row0Top = m_styleRect.Y + kStylePadV * m_scale;
// ---- 第一行:颜色(预设色块 + 同尺寸自定义颜色入口)----
int paletteCount = 0;
SignalPalette(paletteCount);
float swatchSize = kSwatchEdge * m_scale;
const float swatchGap = kSwatchGap * m_scale;
int count = std::min(paletteCount, static_cast<int>(m_colorRects.size()));
// 为自定义颜色入口预留一个同尺寸格位,优先整体缩小色块以保留全部预设颜色。
if (count > 0)
{
const float fit = (contentWidth - swatchGap * static_cast<float>(count)) /
static_cast<float>(count + 1);
if (fit < swatchSize)
{
swatchSize = fit;
}
// 色块小于可用下限时再从末尾裁剪预设,避免尺寸不可用。
const float minSwatch = 14.0f * m_scale;
while (count > 1 && swatchSize < minSwatch)
{
--count;
swatchSize = (contentWidth - swatchGap * static_cast<float>(count)) /
static_cast<float>(count + 1);
}
}
m_colorCount = std::max(0, count);
m_swatchSize = swatchSize / m_scale;
const float swatchTop = row0Top + (rowHeight - swatchSize) * 0.5f;
for (int index = 0; index < m_colorCount; ++index)
{
const float x = contentLeft + static_cast<float>(index) * (swatchSize + swatchGap);
m_colorRects[static_cast<std::size_t>(index)] = gd::RectF(x, swatchTop, swatchSize, swatchSize);
}
m_customChipRect =
gd::RectF(contentLeft + static_cast<float>(m_colorCount) * (swatchSize + swatchGap), swatchTop,
swatchSize, swatchSize);
// ---- 第二行:线宽(最细 / 数值旋钮 / 最粗)----
const float presetWidth = kWidthPresetWidth * m_scale;
const float spinButton = kWidthSpinButton * m_scale;
const float valueWidth = kWidthValueWidth * m_scale;
float groupGap = 8.0f * m_scale;
float spinnerWidth = spinButton * 2.0f + valueWidth;
float groupWidth = presetWidth * 2.0f + spinnerWidth + groupGap * 2.0f;
if (groupWidth > contentWidth)
{
const float fit = contentWidth / groupWidth;
groupGap = std::max(3.0f * m_scale, groupGap * fit);
spinnerWidth = std::max(spinButton * 2.0f + 36.0f * m_scale, spinnerWidth * fit);
groupWidth = std::min(contentWidth, presetWidth * 2.0f + spinnerWidth + groupGap * 2.0f);
}
const float row1Top = row0Top + (kStyleRowHeight + kStyleRowGap) * m_scale;
const float groupLeft = contentLeft + std::max(0.0f, (contentWidth - groupWidth) * 0.5f);
const float innerSpinButton = std::min(spinButton, spinnerWidth * 0.25f);
const float innerValueWidth = std::max(24.0f * m_scale, spinnerWidth - innerSpinButton * 2.0f);
m_widthThinRect = gd::RectF(groupLeft, row1Top, presetWidth, rowHeight);
m_widthSpinnerRect = gd::RectF(groupLeft + presetWidth + groupGap, row1Top, spinnerWidth, rowHeight);
m_widthMinusRect = gd::RectF(m_widthSpinnerRect.X, row1Top, innerSpinButton, rowHeight);
m_widthValueRect = gd::RectF(m_widthMinusRect.X + innerSpinButton, row1Top, innerValueWidth, rowHeight);
m_widthPlusRect = gd::RectF(m_widthValueRect.X + innerValueWidth, row1Top, innerSpinButton, rowHeight);
m_widthThickRect =
gd::RectF(m_widthSpinnerRect.X + spinnerWidth + groupGap, row1Top, presetWidth, rowHeight);
// ---- 第三行:样式(线段 / 散点 / 点线)----
const float row2Top = row1Top + (kStyleRowHeight + kStyleRowGap) * m_scale;
const float modeGap = 6.0f * m_scale;
const float modeWidth = (contentWidth - modeGap * static_cast<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, rowHeight);
}
// ---- 第四行:不透明度 ----
const float row3Top = row2Top + (kStyleRowHeight + kStyleRowGap) * m_scale;
const float opacityValueWidth = kOpacityValueWidth * m_scale;
const float trackWidth = std::max(40.0f, contentWidth - opacityValueWidth - 8.0f * m_scale);
m_opacityTrackRect = gd::RectF(contentLeft, row3Top + (rowHeight - kTrackHeight * m_scale) * 0.5f,
trackWidth, kTrackHeight * m_scale);
m_opacityValueRect = gd::RectF(contentLeft + trackWidth + 8.0f * m_scale, row3Top,
opacityValueWidth - 8.0f * m_scale, rowHeight);
}
void SignalPanel::Layout(const gd::RectF& area)
{
m_area = area;
m_headerRect = gd::RectF(area.X, area.Y, area.Width, kHeaderHeight * m_scale);
const float styleHeight = DesiredStyleHeight() * m_scale;
m_styleRect = gd::RectF(area.X, area.Y + area.Height - styleHeight, area.Width, styleHeight);
m_listRect = gd::RectF(area.X, m_headerRect.Y + m_headerRect.Height, area.Width,
std::max(0.0f, m_styleRect.Y - m_headerRect.Y - m_headerRect.Height));
// 标题区右侧的“全部显示/隐藏”按钮。
const float toggleWidth = 68.0f * m_scale;
const float toggleHeight = 22.0f * m_scale;
m_toggleAllRect = gd::RectF(area.X + area.Width - kPadding * m_scale - toggleWidth,
m_headerRect.Y + (m_headerRect.Height - toggleHeight) * 0.5f,
toggleWidth, toggleHeight);
// 依据内容决定是否显示滚动条,并据此收缩列表可用宽度。
m_listFullWidth = m_listRect.Width;
EnsureRows();
RefreshScrollBar();
LayoutStyleControls();
}
void SignalPanel::Paint(gd::Graphics& g)
{
if (m_theme == nullptr || m_fonts == nullptr)
{
return;
}
EnsureRows();
UiFillRect(g, m_area, m_theme->panelBg);
// 标题区。
{
std::wstring title = L"信号";
if (m_dataSet != nullptr && m_tab != nullptr)
{
title += L" · " + std::to_wstring(m_tab->VisibleCount()) + L"/" +
std::to_wstring(TotalSignalCount()) + L" 已选";
}
gd::RectF titleRect(m_headerRect.X + kPadding * m_scale, m_headerRect.Y,
std::max(20.0f, m_headerRect.Width - kPadding * 2.0f * m_scale -
m_toggleAllRect.Width - 8.0f * m_scale),
m_headerRect.Height);
UiText(g, UiEllipsize(g, title, m_fonts->uiBold, titleRect.Width), m_fonts->uiBold, titleRect,
m_theme->textStrong, TextAlign::LEFT);
const bool allVisible = AreAllSignalsVisible();
const std::wstring toggleLabel = allVisible ? L"全部隐藏" : L"全部显示";
const bool toggleHover = (m_hoverStyle == StyleHit::TOGGLE_ALL);
if (toggleHover)
{
UiFillRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->hover);
}
UiDrawRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->border, 1.0f);
const bool enabled = (m_dataSet != nullptr && !m_dataSet->Empty());
UiText(g, toggleLabel, m_fonts->uiSmall, m_toggleAllRect,
enabled ? m_theme->text : m_theme->textMuted, TextAlign::CENTER);
}
// 列表区。
{
const int saveState = g.Save();
g.SetClip(gd::RectF(m_listRect.X, m_listRect.Y, m_listRect.Width, m_listRect.Height));
if (m_dataSet == nullptr || m_dataSet->Empty())
{
UiText(g, L"尚未加载数据,请打开 CSV 文件", m_fonts->uiSmall, m_listRect, m_theme->textMuted,
TextAlign::CENTER);
}
else
{
const int rowCount = static_cast<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 std::uint32_t swatchColor = ApplyOpacity(style->color, style->opacity);
const gd::RectF swatchRect(checkRect.X + checkRect.Width + 6.0f * m_scale,
centerY - kSwatchSize * m_scale * 0.5f, kSwatchSize * m_scale,
kSwatchSize * m_scale);
UiFillRoundRect(g, swatchRect, 2.5f * m_scale,
style->visible ? swatchColor : WithAlpha(style->color, 0x50));
// 网格徽标(可点击,用于调整所属网格)。
const int laneIndex = m_tab->LaneOfSignal(ref);
const bool badgeVisible = (laneIndex >= 0) && style->visible;
const gd::RectF badgeRect = SignalBadgeRect(row);
float nameRightLimit = rect.X + rect.Width - kPadding * m_scale;
if (badgeVisible)
{
nameRightLimit = badgeRect.X - 6.0f * m_scale;
}
// 名称:完全保留文件内的列名(含 "[n]" 序号与括号描述)。
const float nameLeft = swatchRect.X + swatchRect.Width + 6.0f * m_scale;
const float nameWidth = std::max(16.0f, nameRightLimit - nameLeft);
const std::uint32_t nameColor = style->visible ? m_theme->text : m_theme->textMuted;
UiText(g, UiEllipsize(g, signal->name, m_fonts->ui, nameWidth), m_fonts->ui,
gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), nameColor, TextAlign::LEFT);
if (badgeVisible)
{
const bool badgeHot = hovered && (m_hoverPart == RowPart::LANE_BADGE);
UiFillRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
badgeHot ? m_theme->accentSoft : m_theme->cardBg);
UiDrawRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
badgeHot ? m_theme->accent : m_theme->border, 1.0f);
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
}
}
gd::RectF SignalPanel::ScrollThumbRect() const
{
if (!m_scrollBarVisible || m_contentHeight <= m_listRect.Height || m_listRect.Height <= 0.0f)
{
return gd::RectF();
}
const float ratio = std::min(1.0f, m_listRect.Height / m_contentHeight);
const float thumbHeight = std::max(24.0f * m_scale, m_scrollBarRect.Height * ratio);
const float maxScroll = m_contentHeight - m_listRect.Height;
const float progress = (maxScroll > 0.0f) ? (m_scrollOffset / maxScroll) : 0.0f;
const float thumbY = m_scrollBarRect.Y + progress * (m_scrollBarRect.Height - thumbHeight);
return gd::RectF(m_scrollBarRect.X, thumbY, m_scrollBarRect.Width, thumbHeight);
}
void SignalPanel::PaintScrollBar(gd::Graphics& g)
{
if (!m_scrollBarVisible || m_contentHeight <= m_listRect.Height)
{
return;
}
UiFillRoundRect(g, m_scrollBarRect, kScrollBarThickness * m_scale * 0.5f, m_theme->gridWeak);
UiFillRoundRect(g, ScrollThumbRect(), kScrollBarThickness * m_scale * 0.5f, m_theme->borderStrong);
}
void SignalPanel::PaintStyleSection(gd::Graphics& g)
{
if (m_styleRect.Height <= 0.0f)
{
return;
}
UiFillRect(g, m_styleRect, m_theme->panelBg);
const SignalStyle* style = (m_tab == nullptr) ? nullptr : m_tab->Style(m_tab->Selected());
if (style == nullptr || m_tab->Selected().IsValid() == false)
{
UiText(g, L"请选择信号以编辑样式", m_fonts->uiSmall, m_styleRect, m_theme->textMuted,
TextAlign::CENTER);
return;
}
const float labelX = m_styleRect.X + kStylePadH * m_scale;
const float labelWidth = kStyleLabelWidth * m_scale;
const float rowHeight = kStyleRowHeight * m_scale;
// ---- 第一行:颜色 ----
{
UiText(g, L"颜色", m_fonts->uiSmall, gd::RectF(labelX, m_customChipRect.Y, labelWidth, rowHeight),
m_theme->textMuted, TextAlign::LEFT);
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
bool presetActive = false;
for (int index = 0; index < m_colorCount; ++index)
{
const gd::RectF& rect = m_colorRects[static_cast<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);
presetActive = presetActive || active;
UiFillRoundRect(g, rect, kCorner * m_scale, color);
if (active || hovered)
{
const gd::RectF ring(rect.X - 2.0f * m_scale, rect.Y - 2.0f * m_scale,
rect.Width + 4.0f * m_scale, rect.Height + 4.0f * m_scale);
UiDrawRoundRect(g, ring, 5.0f * m_scale,
active ? m_theme->textStrong : m_theme->borderStrong,
active ? 1.6f : 1.0f);
}
}
// 自定义颜色入口:与预设色块完全同尺寸、同外框样式的小方格,仅多一个加号图标。
const bool chipHot = (m_hoverStyle == StyleHit::CUSTOM_CHIP);
const bool chipActive = !presetActive && (m_colorPicker != nullptr) && m_colorPicker->IsOpen();
UiFillRoundRect(g, m_customChipRect, kCorner * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, m_customChipRect, kCorner * m_scale, m_theme->borderStrong, 1.0f);
UiDrawPlusIcon(g, m_customChipRect, chipHot ? m_theme->accent : m_theme->text,
1.6f * m_scale);
// 当前颜色不在预设中时,用与预设一致的外框指示“正在使用自定义颜色”。
if (!presetActive || chipHot)
{
const gd::RectF& chip = m_customChipRect;
const gd::RectF ring(chip.X - 2.0f * m_scale, chip.Y - 2.0f * m_scale,
chip.Width + 4.0f * m_scale, chip.Height + 4.0f * m_scale);
UiDrawRoundRect(g, ring, 5.0f * m_scale,
!presetActive ? m_theme->textStrong : m_theme->borderStrong,
!presetActive ? 1.6f : 1.0f);
}
(void)chipActive;
}
// ---- 第二行:线宽 ----
{
UiText(g, L"线宽", m_fonts->uiSmall,
gd::RectF(labelX, m_widthThinRect.Y, labelWidth, rowHeight), m_theme->textMuted,
TextAlign::LEFT);
const gd::RectF presetRects[2] = {m_widthThinRect, m_widthThickRect};
const float presetWidths[2] = {kWidthThinPreset, kWidthThickPreset};
for (int index = 0; index < 2; ++index)
{
const gd::RectF& rect = presetRects[index];
const bool active = std::fabs(style->lineWidth - presetWidths[index]) < 0.01f;
const bool hovered =
(m_hoverStyle == StyleHit::WIDTH_PRESET && m_hoverStyleIndex == index);
UiFillRoundRect(g, rect, kCorner * m_scale,
active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg));
UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f);
const float centerY = rect.Y + rect.Height * 0.5f;
UiDrawLine(g, rect.X + 9.0f * m_scale, centerY, rect.X + rect.Width - 9.0f * m_scale,
centerY, active ? m_theme->accent : m_theme->text,
presetWidths[index] * m_scale);
}
// 数值旋钮(线宽输入框)。
const bool valueHot = (m_hoverStyle == StyleHit::WIDTH_VALUE);
UiFillRoundRect(g, m_widthSpinnerRect, kCorner * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, m_widthSpinnerRect, kCorner * m_scale,
m_widthEditing ? m_theme->accent
: (valueHot ? m_theme->borderStrong : m_theme->border),
m_widthEditing ? 1.6f : 1.0f);
if (m_widthEditing)
{
const std::wstring editText =
m_widthEditText.empty() ? std::wstring(L" ") : m_widthEditText;
const float textWidth = UiTextWidth(g, editText, m_fonts->monoSmall);
const float caretX =
m_widthValueRect.X + m_widthValueRect.Width - 6.0f * m_scale - textWidth;
UiText(g, editText + L" px", m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong,
TextAlign::RIGHT);
if (caretX > m_widthValueRect.X)
{
const float caretY = m_widthValueRect.Y + 5.0f * m_scale;
UiDrawLine(g, caretX, caretY, caretX, m_widthValueRect.Y + rowHeight - 5.0f * m_scale,
m_theme->accent, 1.4f * m_scale);
}
}
else
{
UiText(g, FormatNumber(style->lineWidth, 1) + L" px", m_fonts->monoSmall, m_widthValueRect,
m_theme->textStrong, TextAlign::CENTER);
}
const bool minusHot = (m_hoverStyle == StyleHit::WIDTH_MINUS);
const bool plusHot = (m_hoverStyle == StyleHit::WIDTH_PLUS);
const bool minusEnabled = style->lineWidth > (kWidthMin + 1.0e-4f);
const bool plusEnabled = style->lineWidth < (kWidthMax - 1.0e-4f);
if (minusHot && minusEnabled)
{
UiFillRoundRect(g, m_widthMinusRect, kCorner * m_scale, m_theme->hover);
}
if (plusHot && plusEnabled)
{
UiFillRoundRect(g, m_widthPlusRect, kCorner * m_scale, m_theme->hover);
}
UiDrawArrowIcon(g, m_widthMinusRect, minusEnabled ? m_theme->text : m_theme->textMuted,
1.6f * m_scale, IconDirection::LEFT);
UiDrawArrowIcon(g, m_widthPlusRect, plusEnabled ? m_theme->text : m_theme->textMuted,
1.6f * m_scale, IconDirection::RIGHT);
}
// ---- 第三行:样式 ----
{
UiText(g, L"样式", m_fonts->uiSmall, gd::RectF(labelX, m_modeRects[0].Y, labelWidth, rowHeight),
m_theme->textMuted, TextAlign::LEFT);
for (int index = 0; index < kModeOptionCount; ++index)
{
const gd::RectF& rect = m_modeRects[static_cast<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_opacityValueRect.Y, labelWidth, rowHeight), m_theme->textMuted,
TextAlign::LEFT);
const bool active = m_draggingOpacity || (m_hoverStyle == StyleHit::OPACITY);
DrawSlider(g, m_opacityTrackRect, static_cast<double>(style->opacity), m_theme->accent, active,
m_scale);
const int percent = static_cast<int>(style->opacity * 100.0f + 0.5f);
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect,
m_theme->textStrong, TextAlign::RIGHT);
}
}
bool SignalPanel::AreAllSignalsVisible() const
{
if (m_dataSet == nullptr || m_tab == nullptr || m_dataSet->Empty())
{
return false;
}
return m_tab->VisibleCount() == TotalSignalCount();
}
bool SignalPanel::StepLineWidth(float delta)
{
if (m_tab == nullptr)
{
return false;
}
SignalStyle* style = m_tab->Style(m_tab->Selected());
if (style == nullptr)
{
return false;
}
float value = ClampFloat(style->lineWidth + delta, kWidthMin, kWidthMax);
value = std::round(value * 10.0f) / 10.0f;
if (std::fabs(value - style->lineWidth) < 0.001f)
{
return false;
}
style->lineWidth = value;
return true;
}
bool SignalPanel::BeginWidthEdit()
{
if (m_tab == nullptr)
{
return false;
}
const SignalStyle* style = m_tab->Style(m_tab->Selected());
if (style == nullptr)
{
return false;
}
if (m_widthEditing)
{
return false;
}
m_widthEditText = FormatNumber(style->lineWidth, 1);
m_widthEditing = true;
m_widthEditFresh = true;
return true;
}
bool SignalPanel::EndWidthEdit(bool commit)
{
if (!m_widthEditing)
{
return false;
}
bool changed = true;
if (commit && m_tab != nullptr)
{
const std::wstring trimmed = Trim(m_widthEditText);
if (!trimmed.empty())
{
wchar_t* stop = nullptr;
const double parsed = std::wcstod(trimmed.c_str(), &stop);
if (stop != trimmed.c_str() && parsed == parsed)
{
if (SignalStyle* style = m_tab->Style(m_tab->Selected()))
{
const float value = ClampFloat(static_cast<float>(parsed), kWidthMin, kWidthMax);
style->lineWidth = std::round(value * 10.0f) / 10.0f;
}
}
}
}
m_widthEditing = false;
m_widthEditFresh = false;
m_widthEditText.clear();
return changed;
}
bool SignalPanel::OnEditChar(wchar_t character)
{
if (!m_widthEditing)
{
return false;
}
if (character == L'\r' || character == L'\n')
{
return EndWidthEdit(true);
}
if (character == L'\b')
{
m_widthEditFresh = false;
if (!m_widthEditText.empty())
{
m_widthEditText.pop_back();
}
return true;
}
if (character >= L'0' && character <= L'9')
{
// 首次键入直接替换原有数值,无需先手动删除。
if (m_widthEditFresh)
{
m_widthEditText.clear();
m_widthEditFresh = false;
}
if (m_widthEditText.size() < 5)
{
m_widthEditText.push_back(character);
}
return true;
}
if (character == L'.')
{
if (m_widthEditFresh)
{
m_widthEditText.clear();
m_widthEditFresh = false;
}
// 只允许一个小数点。
if (m_widthEditText.find(L'.') == std::wstring::npos && m_widthEditText.size() < 4)
{
m_widthEditText.push_back(character);
}
return true;
}
return true;
}
bool SignalPanel::OnEditKey(WPARAM key)
{
if (!m_widthEditing)
{
return false;
}
switch (key)
{
case VK_RETURN:
return EndWidthEdit(true);
case VK_ESCAPE:
return EndWidthEdit(false);
case VK_BACK:
m_widthEditFresh = false;
if (!m_widthEditText.empty())
{
m_widthEditText.pop_back();
}
return true;
default:
break;
}
return true;
}
bool SignalPanel::ApplyColor(std::uint32_t color)
{
if (m_tab == nullptr)
{
return false;
}
SignalStyle* style = m_tab->Style(m_tab->Selected());
if (style == nullptr)
{
return false;
}
if (style->color == color)
{
return false;
}
style->color = color;
return true;
}
bool SignalPanel::ApplyOpacityFromX(float x)
{
if (m_tab == nullptr)
{
return false;
}
SignalStyle* style = m_tab->Style(m_tab->Selected());
if (style == nullptr)
{
return false;
}
const float value = static_cast<float>(UiSliderRatioFromX(m_opacityTrackRect, x));
if (std::fabs(value - style->opacity) < 0.005f)
{
return false;
}
style->opacity = value;
return true;
}
bool SignalPanel::OpenColorPickerPopup(const SignalStyle& style)
{
if (m_colorPicker == nullptr)
{
return false;
}
const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
m_colorPicker->Open(bounds, static_cast<int>(m_customChipRect.X),
static_cast<int>(m_customChipRect.Y + m_customChipRect.Height), style.color);
return true;
}
bool SignalPanel::CloseTransientUi()
{
bool changed = false;
if (m_widthEditing)
{
changed = EndWidthEdit(true) || changed;
}
if (m_colorPicker != nullptr && m_colorPicker->IsOpen())
{
m_colorPicker->Close();
changed = true;
}
return changed;
}
bool SignalPanel::OnMouseMove(int x, int y)
{
// 拖拽优先级最高。
if (m_draggingOpacity)
{
return ApplyOpacityFromX(static_cast<float>(x));
}
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 && UiPointInRect(m_customChipRect, fx, fy))
{
hoverStyle = StyleHit::CUSTOM_CHIP;
}
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthThinRect, fx, fy))
{
hoverStyle = StyleHit::WIDTH_PRESET;
hoverStyleIndex = 0;
}
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthThickRect, fx, fy))
{
hoverStyle = StyleHit::WIDTH_PRESET;
hoverStyleIndex = 1;
}
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthMinusRect, fx, fy))
{
hoverStyle = StyleHit::WIDTH_MINUS;
}
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthPlusRect, fx, fy))
{
hoverStyle = StyleHit::WIDTH_PLUS;
}
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthValueRect, fx, fy))
{
hoverStyle = StyleHit::WIDTH_VALUE;
}
if (hoverStyle == StyleHit::NONE)
{
for (int index = 0; index < kModeOptionCount; ++index)
{
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
{
hoverStyle = StyleHit::MODE;
hoverStyleIndex = index;
break;
}
}
}
if (hoverStyle == StyleHit::NONE &&
UiPointInRect(SliderHitRect(m_opacityTrackRect, m_scale), fx, fy))
{
hoverStyle = StyleHit::OPACITY;
}
}
if (hoverRow == m_hoverRow && hoverStyle == m_hoverStyle && hoverStyleIndex == m_hoverStyleIndex &&
part == m_hoverPart)
{
return false;
}
m_hoverRow = hoverRow;
m_hoverPart = part;
m_hoverStyle = hoverStyle;
m_hoverStyleIndex = hoverStyleIndex;
return true;
}
bool SignalPanel::OnLButtonDown(int x, int y)
{
if (m_dataSet == nullptr || m_tab == nullptr)
{
return false;
}
const float fx = static_cast<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;
}
m_pendingLaneMenu = false;
m_pendingColorPicker = false;
// 点击样式区以外的位置时结束线宽输入。
if (m_widthEditing && !UiPointInRect(m_widthValueRect, fx, fy))
{
EndWidthEdit(true);
}
// 全部显示/隐藏。
if (UiPointInRect(m_toggleAllRect, fx, fy))
{
m_tab->SetAllVisible(*m_dataSet, !AreAllSignalsVisible());
m_rowsDirty = true;
return true;
}
// 样式编辑区。
if (SignalStyle* style = m_tab->Style(m_tab->Selected()))
{
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
for (int index = 0; index < m_colorCount; ++index)
{
if (UiPointInRect(m_colorRects[static_cast<std::size_t>(index)], fx, fy))
{
style->color = palette[index % paletteCount];
return true;
}
}
if (UiPointInRect(m_customChipRect, fx, fy))
{
// 在左键抬起时展开,避免按下即被后续消息立即收起。
m_pendingColorPicker = true;
return true;
}
if (UiPointInRect(m_widthThinRect, fx, fy))
{
style->lineWidth = kWidthThinPreset;
return true;
}
if (UiPointInRect(m_widthThickRect, fx, fy))
{
style->lineWidth = kWidthThickPreset;
return true;
}
if (UiPointInRect(m_widthMinusRect, fx, fy))
{
return StepLineWidth(-kWidthStep);
}
if (UiPointInRect(m_widthPlusRect, fx, fy))
{
return StepLineWidth(kWidthStep);
}
if (UiPointInRect(m_widthValueRect, fx, fy))
{
// 单击即进入输入状态,双击亦等效。
return BeginWidthEdit();
}
for (int index = 0; index < kModeOptionCount; ++index)
{
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
{
style->mode = kModeOptions[index];
return true;
}
}
if (UiPointInRect(SliderHitRect(m_opacityTrackRect, m_scale), fx, fy))
{
m_draggingOpacity = true;
return ApplyOpacityFromX(fx) || true;
}
}
// 列表行。
RowPart part = RowPart::NONE;
const int row = HitRow(x, y, part);
if (row < 0)
{
return false;
}
const ListRow& item = m_rows[static_cast<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)
{
// 在左键抬起时弹出,保证菜单不会随着按键松开而消失。
m_pendingLaneMenu = true;
m_pendingLaneRef = ref;
}
return true;
}
bool SignalPanel::OnLButtonUp(int x, int y)
{
bool changed = false;
if (m_draggingOpacity)
{
m_draggingOpacity = false;
changed = true;
}
if (m_draggingScrollBar)
{
m_draggingScrollBar = false;
changed = true;
}
if (m_pendingColorPicker)
{
m_pendingColorPicker = false;
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
if (UiPointInRect(m_customChipRect, fx, fy) && m_tab != nullptr)
{
if (const SignalStyle* style = m_tab->Style(m_tab->Selected()))
{
changed = OpenColorPickerPopup(*style) || changed;
}
}
}
if (m_pendingLaneMenu)
{
m_pendingLaneMenu = false;
RowPart part = RowPart::NONE;
const int row = HitRow(x, y, part);
if (row >= 0 && part == RowPart::LANE_BADGE)
{
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
const SignalRef ref{item.fileIndex, item.signalIndex};
if (ref == m_pendingLaneRef)
{
const gd::RectF badge = SignalBadgeRect(row);
OpenLaneMenu(ref, static_cast<int>(badge.X),
static_cast<int>(badge.Y + badge.Height));
changed = true;
}
}
}
return changed;
}
bool SignalPanel::OnDoubleClick(int x, int y)
{
if (m_dataSet == nullptr || m_tab == nullptr)
{
return false;
}
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
// 线宽数值框:双击进入(或保持)输入状态。
if (UiPointInRect(m_widthValueRect, fx, fy))
{
return BeginWidthEdit();
}
RowPart part = RowPart::NONE;
const int row = HitRow(x, y, part);
if (row < 0)
{
return false;
}
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
if (item.kind == RowKind::FILE)
{
m_tab->ToggleCollapsed(item.fileIndex);
m_rowsDirty = true;
return true;
}
// 网格徽标单击已可打开菜单,双击不再切换可见性。
if (part == RowPart::LANE_BADGE)
{
return false;
}
const SignalRef ref{item.fileIndex, item.signalIndex};
m_tab->SetSelected(ref);
m_tab->ToggleVisible(*m_dataSet, ref);
m_rowsDirty = true;
return true;
}
bool SignalPanel::OnRButtonDown(int x, int y)
{
if (m_dataSet == nullptr || m_tab == nullptr)
{
return false;
}
RowPart part = RowPart::NONE;
const int row = HitRow(x, y, part);
if (row < 0)
{
return false;
}
const ListRow& item = m_rows[static_cast<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