Files
Just-DataInspector/Components/SignalPanel/Src/SignalPanel.cpp
T
DeepSeek V4.1 Flash dbe9b904ab fix: 重构样式区交互与布局,修复拾色浮窗穿透与自定义颜色记忆
样式编辑区统一为固定四行网格(颜色 / 样式 / 透明度 / 线宽):
- 四行共用同一内容列宽度,宽度带上下限(面板拉宽时不再无限延伸),
  标签列与内容列作为整体在面板内居中,行标签统一按整行垂直居中,
  修复“颜色”字样与色块存在高低差的问题。
- 颜色行的色块间距按剩余空间自适应,使该行与其它三行等宽对齐。
- 线宽改为与透明度同款的滑块(0.5–10 px),从原先的“预设 + 数值旋钮”
  调整为置于样式区最下方的滑块,保留双击数值直接键入的能力。

自定义颜色浮窗(ColorPickerPopup):
- 拖动滑块时不再与下方图层(尤其是绘图区平移/框选)产生交互:
  拾色浮窗内的按下会被完整消费并抓取鼠标,不再下传。
- 打开浮窗时光标正确显示为横向调整(IDC_SIZEWE),其余区域为箭头。
- 新增自定义颜色记忆:SignalStyle 记录 customColor,从自定义色切换到
  预设色后再次点击加号方格即可直接回到上次的自定义颜色;
  加号方格始终以该颜色填充(与预设色块同尺寸、同外框)。
- 打开浮窗时以记忆色为初始取值,不再重新从预设色起步。

透明度:
- 预设色块与自定义颜色方格同样叠加不透明度显示,
  修复此前透明度只影响自定义颜色预览的问题。

其他:
- 左侧面板宽度上限由 560 收紧至 400,避免样式控件被拉得过长。
- 移除只写不读的色块尺寸缓存与已废弃的线宽步进接口。

验证:真实鼠标输入逐项核对(拾色浮窗拖拽后绘图区视图范围与时间轴完全
不变、滑块光标 SIZEWE、自定义色 #0071C4 在切换预设后仍可一键恢复、
透明度 21% 时全部预设色块同步变淡、线宽滑块 8.1 px / 双击键入 5 设为
5.0 px、面板最窄与最宽两种极限下四行均不溢出且等宽)。
make clean && make all 零错误零警告。
2026-09-29 18:06:34 +08:00

1883 lines
56 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 kStyleLabelGap = 8.0f; /**< 标签列与内容列间距。 */
const float kStyleRowCount = 4.0f; /**< 样式区固定行数。 */
const float kStyleContentMax = 260.0f; /**< 内容列最大宽度(再拉宽面板也不再延伸)。 */
const float kSwatchEdge = 18.0f; /**< 颜色色块边长上限(含自定义颜色入口)。 */
const float kSwatchMin = 11.0f; /**< 颜色色块边长下限。 */
const float kSwatchGap = 4.0f; /**< 颜色色块最小间距。 */
const float kSwatchMaxGap = 10.0f; /**< 颜色色块最大间距(用于铺满内容列)。 */
const float kSliderValueWidth = 52.0f; /**< 滑块行数值文本宽度(透明度与线宽一致)。 */
const float kSliderValueGap = 8.0f; /**< 滑块轨道与数值文本间距。 */
const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */
const float kWidthMin = 0.5f; /**< 允许的最小线宽(px)。 */
const float kWidthMax = 10.0f; /**< 允许的最大线宽(px)。 */
/** @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] color 0xAARRGGBB 颜色值。
* @return 相对亮度低于阈值返回 true。
*/
bool IsDarkColor(std::uint32_t color)
{
const double r = static_cast<double>((color >> 16) & 0xFFu);
const double g = static_cast<double>((color >> 8) & 0xFFu);
const double b = static_cast<double>(color & 0xFFu);
return (0.299 * r + 0.587 * g + 0.114 * b) < 140.0;
}
/**
* @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_draggingWidth = 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 padH = kStylePadH * m_scale;
const float labelWidth = kStyleLabelWidth * m_scale;
const float labelGap = kStyleLabelGap * m_scale;
const float rowHeight = kStyleRowHeight * m_scale;
const float rowStep = (kStyleRowHeight + kStyleRowGap) * m_scale;
const float firstTop = m_styleRect.Y + kStylePadV * m_scale;
// 内容列宽度带上下限:面板变窄时收缩,面板变宽时不再延伸,四行始终共用同一宽度。
const float available = std::max(40.0f, m_styleRect.Width - padH * 2.0f - labelWidth - labelGap);
const float contentWidth = std::min(available, kStyleContentMax * m_scale);
// 标签列 + 内容列作为整体在面板内居中,避免拉宽后控件偏向一侧。
const float groupWidth = labelWidth + labelGap + contentWidth;
const float groupLeft = m_styleRect.X + std::max(padH, (m_styleRect.Width - groupWidth) * 0.5f);
const float contentLeft = groupLeft + labelWidth + labelGap;
m_styleContentRect = gd::RectF(contentLeft, firstTop, contentWidth,
kStyleRowCount * rowHeight + (kStyleRowCount - 1.0f) *
(kStyleRowGap * m_scale));
for (int index = 0; index < static_cast<int>(kStyleRowCount); ++index)
{
m_styleRowRects[static_cast<std::size_t>(index)] =
gd::RectF(groupLeft, firstTop + static_cast<float>(index) * rowStep, groupWidth, rowHeight);
}
const gd::RectF& colorRow = m_styleRowRects[0];
const gd::RectF& modeRow = m_styleRowRects[1];
const gd::RectF& opacityRow = m_styleRowRects[2];
const gd::RectF& widthRow = m_styleRowRects[3];
// ---- 颜色行:预设色块 + 同尺寸自定义颜色入口(合计恰好铺满内容列宽度)----
int paletteCount = 0;
SignalPalette(paletteCount);
const float swatchGap = kSwatchGap * m_scale;
const float swatchMaxGap = kSwatchMaxGap * m_scale;
const float swatchMin = kSwatchMin * m_scale;
int count = std::min(paletteCount, static_cast<int>(m_colorRects.size()));
float swatchSize = kSwatchEdge * m_scale;
float gap = swatchGap;
while (count >= 1)
{
const float slots = static_cast<float>(count + 1);
const float fit = (contentWidth - swatchGap * (slots - 1.0f)) / slots;
if (count == 1 || fit >= swatchMin)
{
swatchSize = std::max(swatchMin, std::min(kSwatchEdge * m_scale, fit));
// 用实际色块尺寸反算间距,让颜色行与透明度、线宽、样式各行同宽。
gap = (contentWidth - swatchSize * slots) / static_cast<float>(count);
gap = std::max(swatchGap, std::min(gap, swatchMaxGap));
break;
}
--count;
}
m_colorCount = std::max(1, count);
// 若受色块尺寸上限约束仍有剩余空间,则整行在内容列内居中。
const float colorRowWidth = swatchSize * static_cast<float>(m_colorCount + 1) +
gap * static_cast<float>(m_colorCount);
const float colorRowLeft = contentLeft + std::max(0.0f, (contentWidth - colorRowWidth) * 0.5f);
const float swatchTop = colorRow.Y + (rowHeight - swatchSize) * 0.5f;
for (int index = 0; index < m_colorCount; ++index)
{
m_colorRects[static_cast<std::size_t>(index)] =
gd::RectF(colorRowLeft + static_cast<float>(index) * (swatchSize + gap), swatchTop,
swatchSize, swatchSize);
}
m_customChipRect =
gd::RectF(colorRowLeft + static_cast<float>(m_colorCount) * (swatchSize + gap), swatchTop,
swatchSize, swatchSize);
// ---- 样式行:三个等宽按钮铺满内容列 ----
const float modeGap = 6.0f * m_scale;
const float modeWidth =
std::max(20.0f, (contentWidth - modeGap * static_cast<float>(kModeOptionCount - 1)) /
static_cast<float>(kModeOptionCount));
for (int index = 0; index < kModeOptionCount; ++index)
{
m_modeRects[static_cast<std::size_t>(index)] =
gd::RectF(contentLeft + static_cast<float>(index) * (modeWidth + modeGap), modeRow.Y,
modeWidth, rowHeight);
}
// ---- 透明度行与线宽行:几何完全一致,保证纵向宽度对齐 ----
const float valueWidth = std::min(kSliderValueWidth * m_scale, contentWidth * 0.4f);
const float trackWidth = std::max(24.0f, contentWidth - valueWidth - kSliderValueGap * m_scale);
const float trackHeight = kTrackHeight * m_scale;
m_opacityTrackRect = gd::RectF(contentLeft, opacityRow.Y + (rowHeight - trackHeight) * 0.5f,
trackWidth, trackHeight);
m_opacityValueRect = gd::RectF(contentLeft + trackWidth + kSliderValueGap * m_scale, opacityRow.Y,
valueWidth, rowHeight);
m_widthTrackRect =
gd::RectF(contentLeft, widthRow.Y + (rowHeight - trackHeight) * 0.5f, trackWidth, trackHeight);
m_widthValueRect = gd::RectF(contentLeft + trackWidth + kSliderValueGap * m_scale, widthRow.Y,
valueWidth, 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 labelWidth = kStyleLabelWidth * m_scale;
const float rowHeight = kStyleRowHeight * m_scale;
// 行标签统一以整行为基准垂直居中,确保与同行控件纵向对齐。
auto paintRowLabel = [&](int rowIndex, const wchar_t* text)
{
const gd::RectF& row = m_styleRowRects[static_cast<std::size_t>(rowIndex)];
UiText(g, text, m_fonts->uiSmall, gd::RectF(row.X, row.Y, labelWidth, row.Height),
m_theme->textMuted, TextAlign::LEFT);
};
// ---- 第一行:颜色(预设色块 + 自定义颜色入口)----
{
paintRowLabel(0, L"颜色");
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
// 当自定义颜色与某个预设色恰好相同时,优先视为自定义颜色,避免两处同时高亮。
const bool customActive = (style->color == style->customColor);
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 = !customActive && (style->color == color);
// 预设色块同样叠加不透明度,直观反映实际显示效果。
UiFillRoundRect(g, rect, kCorner * m_scale, ApplyOpacity(color, style->opacity));
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 std::uint32_t chipColor = ApplyOpacity(style->customColor, style->opacity);
const std::uint32_t iconColor = IsDarkColor(chipColor) ? 0xFFFDF6E3u : 0xFF073642u;
UiFillRoundRect(g, m_customChipRect, kCorner * m_scale, chipColor);
UiDrawRoundRect(g, m_customChipRect, kCorner * m_scale,
customActive ? m_theme->textStrong : (chipHot ? m_theme->accent : m_theme->borderStrong),
customActive ? 1.6f : 1.0f);
UiDrawPlusIcon(g, m_customChipRect, iconColor, 1.6f * m_scale);
// 当前正在使用自定义颜色、或鼠标悬停时,显示与预设色块一致的外框。
if (customActive || 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,
customActive ? m_theme->textStrong : m_theme->borderStrong,
customActive ? 1.6f : 1.0f);
}
}
// ---- 第二行:样式(线段 / 散点 / 点线)----
{
paintRowLabel(1, L"样式");
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);
}
}
// ---- 第三行:透明度 ----
{
paintRowLabel(2, L"透明度");
const bool active = m_draggingOpacity || (m_hoverStyle == StyleHit::OPACITY_SLIDER);
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);
}
// ---- 第四行:线宽(与透明度行几何一致)----
{
paintRowLabel(3, L"线宽");
const double ratio =
(static_cast<double>(style->lineWidth) - kWidthMin) / (kWidthMax - kWidthMin);
const bool active = m_draggingWidth || (m_hoverStyle == StyleHit::WIDTH_SLIDER);
DrawSlider(g, m_widthTrackRect, ratio, m_theme->accent, active, m_scale);
if (m_widthEditing)
{
// 输入状态:右对齐并显示插入符,便于用户看到键入内容。
const std::wstring editText = m_widthEditText.empty() ? std::wstring(L" ") : m_widthEditText;
UiFillRoundRect(g, m_widthValueRect, kCorner * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, m_widthValueRect, kCorner * m_scale, m_theme->accent, 1.6f);
const float textWidth = UiTextWidth(g, editText, m_fonts->monoSmall);
UiText(g, editText, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong,
TextAlign::RIGHT);
const float caretX = m_widthValueRect.X + m_widthValueRect.Width - 6.0f * m_scale - textWidth;
if (caretX > m_widthValueRect.X)
{
UiDrawLine(g, caretX, m_widthValueRect.Y + 5.0f * m_scale, caretX,
m_widthValueRect.Y + rowHeight - 5.0f * m_scale, m_theme->accent,
1.4f * m_scale);
}
}
else
{
const std::wstring numberText = FormatNumber(style->lineWidth, 1);
const std::wstring fullText = numberText + L" px";
// 空间不足时省略单位,保证数值本身完整可读。
const std::wstring shown =
(UiTextWidth(g, fullText, m_fonts->monoSmall) > m_widthValueRect.Width - 6.0f * m_scale)
? numberText
: fullText;
UiText(g, shown, m_fonts->monoSmall, m_widthValueRect, 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::ApplyWidthFromX(float x)
{
if (m_tab == nullptr)
{
return false;
}
SignalStyle* style = m_tab->Style(m_tab->Selected());
if (style == nullptr)
{
return false;
}
const double ratio = UiSliderRatioFromX(m_widthTrackRect, x);
float value = static_cast<float>(kWidthMin + ratio * (kWidthMax - kWidthMin));
value = std::round(value * 10.0f) / 10.0f;
value = ClampFloat(value, kWidthMin, kWidthMax);
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;
}
// 同时记住最后一次自定义取值,便于切换到预设色后一键回到该颜色。
const bool changed = (style->color != color) || (style->customColor != color);
style->color = color;
style->customColor = color;
return changed;
}
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, m_customChipRect, style.customColor);
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_draggingWidth)
{
return ApplyWidthFromX(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)
{
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_SLIDER;
}
if (hoverStyle == StyleHit::NONE &&
UiPointInRect(SliderHitRect(m_widthTrackRect, m_scale), fx, fy))
{
hoverStyle = StyleHit::WIDTH_SLIDER;
}
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthValueRect, fx, fy))
{
hoverStyle = StyleHit::WIDTH_VALUE;
}
}
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))
{
// 先恢复到记忆的自定义颜色(即便随后收起浮窗也会生效),再在抬起时展开浮窗。
style->color = style->customColor;
m_pendingColorPicker = true;
return true;
}
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;
ApplyOpacityFromX(fx);
return true;
}
if (UiPointInRect(SliderHitRect(m_widthTrackRect, m_scale), fx, fy))
{
m_draggingWidth = true;
ApplyWidthFromX(fx);
return true;
}
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)
{
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_draggingWidth)
{
m_draggingWidth = 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