样式编辑区: - 移除顶部「样式 + 信号名 + 文件名」标题行,选中信号即代表编辑目标。 - 绘制方式取消阶梯选项,仅保留线段/散点/点线,标签由「方式」改为「样式」。 - 线宽改为「最细预设 + 数值旋钮 + 最粗预设」:中间为带左右增减箭头的输入框, 步进 0.2px,取值范围 0.5-6.0px,并标注单位为 px。 - 颜色在 10 个预设色块后新增带加号图标的方格,点击展开 RGB 三通道滑块面板, 可实时预览并显示十六进制值;点击面板外任意位置收起。 - 移除网格归属下拉框,改由每条信号行末尾的 G 徽标承载,点击弹出网格归属菜单。 信号面板与菜单: - 文件分组行新增关闭按钮,可直接从工作区移除该文件,与整体风格保持一致。 - 新增 PopupMenu 组件:绘制在窗口后备缓冲区内的自绘菜单,含圆角、投影、悬停高亮、 单选对勾、色块行与分组标题,彻底替换系统默认弹出菜单; 菜单打开期间接管鼠标与键盘(Esc / 上下键 / 回车),失焦或在菜单外释放时自动关闭。 - 信号、文件、网格、标签页四类右键菜单全部改用该组件。 其他修复: - 信号名称不再保留尾部单位括号,避免名称后再重复显示单位。 - 数据模型移除 DrawMode::STEP 枚举。 - 标签栏与信号面板弹出菜单后补充重绘请求,修复菜单不显示的问题。
1737 lines
54 KiB
C++
1737 lines
54 KiB
C++
#include "SignalPanel.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
#include "Utf8Util.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
namespace
|
||
{
|
||
const float kHeaderHeight = 38.0f; /**< 标题区高度(逻辑像素)。 */
|
||
const float kFileRowHeight = 30.0f; /**< 文件分组行高。 */
|
||
const float kSignalRowHeight = 27.0f; /**< 信号行高。 */
|
||
const float kStyleBaseHeight = 124.0f; /**< 样式编辑区基础高度。 */
|
||
const float kColorPickerHeight = 132.0f;/**< 自定义颜色面板高度。 */
|
||
const float kRowHeight = 26.0f; /**< 样式区单行高度。 */
|
||
const float kRowGap = 10.0f; /**< 样式区行间距。 */
|
||
const float kStylePadTop = 12.0f; /**< 样式区上内边距。 */
|
||
const float kStylePadH = 12.0f; /**< 面板左右内边距。 */
|
||
const float kPadding = 12.0f; /**< 通用内边距。 */
|
||
const float kCheckSize = 15.0f; /**< 复选框尺寸。 */
|
||
const float kSwatchSize = 12.0f; /**< 信号行颜色小方块尺寸。 */
|
||
const float kBadgeWidth = 30.0f; /**< 网格徽标宽度。 */
|
||
const float kBadgeHeight = 17.0f; /**< 网格徽标高度。 */
|
||
const float kCorner = 4.0f; /**< 通用圆角半径。 */
|
||
const float kLineLabelWidth = 40.0f; /**< 样式行标签列宽度。 */
|
||
const float kDisclosureWidth = 16.0f; /**< 折叠三角占位宽度。 */
|
||
const float kIndent = 20.0f; /**< 信号行相对文件行的缩进。 */
|
||
const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */
|
||
const float kCloseButtonSize = 18.0f; /**< 文件行关闭按钮尺寸。 */
|
||
const float kSliderLabelWidth = 18.0f; /**< 滑块行标签列宽度。 */
|
||
const float kSliderValueWidth = 42.0f; /**< 滑块数值显示宽度。 */
|
||
const float kSliderRowHeight = 24.0f; /**< 滑块行高。 */
|
||
const float kSliderGap = 4.0f; /**< 滑块行间距。 */
|
||
|
||
const float kWidthMin = 0.5f; /**< 允许的最小线宽。 */
|
||
const float kWidthMax = 6.0f; /**< 允许的最大线宽。 */
|
||
const float kWidthStep = 0.2f; /**< 线宽增减步进。 */
|
||
const float kWidthThinPreset = 1.0f; /**< 最细线宽预设值。 */
|
||
const float kWidthThickPreset = 3.0f; /**< 最粗线宽预设值。 */
|
||
|
||
/** @brief 可选绘制样式顺序(已取消阶梯方式)。 */
|
||
const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER};
|
||
const int kModeOptionCount = static_cast<int>(sizeof(kModeOptions) / sizeof(kModeOptions[0]));
|
||
|
||
/**
|
||
* @brief 计算滑块的便捷命中矩形(比轨道略大,便于拖拽)。
|
||
* @param [IN] track 轨道矩形。
|
||
* @param [IN] scale 缩放比例。
|
||
* @return 命中矩形。
|
||
*/
|
||
gd::RectF SliderHitRect(const gd::RectF& track, float scale)
|
||
{
|
||
return gd::RectF(track.X - 4.0f * scale, track.Y - 9.0f * scale, track.Width + 8.0f * scale,
|
||
track.Height + 18.0f * scale);
|
||
}
|
||
|
||
/**
|
||
* @brief 绘制水平滑块(轨道 + 已填充部分 + 圆钮)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] track 轨道矩形。
|
||
* @param [IN] ratio 当前值比例(0-1)。
|
||
* @param [IN] fillColor 已填充部分颜色。
|
||
* @param [IN] hovered 是否处于悬停状态。
|
||
* @param [IN] scale 缩放比例。
|
||
*/
|
||
void DrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
||
bool hovered, float scale)
|
||
{
|
||
const float radius = track.Height * 0.5f;
|
||
UiFillRoundRect(g, track, radius, 0x2A657B83u);
|
||
|
||
ratio = std::max(0.0, std::min(1.0, ratio));
|
||
const float fillWidth = std::max(track.Height, static_cast<float>(ratio) * track.Width);
|
||
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
|
||
|
||
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
|
||
const float knobRadius = radius + (hovered ? 2.5f * scale : 1.0f * scale);
|
||
|
||
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
|
||
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
|
||
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
||
knobRadius * 2.0f);
|
||
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
||
knobRadius * 2.0f);
|
||
}
|
||
|
||
/**
|
||
* @brief 生成十六进制颜色文本。
|
||
* @param [IN] color 0xAARRGGBB 颜色值。
|
||
* @return 形如 #RRGGBB 的文本。
|
||
*/
|
||
std::wstring ColorToHex(std::uint32_t color)
|
||
{
|
||
wchar_t buffer[16];
|
||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu,
|
||
(color >> 8) & 0xFFu, color & 0xFFu);
|
||
return std::wstring(buffer);
|
||
}
|
||
}
|
||
|
||
SignalPanel::SignalPanel()
|
||
{
|
||
m_colorRects.fill(gd::RectF());
|
||
m_modeRects.fill(gd::RectF());
|
||
m_sliderRects.fill(gd::RectF());
|
||
}
|
||
|
||
void SignalPanel::Setup(HWND host, const UiFonts* fonts, const Theme* theme, PopupMenu* menu)
|
||
{
|
||
m_host = host;
|
||
m_fonts = fonts;
|
||
m_theme = theme;
|
||
m_menu = menu;
|
||
}
|
||
|
||
void SignalPanel::SetContext(const DataSet* dataSet, TabConfig* tab)
|
||
{
|
||
m_dataSet = dataSet;
|
||
m_tab = tab;
|
||
m_rowsDirty = true;
|
||
m_scrollOffset = 0.0f;
|
||
m_colorPickerOpen = false;
|
||
m_draggingSlider = -1;
|
||
}
|
||
|
||
void SignalPanel::SetScale(float scale)
|
||
{
|
||
m_scale = (scale > 0.1f) ? scale : 1.0f;
|
||
}
|
||
|
||
bool SignalPanel::HitTest(int x, int y) const
|
||
{
|
||
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
|
||
}
|
||
|
||
int SignalPanel::TotalSignalCount() const
|
||
{
|
||
if (m_dataSet == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
return static_cast<int>(m_dataSet->SignalCount());
|
||
}
|
||
|
||
float SignalPanel::DesiredStyleHeight() const
|
||
{
|
||
return kStyleBaseHeight + (m_colorPickerOpen ? kColorPickerHeight : 0.0f);
|
||
}
|
||
|
||
bool SignalPanel::TakeLayoutDirty()
|
||
{
|
||
const bool dirty = m_layoutDirty;
|
||
m_layoutDirty = false;
|
||
return dirty;
|
||
}
|
||
|
||
bool SignalPanel::CloseTransientUi()
|
||
{
|
||
if (!m_colorPickerOpen)
|
||
{
|
||
return false;
|
||
}
|
||
m_colorPickerOpen = false;
|
||
m_draggingSlider = -1;
|
||
m_layoutDirty = true;
|
||
return true;
|
||
}
|
||
|
||
void SignalPanel::BuildRows()
|
||
{
|
||
m_rows.clear();
|
||
m_rowsDirty = false;
|
||
|
||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||
{
|
||
m_contentHeight = 0.0f;
|
||
return;
|
||
}
|
||
|
||
const float fileHeight = kFileRowHeight * m_scale;
|
||
const float signalHeight = kSignalRowHeight * m_scale;
|
||
|
||
const std::vector<DataFile>& files = m_dataSet->Files();
|
||
for (std::size_t fileIndex = 0; fileIndex < files.size(); ++fileIndex)
|
||
{
|
||
ListRow fileRow;
|
||
fileRow.kind = RowKind::FILE;
|
||
fileRow.fileIndex = static_cast<int>(fileIndex);
|
||
fileRow.height = fileHeight;
|
||
m_rows.push_back(fileRow);
|
||
|
||
if (m_tab->IsCollapsed(static_cast<int>(fileIndex)))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
const std::size_t signalCount = files[fileIndex].signals.size();
|
||
for (std::size_t signalIndex = 0; signalIndex < signalCount; ++signalIndex)
|
||
{
|
||
ListRow signalRow;
|
||
signalRow.kind = RowKind::SIGNAL;
|
||
signalRow.fileIndex = static_cast<int>(fileIndex);
|
||
signalRow.signalIndex = static_cast<int>(signalIndex);
|
||
signalRow.height = signalHeight;
|
||
m_rows.push_back(signalRow);
|
||
}
|
||
}
|
||
|
||
m_contentHeight = 0.0f;
|
||
for (const ListRow& row : m_rows)
|
||
{
|
||
m_contentHeight += row.height;
|
||
}
|
||
}
|
||
|
||
void SignalPanel::EnsureRows()
|
||
{
|
||
if (m_rowsDirty)
|
||
{
|
||
BuildRows();
|
||
RefreshScrollBar();
|
||
}
|
||
}
|
||
|
||
void SignalPanel::RefreshScrollBar()
|
||
{
|
||
if (m_listFullWidth <= 0.0f)
|
||
{
|
||
m_scrollBarVisible = false;
|
||
return;
|
||
}
|
||
|
||
m_scrollBarVisible = (m_contentHeight > m_listRect.Height + 0.5f);
|
||
if (m_scrollBarVisible)
|
||
{
|
||
const float thickness = kScrollBarThickness * m_scale;
|
||
m_listRect.Width = std::max(0.0f, m_listFullWidth - thickness - 2.0f * m_scale);
|
||
m_scrollBarRect = gd::RectF(m_area.X + m_area.Width - thickness - 3.0f * m_scale, m_listRect.Y,
|
||
thickness, m_listRect.Height);
|
||
}
|
||
else
|
||
{
|
||
m_listRect.Width = m_listFullWidth;
|
||
m_scrollBarRect = gd::RectF();
|
||
}
|
||
|
||
ClampScroll();
|
||
}
|
||
|
||
float SignalPanel::ContentHeight() const
|
||
{
|
||
return m_contentHeight;
|
||
}
|
||
|
||
void SignalPanel::ClampScroll()
|
||
{
|
||
const float maxOffset = std::max(0.0f, m_contentHeight - m_listRect.Height);
|
||
m_scrollOffset = std::max(0.0f, std::min(m_scrollOffset, maxOffset));
|
||
}
|
||
|
||
gd::RectF SignalPanel::RowRect(int row) const
|
||
{
|
||
if (row < 0 || row >= static_cast<int>(m_rows.size()))
|
||
{
|
||
return gd::RectF();
|
||
}
|
||
|
||
float top = m_listRect.Y - m_scrollOffset;
|
||
for (int index = 0; index < row; ++index)
|
||
{
|
||
top += m_rows[static_cast<std::size_t>(index)].height;
|
||
}
|
||
return gd::RectF(m_listRect.X, top, m_listRect.Width, m_rows[static_cast<std::size_t>(row)].height);
|
||
}
|
||
|
||
gd::RectF SignalPanel::FileCloseRect(int row) const
|
||
{
|
||
const gd::RectF rect = RowRect(row);
|
||
const float size = kCloseButtonSize * m_scale;
|
||
return gd::RectF(rect.X + rect.Width - kPadding * m_scale - size,
|
||
rect.Y + (rect.Height - size) * 0.5f, size, size);
|
||
}
|
||
|
||
gd::RectF SignalPanel::SignalBadgeRect(int row) const
|
||
{
|
||
const gd::RectF rect = RowRect(row);
|
||
const float width = kBadgeWidth * m_scale;
|
||
const float height = kBadgeHeight * m_scale;
|
||
return gd::RectF(rect.X + rect.Width - kPadding * m_scale - width,
|
||
rect.Y + (rect.Height - height) * 0.5f, width, height);
|
||
}
|
||
|
||
int SignalPanel::HitRow(int x, int y, RowPart& part)
|
||
{
|
||
part = RowPart::NONE;
|
||
EnsureRows();
|
||
|
||
if (m_listRect.Height <= 0.0f)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
if (fy < m_listRect.Y || fy > m_listRect.Y + m_listRect.Height)
|
||
{
|
||
return -1;
|
||
}
|
||
if (fx < m_listRect.X || fx > m_listRect.X + m_listRect.Width)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
const float local = fy - m_listRect.Y + m_scrollOffset;
|
||
if (local < 0.0f)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
float accumulated = 0.0f;
|
||
for (std::size_t index = 0; index < m_rows.size(); ++index)
|
||
{
|
||
const ListRow& row = m_rows[index];
|
||
if (local >= accumulated && local < accumulated + row.height)
|
||
{
|
||
const int rowIndex = static_cast<int>(index);
|
||
const gd::RectF rect = RowRect(rowIndex);
|
||
const bool isFile = (row.kind == RowKind::FILE);
|
||
const float baseX = rect.X + kPadding * m_scale + (isFile ? 0.0f : kIndent * m_scale);
|
||
|
||
const gd::RectF disclosureRect(baseX, rect.Y, kDisclosureWidth * m_scale, rect.Height);
|
||
const gd::RectF checkRect(baseX + kDisclosureWidth * m_scale,
|
||
rect.Y + (rect.Height - kCheckSize * m_scale) * 0.5f,
|
||
kCheckSize * m_scale, kCheckSize * m_scale);
|
||
|
||
if (isFile)
|
||
{
|
||
if (UiPointInRect(disclosureRect, fx, fy))
|
||
{
|
||
part = RowPart::DISCLOSURE;
|
||
}
|
||
else if (UiPointInRect(checkRect, fx, fy))
|
||
{
|
||
part = RowPart::CHECK;
|
||
}
|
||
else if (UiPointInRect(FileCloseRect(rowIndex), fx, fy))
|
||
{
|
||
part = RowPart::CLOSE;
|
||
}
|
||
else
|
||
{
|
||
part = RowPart::BODY;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
const SignalRef ref{row.fileIndex, row.signalIndex};
|
||
const bool badgeVisible = (m_tab != nullptr) && m_tab->IsVisible(ref);
|
||
|
||
if (UiPointInRect(checkRect, fx, fy))
|
||
{
|
||
part = RowPart::CHECK;
|
||
}
|
||
else if (badgeVisible && UiPointInRect(SignalBadgeRect(rowIndex), fx, fy))
|
||
{
|
||
part = RowPart::LANE_BADGE;
|
||
}
|
||
else
|
||
{
|
||
part = RowPart::BODY;
|
||
}
|
||
}
|
||
return rowIndex;
|
||
}
|
||
accumulated += row.height;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
void SignalPanel::ScrollToSignal(const SignalRef& ref)
|
||
{
|
||
EnsureRows();
|
||
if (!ref.IsValid())
|
||
{
|
||
return;
|
||
}
|
||
|
||
float top = 0.0f;
|
||
for (std::size_t index = 0; index < m_rows.size(); ++index)
|
||
{
|
||
const ListRow& row = m_rows[index];
|
||
if (row.kind == RowKind::SIGNAL && row.fileIndex == ref.fileIndex &&
|
||
row.signalIndex == ref.signalIndex)
|
||
{
|
||
if (top < m_scrollOffset)
|
||
{
|
||
m_scrollOffset = top;
|
||
}
|
||
else if (top + row.height > m_scrollOffset + m_listRect.Height)
|
||
{
|
||
m_scrollOffset = top + row.height - m_listRect.Height;
|
||
}
|
||
ClampScroll();
|
||
return;
|
||
}
|
||
top += row.height;
|
||
}
|
||
}
|
||
|
||
void SignalPanel::LayoutStyleControls()
|
||
{
|
||
const float labelWidth = kLineLabelWidth * m_scale;
|
||
const float contentLeft = m_styleRect.X + kStylePadH * m_scale + labelWidth;
|
||
const float contentWidth = std::max(40.0f, m_styleRect.Width - kStylePadH * 2.0f * m_scale - labelWidth);
|
||
|
||
const float row0Top = m_styleRect.Y + kStylePadTop * m_scale;
|
||
const float row1Top = row0Top + (kRowHeight + kRowGap) * m_scale;
|
||
const float row2Top = row1Top + (kRowHeight + kRowGap) * m_scale;
|
||
|
||
// ---- 第一行:颜色(预设色块 + 自定义颜色入口)----
|
||
int paletteCount = 0;
|
||
SignalPalette(paletteCount);
|
||
|
||
const float chipSize = 22.0f * m_scale;
|
||
const float chipGap = 8.0f * m_scale;
|
||
const float swatchGap = 4.0f * m_scale;
|
||
const float available = std::max(swatchGap, contentWidth - chipGap - chipSize);
|
||
|
||
int count = std::min(paletteCount, static_cast<int>(m_colorRects.size()));
|
||
float swatchSize = 18.0f * m_scale;
|
||
while (count > 1)
|
||
{
|
||
const float fitted =
|
||
(available - swatchGap * static_cast<float>(count - 1)) / static_cast<float>(count);
|
||
if (fitted >= 12.0f * m_scale)
|
||
{
|
||
swatchSize = std::min(18.0f * m_scale, fitted);
|
||
break;
|
||
}
|
||
--count;
|
||
}
|
||
if (count <= 1)
|
||
{
|
||
count = std::max(1, std::min(count, 1));
|
||
swatchSize = std::min(18.0f * m_scale, available);
|
||
}
|
||
m_colorCount = count;
|
||
|
||
const float swatchTop = row0Top + (kRowHeight * m_scale - swatchSize) * 0.5f;
|
||
for (int index = 0; index < m_colorCount; ++index)
|
||
{
|
||
const float x = contentLeft + static_cast<float>(index) * (swatchSize + swatchGap);
|
||
m_colorRects[static_cast<std::size_t>(index)] = gd::RectF(x, swatchTop, swatchSize, swatchSize);
|
||
}
|
||
|
||
const float chipLeft = contentLeft + static_cast<float>(m_colorCount) * (swatchSize + swatchGap) -
|
||
swatchGap + chipGap;
|
||
m_customChipRect = gd::RectF(std::min(chipLeft, contentLeft + contentWidth - chipSize),
|
||
row0Top + (kRowHeight * m_scale - chipSize) * 0.5f, chipSize, chipSize);
|
||
|
||
// ---- 第二行:线宽(两个预设 + 数值旋钮)----
|
||
float presetWidth = 46.0f * m_scale;
|
||
float buttonWidth = 22.0f * m_scale;
|
||
float valueWidth = 62.0f * m_scale;
|
||
float gap = 6.0f * m_scale;
|
||
const float natural = presetWidth * 2.0f + buttonWidth * 2.0f + valueWidth + gap * 2.0f;
|
||
if (natural > contentWidth)
|
||
{
|
||
const float shrink = contentWidth / natural;
|
||
presetWidth = std::max(28.0f * m_scale, presetWidth * shrink);
|
||
buttonWidth = std::max(16.0f * m_scale, buttonWidth * shrink);
|
||
valueWidth = std::max(40.0f * m_scale, valueWidth * shrink);
|
||
gap = std::max(3.0f * m_scale, gap * shrink);
|
||
}
|
||
|
||
const float spinnerWidth = buttonWidth * 2.0f + valueWidth;
|
||
const float groupWidth = presetWidth * 2.0f + spinnerWidth + gap * 2.0f;
|
||
const float groupLeft = contentLeft + std::max(0.0f, (contentWidth - groupWidth) * 0.5f);
|
||
|
||
m_widthThinRect = gd::RectF(groupLeft, row1Top, presetWidth, kRowHeight * m_scale);
|
||
m_widthSpinnerRect = gd::RectF(groupLeft + presetWidth + gap, row1Top, spinnerWidth,
|
||
kRowHeight * m_scale);
|
||
m_widthMinusRect = gd::RectF(m_widthSpinnerRect.X, row1Top, buttonWidth, kRowHeight * m_scale);
|
||
m_widthValueRect = gd::RectF(m_widthMinusRect.X + buttonWidth, row1Top, valueWidth,
|
||
kRowHeight * m_scale);
|
||
m_widthPlusRect = gd::RectF(m_widthValueRect.X + valueWidth, row1Top, buttonWidth,
|
||
kRowHeight * m_scale);
|
||
m_widthThickRect = gd::RectF(m_widthSpinnerRect.X + spinnerWidth + gap, row1Top, presetWidth,
|
||
kRowHeight * m_scale);
|
||
|
||
// ---- 第三行:样式(线段 / 散点 / 点线)----
|
||
const float modeGap = 6.0f * m_scale;
|
||
const float modeWidth = (contentWidth - modeGap * static_cast<float>(kModeOptionCount - 1)) /
|
||
static_cast<float>(kModeOptionCount);
|
||
for (int index = 0; index < kModeOptionCount; ++index)
|
||
{
|
||
const float x = contentLeft + static_cast<float>(index) * (modeWidth + modeGap);
|
||
m_modeRects[static_cast<std::size_t>(index)] = gd::RectF(x, row2Top, modeWidth, kRowHeight * m_scale);
|
||
}
|
||
|
||
// ---- 自定义颜色面板 ----
|
||
if (!m_colorPickerOpen)
|
||
{
|
||
m_pickerRect = gd::RectF();
|
||
return;
|
||
}
|
||
|
||
const float pickerTop = row2Top + (kRowHeight + kRowGap) * m_scale;
|
||
m_pickerRect = gd::RectF(m_styleRect.X + kStylePadH * m_scale, pickerTop,
|
||
std::max(40.0f, m_styleRect.Width - kStylePadH * 2.0f * m_scale),
|
||
kColorPickerHeight * m_scale - kRowGap * m_scale);
|
||
|
||
const float sliderLabelX = m_pickerRect.X + 6.0f * m_scale;
|
||
const float trackLeft = sliderLabelX + kSliderLabelWidth * m_scale;
|
||
const float trackWidth =
|
||
std::max(24.0f, m_pickerRect.Width - (trackLeft - m_pickerRect.X) - 6.0f * m_scale -
|
||
kSliderValueWidth * m_scale);
|
||
const float trackHeight = 6.0f * m_scale;
|
||
const float firstTop = m_pickerRect.Y + 10.0f * m_scale;
|
||
|
||
for (int index = 0; index < 3; ++index)
|
||
{
|
||
const float rowTop = firstTop + static_cast<float>(index) * (kSliderRowHeight + kSliderGap) * m_scale;
|
||
m_sliderRects[static_cast<std::size_t>(index)] =
|
||
gd::RectF(trackLeft, rowTop + (kSliderRowHeight * m_scale - trackHeight) * 0.5f, trackWidth,
|
||
trackHeight);
|
||
}
|
||
|
||
const float previewTop = firstTop + 3.0f * (kSliderRowHeight + kSliderGap) * m_scale;
|
||
m_pickerPreviewRect = gd::RectF(trackLeft, previewTop, 54.0f * m_scale, 20.0f * m_scale);
|
||
}
|
||
|
||
void SignalPanel::Layout(const gd::RectF& area)
|
||
{
|
||
m_area = area;
|
||
|
||
m_headerRect = gd::RectF(area.X, area.Y, area.Width, kHeaderHeight * m_scale);
|
||
|
||
const float styleHeight = DesiredStyleHeight() * m_scale;
|
||
m_styleRect = gd::RectF(area.X, area.Y + area.Height - styleHeight, area.Width, styleHeight);
|
||
m_listRect = gd::RectF(area.X, m_headerRect.Y + m_headerRect.Height, area.Width,
|
||
std::max(0.0f, m_styleRect.Y - m_headerRect.Y - m_headerRect.Height));
|
||
|
||
// 标题区右侧的“全部显示/隐藏”按钮。
|
||
const float toggleWidth = 68.0f * m_scale;
|
||
const float toggleHeight = 22.0f * m_scale;
|
||
m_toggleAllRect = gd::RectF(area.X + area.Width - kPadding * m_scale - toggleWidth,
|
||
m_headerRect.Y + (m_headerRect.Height - toggleHeight) * 0.5f,
|
||
toggleWidth, toggleHeight);
|
||
|
||
// 依据内容决定是否显示滚动条,并据此收缩列表可用宽度。
|
||
m_listFullWidth = m_listRect.Width;
|
||
EnsureRows();
|
||
RefreshScrollBar();
|
||
|
||
LayoutStyleControls();
|
||
}
|
||
|
||
void SignalPanel::Paint(gd::Graphics& g)
|
||
{
|
||
if (m_theme == nullptr || m_fonts == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
EnsureRows();
|
||
UiFillRect(g, m_area, m_theme->panelBg);
|
||
|
||
// 标题区。
|
||
{
|
||
std::wstring title = L"信号";
|
||
if (m_dataSet != nullptr && m_tab != nullptr)
|
||
{
|
||
title += L" · " + std::to_wstring(m_tab->VisibleCount()) + L"/" +
|
||
std::to_wstring(TotalSignalCount()) + L" 已选";
|
||
}
|
||
|
||
gd::RectF titleRect(m_headerRect.X + kPadding * m_scale, m_headerRect.Y,
|
||
std::max(20.0f, m_headerRect.Width - kPadding * 2.0f * m_scale -
|
||
m_toggleAllRect.Width - 8.0f * m_scale),
|
||
m_headerRect.Height);
|
||
UiText(g, UiEllipsize(g, title, m_fonts->uiBold, titleRect.Width), m_fonts->uiBold, titleRect,
|
||
m_theme->textStrong, TextAlign::LEFT);
|
||
|
||
const bool allVisible = AreAllSignalsVisible();
|
||
const std::wstring toggleLabel = allVisible ? L"全部隐藏" : L"全部显示";
|
||
const bool toggleHover = (m_hoverStyle == StyleHit::TOGGLE_ALL);
|
||
if (toggleHover)
|
||
{
|
||
UiFillRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->hover);
|
||
}
|
||
UiDrawRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->border, 1.0f);
|
||
const bool enabled = (m_dataSet != nullptr && !m_dataSet->Empty());
|
||
UiText(g, toggleLabel, m_fonts->uiSmall, m_toggleAllRect,
|
||
enabled ? m_theme->text : m_theme->textMuted, TextAlign::CENTER);
|
||
}
|
||
|
||
// 列表区。
|
||
{
|
||
const int saveState = g.Save();
|
||
g.SetClip(gd::RectF(m_listRect.X, m_listRect.Y, m_listRect.Width, m_listRect.Height));
|
||
|
||
if (m_dataSet == nullptr || m_dataSet->Empty())
|
||
{
|
||
UiText(g, L"尚未加载数据,请打开 CSV 文件", m_fonts->uiSmall, m_listRect, m_theme->textMuted,
|
||
TextAlign::CENTER);
|
||
}
|
||
else
|
||
{
|
||
const int rowCount = static_cast<int>(m_rows.size());
|
||
for (int row = 0; row < rowCount; ++row)
|
||
{
|
||
const gd::RectF rect = RowRect(row);
|
||
if (rect.Y + rect.Height < m_listRect.Y || rect.Y > m_listRect.Y + m_listRect.Height)
|
||
{
|
||
continue;
|
||
}
|
||
if (m_rows[static_cast<std::size_t>(row)].kind == RowKind::FILE)
|
||
{
|
||
PaintFileRow(g, row);
|
||
}
|
||
else
|
||
{
|
||
PaintSignalRow(g, row);
|
||
}
|
||
}
|
||
}
|
||
|
||
g.Restore(saveState);
|
||
}
|
||
|
||
PaintScrollBar(g);
|
||
PaintStyleSection(g);
|
||
|
||
// 分隔线。
|
||
UiDrawLine(g, m_area.X + m_area.Width - 1.0f, m_area.Y, m_area.X + m_area.Width - 1.0f,
|
||
m_area.Y + m_area.Height, m_theme->border, 1.0f);
|
||
UiDrawLine(g, m_headerRect.X, m_headerRect.Y + m_headerRect.Height - 1.0f,
|
||
m_headerRect.X + m_headerRect.Width, m_headerRect.Y + m_headerRect.Height - 1.0f,
|
||
m_theme->border, 1.0f);
|
||
if (m_styleRect.Height > 0.0f)
|
||
{
|
||
UiDrawLine(g, m_styleRect.X, m_styleRect.Y, m_styleRect.X + m_styleRect.Width, m_styleRect.Y,
|
||
m_theme->border, 1.0f);
|
||
}
|
||
}
|
||
|
||
void SignalPanel::PaintFileRow(gd::Graphics& g, int row)
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast<int>(m_rows.size()))
|
||
{
|
||
return;
|
||
}
|
||
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||
const DataFile* file = m_dataSet->FileAt(item.fileIndex);
|
||
if (file == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const gd::RectF rect = RowRect(row);
|
||
const bool hovered = (m_hoverRow == row);
|
||
if (hovered)
|
||
{
|
||
UiFillRect(g, rect, m_theme->hover);
|
||
}
|
||
UiFillRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height),
|
||
WithAlpha(m_theme->borderStrong, 0x90));
|
||
|
||
const float baseX = rect.X + kPadding * m_scale;
|
||
const float centerY = rect.Y + rect.Height * 0.5f;
|
||
const bool collapsed = m_tab->IsCollapsed(item.fileIndex);
|
||
|
||
// 折叠三角。
|
||
{
|
||
const float cx = baseX + kDisclosureWidth * m_scale * 0.5f;
|
||
const std::uint32_t color =
|
||
(hovered && m_hoverPart == RowPart::DISCLOSURE) ? m_theme->accent : m_theme->text;
|
||
UiDrawTriangleIcon(g, gd::RectF(cx - 6.0f * m_scale, centerY - 6.0f * m_scale, 12.0f * m_scale,
|
||
12.0f * m_scale),
|
||
color, collapsed ? IconDirection::RIGHT : IconDirection::DOWN);
|
||
}
|
||
|
||
// 分组复选框(三态:全选 / 部分 / 未选)。
|
||
const gd::RectF checkRect(baseX + kDisclosureWidth * m_scale, centerY - kCheckSize * m_scale * 0.5f,
|
||
kCheckSize * m_scale, kCheckSize * m_scale);
|
||
const bool allVisible = m_tab->FileAllVisible(*m_dataSet, item.fileIndex);
|
||
const bool anyVisible = m_tab->FileHasVisible(*m_dataSet, item.fileIndex);
|
||
UiDrawCheckBox(g, checkRect, allVisible ? 1 : (anyVisible ? 2 : 0), 3.0f * m_scale);
|
||
|
||
const gd::RectF closeRect = FileCloseRect(row);
|
||
|
||
// 文件名称与统计。
|
||
const float nameLeft = checkRect.X + checkRect.Width + 8.0f * m_scale;
|
||
const std::wstring stats = std::to_wstring(file->signals.size()) + L" 项";
|
||
const float statsWidth = UiMeasureTextWidth(m_fonts->uiSmall, stats);
|
||
const float statsRight = closeRect.X - 6.0f * m_scale;
|
||
const float nameWidth = std::max(20.0f, statsRight - statsWidth - 6.0f * m_scale - nameLeft);
|
||
|
||
UiText(g, UiEllipsize(g, file->fileName, m_fonts->uiBold, nameWidth), m_fonts->uiBold,
|
||
gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), m_theme->textStrong, TextAlign::LEFT);
|
||
UiText(g, stats, m_fonts->uiSmall, gd::RectF(statsRight - statsWidth, rect.Y, statsWidth, rect.Height),
|
||
m_theme->textMuted, TextAlign::RIGHT);
|
||
|
||
// 关闭按钮(从工作区移除该文件)。
|
||
const bool closeHot = hovered && (m_hoverPart == RowPart::CLOSE);
|
||
if (closeHot)
|
||
{
|
||
UiFillRoundRect(g, closeRect, 3.0f * m_scale, m_theme->pressed);
|
||
}
|
||
UiDrawCrossIcon(g, closeRect,
|
||
closeHot ? m_theme->danger : m_theme->textMuted, 1.5f * m_scale);
|
||
}
|
||
|
||
void SignalPanel::PaintSignalRow(gd::Graphics& g, int row)
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast<int>(m_rows.size()))
|
||
{
|
||
return;
|
||
}
|
||
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||
const SignalRef ref{item.fileIndex, item.signalIndex};
|
||
const SignalData* signal = m_dataSet->Resolve(ref);
|
||
const SignalStyle* style = m_tab->Style(ref);
|
||
if (signal == nullptr || style == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const gd::RectF rect = RowRect(row);
|
||
const bool selected = (m_tab->Selected() == ref);
|
||
const bool hovered = (m_hoverRow == row);
|
||
|
||
if (selected)
|
||
{
|
||
UiFillRect(g, rect, m_theme->selection);
|
||
UiFillRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height), m_theme->accent);
|
||
}
|
||
else if (hovered)
|
||
{
|
||
UiFillRect(g, rect, m_theme->hover);
|
||
}
|
||
|
||
const float baseX = rect.X + kPadding * m_scale + kIndent * m_scale;
|
||
const float centerY = rect.Y + rect.Height * 0.5f;
|
||
|
||
// 复选框。
|
||
const gd::RectF checkRect(baseX + kDisclosureWidth * m_scale, centerY - kCheckSize * m_scale * 0.5f,
|
||
kCheckSize * m_scale, kCheckSize * m_scale);
|
||
UiDrawCheckBox(g, checkRect, style->visible ? 1 : 0, 3.0f * m_scale);
|
||
|
||
// 颜色块。
|
||
const gd::RectF swatchRect(checkRect.X + checkRect.Width + 8.0f * m_scale,
|
||
centerY - kSwatchSize * m_scale * 0.5f, kSwatchSize * m_scale,
|
||
kSwatchSize * m_scale);
|
||
UiFillRoundRect(g, swatchRect, 2.5f * m_scale,
|
||
style->visible ? style->color : WithAlpha(style->color, 0x55));
|
||
|
||
// 网格徽标(可点击,用于调整所属网格)。
|
||
const int laneIndex = m_tab->LaneOfSignal(ref);
|
||
const bool badgeVisible = (laneIndex >= 0) && style->visible;
|
||
const gd::RectF badgeRect = SignalBadgeRect(row);
|
||
|
||
float nameRightLimit = rect.X + rect.Width - kPadding * m_scale;
|
||
if (badgeVisible)
|
||
{
|
||
nameRightLimit = badgeRect.X - 6.0f * m_scale;
|
||
}
|
||
|
||
// 名称与单位。
|
||
const float unitWidth = signal->unit.empty()
|
||
? 0.0f
|
||
: UiMeasureTextWidth(m_fonts->uiSmall, L" " + signal->unit) + 4.0f * m_scale;
|
||
const float nameLeft = swatchRect.X + swatchRect.Width + 8.0f * m_scale;
|
||
const float nameWidth = std::max(16.0f, nameRightLimit - unitWidth - nameLeft);
|
||
const std::uint32_t nameColor = style->visible ? m_theme->text : m_theme->textMuted;
|
||
UiText(g, UiEllipsize(g, signal->name, m_fonts->ui, nameWidth), m_fonts->ui,
|
||
gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), nameColor, TextAlign::LEFT);
|
||
|
||
if (!signal->unit.empty())
|
||
{
|
||
const std::wstring unitText = L" " + signal->unit;
|
||
UiText(g, unitText, m_fonts->uiSmall,
|
||
gd::RectF(nameLeft + nameWidth, rect.Y, unitWidth, rect.Height), m_theme->textMuted,
|
||
TextAlign::LEFT);
|
||
}
|
||
|
||
if (badgeVisible)
|
||
{
|
||
const bool badgeHot = hovered && (m_hoverPart == RowPart::LANE_BADGE);
|
||
UiFillRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
|
||
badgeHot ? m_theme->accentSoft : m_theme->cardBg);
|
||
UiDrawRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
|
||
badgeHot ? m_theme->accent : m_theme->border, 1.0f);
|
||
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
|
||
badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||
}
|
||
}
|
||
|
||
gd::RectF SignalPanel::ScrollThumbRect() const
|
||
{
|
||
if (!m_scrollBarVisible || m_contentHeight <= m_listRect.Height || m_listRect.Height <= 0.0f)
|
||
{
|
||
return gd::RectF();
|
||
}
|
||
|
||
const float ratio = std::min(1.0f, m_listRect.Height / m_contentHeight);
|
||
const float thumbHeight = std::max(24.0f * m_scale, m_scrollBarRect.Height * ratio);
|
||
const float maxScroll = m_contentHeight - m_listRect.Height;
|
||
const float progress = (maxScroll > 0.0f) ? (m_scrollOffset / maxScroll) : 0.0f;
|
||
const float thumbY = m_scrollBarRect.Y + progress * (m_scrollBarRect.Height - thumbHeight);
|
||
|
||
return gd::RectF(m_scrollBarRect.X, thumbY, m_scrollBarRect.Width, thumbHeight);
|
||
}
|
||
|
||
void SignalPanel::PaintScrollBar(gd::Graphics& g)
|
||
{
|
||
if (!m_scrollBarVisible || m_contentHeight <= m_listRect.Height)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UiFillRoundRect(g, m_scrollBarRect, kScrollBarThickness * m_scale * 0.5f, m_theme->gridWeak);
|
||
UiFillRoundRect(g, ScrollThumbRect(), kScrollBarThickness * m_scale * 0.5f, m_theme->borderStrong);
|
||
}
|
||
|
||
void SignalPanel::PaintStyleSection(gd::Graphics& g)
|
||
{
|
||
if (m_styleRect.Height <= 0.0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UiFillRect(g, m_styleRect, m_theme->panelBg);
|
||
|
||
const SignalStyle* style = (m_tab == nullptr) ? nullptr : m_tab->Style(m_tab->Selected());
|
||
if (style == nullptr)
|
||
{
|
||
UiText(g, L"请选择信号以编辑样式", m_fonts->uiSmall, m_styleRect, m_theme->textMuted,
|
||
TextAlign::CENTER);
|
||
return;
|
||
}
|
||
|
||
const float labelX = m_styleRect.X + kStylePadH * m_scale;
|
||
const float labelWidth = kLineLabelWidth * m_scale;
|
||
|
||
// ---- 颜色 ----
|
||
{
|
||
const float rowTop = m_styleRect.Y + kStylePadTop * m_scale;
|
||
UiText(g, L"颜色", m_fonts->uiSmall, gd::RectF(labelX, rowTop, labelWidth, kRowHeight * m_scale),
|
||
m_theme->textMuted, TextAlign::LEFT);
|
||
|
||
int paletteCount = 0;
|
||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||
for (int index = 0; index < m_colorCount; ++index)
|
||
{
|
||
const gd::RectF& rect = m_colorRects[static_cast<std::size_t>(index)];
|
||
const std::uint32_t color = palette[index % paletteCount];
|
||
const bool hovered = (m_hoverStyle == StyleHit::COLOR && m_hoverStyleIndex == index);
|
||
const bool active = (style->color == color);
|
||
|
||
UiFillRoundRect(g, rect, 4.0f * m_scale, color);
|
||
if (active || hovered)
|
||
{
|
||
const gd::RectF ring(rect.X - 2.0f * m_scale, rect.Y - 2.0f * m_scale,
|
||
rect.Width + 4.0f * m_scale, rect.Height + 4.0f * m_scale);
|
||
UiDrawRoundRect(g, ring, 5.0f * m_scale,
|
||
active ? m_theme->textStrong : m_theme->borderStrong,
|
||
active ? 1.6f : 1.0f);
|
||
}
|
||
}
|
||
|
||
// 自定义颜色入口:带加号图标的小方格。
|
||
const bool chipHot = (m_hoverStyle == StyleHit::CUSTOM_CHIP);
|
||
const bool chipActive = m_colorPickerOpen;
|
||
UiFillRoundRect(g, m_customChipRect, 4.0f * m_scale,
|
||
chipActive ? m_theme->accentSoft : (chipHot ? m_theme->hover : m_theme->cardBg));
|
||
UiDrawRoundRect(g, m_customChipRect, 4.0f * m_scale,
|
||
chipActive ? m_theme->accent : m_theme->borderStrong, 1.0f);
|
||
UiDrawPlusIcon(g, m_customChipRect, chipActive ? m_theme->accent : m_theme->text, 1.6f * m_scale);
|
||
}
|
||
|
||
// ---- 线宽:两个预设 + 数值旋钮 ----
|
||
{
|
||
UiText(g, L"线宽", m_fonts->uiSmall,
|
||
gd::RectF(labelX, m_widthThinRect.Y, labelWidth, m_widthThinRect.Height),
|
||
m_theme->textMuted, TextAlign::LEFT);
|
||
|
||
struct PresetInfo
|
||
{
|
||
gd::RectF rect;
|
||
float width;
|
||
int index;
|
||
};
|
||
const PresetInfo presets[] = {
|
||
{m_widthThinRect, kWidthThinPreset, 0},
|
||
{m_widthThickRect, kWidthThickPreset, 1},
|
||
};
|
||
|
||
for (const PresetInfo& preset : presets)
|
||
{
|
||
const bool active = std::fabs(style->lineWidth - preset.width) < 0.01f;
|
||
const bool hovered = (m_hoverStyle == StyleHit::WIDTH_PRESET && m_hoverStyleIndex == preset.index);
|
||
|
||
UiFillRoundRect(g, preset.rect, kCorner * m_scale,
|
||
active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg));
|
||
UiDrawRoundRect(g, preset.rect, kCorner * m_scale,
|
||
active ? m_theme->accent : m_theme->border, 1.0f);
|
||
|
||
const float centerY = preset.rect.Y + preset.rect.Height * 0.5f;
|
||
UiDrawLine(g, preset.rect.X + 9.0f * m_scale, centerY, preset.rect.X + preset.rect.Width - 9.0f * m_scale,
|
||
centerY, active ? m_theme->accent : m_theme->text, preset.width * m_scale);
|
||
}
|
||
|
||
// 数值输入框。
|
||
UiFillRoundRect(g, m_widthSpinnerRect, kCorner * m_scale, m_theme->cardBg);
|
||
UiDrawRoundRect(g, m_widthSpinnerRect, kCorner * m_scale, m_theme->border, 1.0f);
|
||
|
||
const std::wstring valueText = FormatNumber(style->lineWidth, 1) + L" px";
|
||
UiText(g, valueText, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, TextAlign::CENTER);
|
||
|
||
const bool minusHot = (m_hoverStyle == StyleHit::WIDTH_MINUS);
|
||
const bool plusHot = (m_hoverStyle == StyleHit::WIDTH_PLUS);
|
||
const bool minusEnabled = style->lineWidth > (kWidthMin + 1.0e-4f);
|
||
const bool plusEnabled = style->lineWidth < (kWidthMax - 1.0e-4f);
|
||
|
||
if (minusHot && minusEnabled)
|
||
{
|
||
UiFillRoundRect(g, m_widthMinusRect, kCorner * m_scale, m_theme->hover);
|
||
}
|
||
if (plusHot && plusEnabled)
|
||
{
|
||
UiFillRoundRect(g, m_widthPlusRect, kCorner * m_scale, m_theme->hover);
|
||
}
|
||
UiDrawLine(g, m_widthMinusRect.X + m_widthMinusRect.Width, m_widthMinusRect.Y + 3.0f * m_scale,
|
||
m_widthMinusRect.X + m_widthMinusRect.Width, m_widthMinusRect.Y + m_widthMinusRect.Height -
|
||
3.0f * m_scale,
|
||
m_theme->border, 1.0f);
|
||
UiDrawLine(g, m_widthPlusRect.X, m_widthPlusRect.Y + 3.0f * m_scale, m_widthPlusRect.X,
|
||
m_widthPlusRect.Y + m_widthPlusRect.Height - 3.0f * m_scale, m_theme->border, 1.0f);
|
||
|
||
UiDrawArrowIcon(g, m_widthMinusRect,
|
||
minusEnabled ? m_theme->text : m_theme->textMuted, 1.6f * m_scale,
|
||
IconDirection::LEFT);
|
||
UiDrawArrowIcon(g, m_widthPlusRect, plusEnabled ? m_theme->text : m_theme->textMuted,
|
||
1.6f * m_scale, IconDirection::RIGHT);
|
||
}
|
||
|
||
// ---- 样式 ----
|
||
{
|
||
const gd::RectF& first = m_modeRects[0];
|
||
UiText(g, L"样式", m_fonts->uiSmall, gd::RectF(labelX, first.Y, labelWidth, first.Height),
|
||
m_theme->textMuted, TextAlign::LEFT);
|
||
|
||
for (int index = 0; index < kModeOptionCount; ++index)
|
||
{
|
||
const gd::RectF& rect = m_modeRects[static_cast<std::size_t>(index)];
|
||
const bool active = (style->mode == kModeOptions[index]);
|
||
const bool hovered = (m_hoverStyle == StyleHit::MODE && m_hoverStyleIndex == index);
|
||
|
||
UiFillRoundRect(g, rect, kCorner * m_scale,
|
||
active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg));
|
||
UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f);
|
||
UiText(g, DrawModeName(kModeOptions[index]), m_fonts->uiSmall, rect,
|
||
active ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||
}
|
||
}
|
||
|
||
if (m_colorPickerOpen)
|
||
{
|
||
PaintColorPicker(g, *style);
|
||
}
|
||
}
|
||
|
||
void SignalPanel::PaintColorPicker(gd::Graphics& g, const SignalStyle& style)
|
||
{
|
||
UiFillRoundRect(g, m_pickerRect, kCorner * m_scale, m_theme->cardBg);
|
||
UiDrawRoundRect(g, m_pickerRect, kCorner * m_scale, m_theme->borderStrong, 1.0f);
|
||
|
||
static const wchar_t* kChannelNames[3] = {L"R", L"G", L"B"};
|
||
static const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u};
|
||
|
||
for (int index = 0; index < 3; ++index)
|
||
{
|
||
const gd::RectF& track = m_sliderRects[static_cast<std::size_t>(index)];
|
||
const gd::RectF rowRect(m_pickerRect.X, track.Y - 9.0f * m_scale,
|
||
m_pickerRect.Width, kSliderRowHeight * m_scale);
|
||
|
||
UiText(g, kChannelNames[index], m_fonts->uiSmall,
|
||
gd::RectF(m_pickerRect.X + 6.0f * m_scale, rowRect.Y, kSliderLabelWidth * m_scale,
|
||
rowRect.Height),
|
||
m_theme->textMuted, TextAlign::LEFT);
|
||
|
||
DrawSlider(g, track, m_pickerChannels[static_cast<std::size_t>(index)], kChannelColors[index],
|
||
m_hoverStyle == StyleHit::SLIDER && m_hoverStyleIndex == index, m_scale);
|
||
|
||
const int value = static_cast<int>(m_pickerChannels[static_cast<std::size_t>(index)] * 255.0 + 0.5);
|
||
UiText(g, std::to_wstring(value), m_fonts->monoSmall,
|
||
gd::RectF(track.X + track.Width + 6.0f * m_scale, rowRect.Y, kSliderValueWidth * m_scale,
|
||
rowRect.Height),
|
||
m_theme->text, TextAlign::RIGHT);
|
||
}
|
||
|
||
// 预览与十六进制值。
|
||
UiText(g, L"预览", m_fonts->uiSmall,
|
||
gd::RectF(m_pickerRect.X + 6.0f * m_scale, m_pickerPreviewRect.Y,
|
||
kSliderLabelWidth * m_scale + 20.0f * m_scale, m_pickerPreviewRect.Height),
|
||
m_theme->textMuted, TextAlign::LEFT);
|
||
|
||
UiFillRoundRect(g, m_pickerPreviewRect, 3.0f * m_scale, style.color);
|
||
UiDrawRoundRect(g, m_pickerPreviewRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
|
||
|
||
UiText(g, ColorToHex(style.color), m_fonts->monoSmall,
|
||
gd::RectF(m_pickerPreviewRect.X + m_pickerPreviewRect.Width + 8.0f * m_scale,
|
||
m_pickerPreviewRect.Y, 96.0f * m_scale, m_pickerPreviewRect.Height),
|
||
m_theme->text, TextAlign::LEFT);
|
||
}
|
||
|
||
bool SignalPanel::AreAllSignalsVisible() const
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr || m_dataSet->Empty())
|
||
{
|
||
return false;
|
||
}
|
||
return m_tab->VisibleCount() == TotalSignalCount();
|
||
}
|
||
|
||
bool SignalPanel::StepLineWidth(float delta)
|
||
{
|
||
if (m_tab == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
SignalStyle* style = m_tab->Style(m_tab->Selected());
|
||
if (style == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
float value = style->lineWidth + delta;
|
||
value = std::max(kWidthMin, std::min(kWidthMax, value));
|
||
value = std::round(value * 10.0f) / 10.0f;
|
||
if (std::fabs(value - style->lineWidth) < 0.001f)
|
||
{
|
||
return false;
|
||
}
|
||
style->lineWidth = value;
|
||
return true;
|
||
}
|
||
|
||
bool SignalPanel::OpenColorPicker(const SignalStyle& style)
|
||
{
|
||
m_pickerChannels[0] = static_cast<double>((style.color >> 16) & 0xFFu) / 255.0;
|
||
m_pickerChannels[1] = static_cast<double>((style.color >> 8) & 0xFFu) / 255.0;
|
||
m_pickerChannels[2] = static_cast<double>(style.color & 0xFFu) / 255.0;
|
||
|
||
if (m_colorPickerOpen)
|
||
{
|
||
return true;
|
||
}
|
||
m_colorPickerOpen = true;
|
||
m_layoutDirty = true;
|
||
return true;
|
||
}
|
||
|
||
bool SignalPanel::OnMouseMove(int x, int y)
|
||
{
|
||
// 滑块拖拽优先级最高。
|
||
if (m_draggingSlider >= 0 && m_draggingSlider < 3)
|
||
{
|
||
const gd::RectF& track = m_sliderRects[static_cast<std::size_t>(m_draggingSlider)];
|
||
m_pickerChannels[static_cast<std::size_t>(m_draggingSlider)] =
|
||
UiSliderRatioFromX(track, static_cast<float>(x));
|
||
if (m_tab != nullptr)
|
||
{
|
||
if (SignalStyle* style = m_tab->Style(m_tab->Selected()))
|
||
{
|
||
const unsigned int r =
|
||
static_cast<unsigned int>(m_pickerChannels[0] * 255.0 + 0.5);
|
||
const unsigned int g =
|
||
static_cast<unsigned int>(m_pickerChannels[1] * 255.0 + 0.5);
|
||
const unsigned int b =
|
||
static_cast<unsigned int>(m_pickerChannels[2] * 255.0 + 0.5);
|
||
style->color = 0xFF000000u | (r << 16) | (g << 8) | b;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
if (m_draggingScrollBar)
|
||
{
|
||
const gd::RectF thumb = ScrollThumbRect();
|
||
const float travel = static_cast<float>(y - m_scrollGrabOffset) - m_scrollBarRect.Y;
|
||
const float usable = m_scrollBarRect.Height - thumb.Height;
|
||
if (usable > 0.0f)
|
||
{
|
||
const float progress = std::max(0.0f, std::min(1.0f, travel / usable));
|
||
m_scrollOffset = progress * (m_contentHeight - m_listRect.Height);
|
||
ClampScroll();
|
||
}
|
||
return true;
|
||
}
|
||
|
||
RowPart part = RowPart::NONE;
|
||
const int hoverRow = HitRow(x, y, part);
|
||
|
||
StyleHit hoverStyle = StyleHit::NONE;
|
||
int hoverStyleIndex = -1;
|
||
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
|
||
if (UiPointInRect(m_toggleAllRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::TOGGLE_ALL;
|
||
}
|
||
else if (m_colorPickerOpen)
|
||
{
|
||
for (int index = 0; index < 3; ++index)
|
||
{
|
||
if (UiPointInRect(SliderHitRect(m_sliderRects[static_cast<std::size_t>(index)], m_scale), fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::SLIDER;
|
||
hoverStyleIndex = index;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (hoverStyle == StyleHit::NONE && m_tab != nullptr && m_tab->Style(m_tab->Selected()) != nullptr)
|
||
{
|
||
for (int index = 0; index < m_colorCount; ++index)
|
||
{
|
||
if (UiPointInRect(m_colorRects[static_cast<std::size_t>(index)], fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::COLOR;
|
||
hoverStyleIndex = index;
|
||
break;
|
||
}
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_customChipRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::CUSTOM_CHIP;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthThinRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::WIDTH_PRESET;
|
||
hoverStyleIndex = 0;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthThickRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::WIDTH_PRESET;
|
||
hoverStyleIndex = 1;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthMinusRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::WIDTH_MINUS;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthPlusRect, fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::WIDTH_PLUS;
|
||
}
|
||
if (hoverStyle == StyleHit::NONE)
|
||
{
|
||
for (int index = 0; index < kModeOptionCount; ++index)
|
||
{
|
||
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
|
||
{
|
||
hoverStyle = StyleHit::MODE;
|
||
hoverStyleIndex = index;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (hoverRow == m_hoverRow && hoverStyle == m_hoverStyle && hoverStyleIndex == m_hoverStyleIndex &&
|
||
part == m_hoverPart)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
m_hoverRow = hoverRow;
|
||
m_hoverPart = part;
|
||
m_hoverStyle = hoverStyle;
|
||
m_hoverStyleIndex = hoverStyleIndex;
|
||
return true;
|
||
}
|
||
|
||
bool SignalPanel::OnLButtonDown(int x, int y)
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const float fx = static_cast<float>(x);
|
||
const float fy = static_cast<float>(y);
|
||
|
||
// 滚动条拖拽优先于其它命中测试。
|
||
if (m_scrollBarVisible && UiPointInRect(m_scrollBarRect, fx, fy))
|
||
{
|
||
const gd::RectF thumb = ScrollThumbRect();
|
||
m_draggingScrollBar = true;
|
||
if (UiPointInRect(thumb, fx, fy))
|
||
{
|
||
m_scrollGrabOffset = y - static_cast<int>(thumb.Y);
|
||
}
|
||
else
|
||
{
|
||
// 点击空白区域时滑块中心对齐鼠标位置。
|
||
m_scrollGrabOffset = static_cast<int>(thumb.Height * 0.5f);
|
||
const float travel = fy - m_scrollBarRect.Y - static_cast<float>(m_scrollGrabOffset);
|
||
const float usable = m_scrollBarRect.Height - thumb.Height;
|
||
if (usable > 0.0f)
|
||
{
|
||
const float progress = std::max(0.0f, std::min(1.0f, travel / usable));
|
||
m_scrollOffset = progress * (m_contentHeight - m_listRect.Height);
|
||
ClampScroll();
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 自定义颜色滑块:开始拖拽(点击轨道任意处即跳转并进入拖拽)。
|
||
if (m_colorPickerOpen)
|
||
{
|
||
for (int index = 0; index < 3; ++index)
|
||
{
|
||
if (UiPointInRect(SliderHitRect(m_sliderRects[static_cast<std::size_t>(index)], m_scale), fx, fy))
|
||
{
|
||
m_draggingSlider = index;
|
||
m_pickerChannels[static_cast<std::size_t>(index)] =
|
||
UiSliderRatioFromX(m_sliderRects[static_cast<std::size_t>(index)], fx);
|
||
if (SignalStyle* style = m_tab->Style(m_tab->Selected()))
|
||
{
|
||
const unsigned int r = static_cast<unsigned int>(m_pickerChannels[0] * 255.0 + 0.5);
|
||
const unsigned int g = static_cast<unsigned int>(m_pickerChannels[1] * 255.0 + 0.5);
|
||
const unsigned int b = static_cast<unsigned int>(m_pickerChannels[2] * 255.0 + 0.5);
|
||
style->color = 0xFF000000u | (r << 16) | (g << 8) | b;
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 全部显示/隐藏。
|
||
if (UiPointInRect(m_toggleAllRect, fx, fy))
|
||
{
|
||
m_tab->SetAllVisible(*m_dataSet, !AreAllSignalsVisible());
|
||
m_rowsDirty = true;
|
||
return true;
|
||
}
|
||
|
||
// 样式编辑区。
|
||
if (SignalStyle* style = m_tab->Style(m_tab->Selected()))
|
||
{
|
||
int paletteCount = 0;
|
||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||
for (int index = 0; index < m_colorCount; ++index)
|
||
{
|
||
if (UiPointInRect(m_colorRects[static_cast<std::size_t>(index)], fx, fy))
|
||
{
|
||
style->color = palette[index % paletteCount];
|
||
return true;
|
||
}
|
||
}
|
||
if (UiPointInRect(m_customChipRect, fx, fy))
|
||
{
|
||
return OpenColorPicker(*style);
|
||
}
|
||
if (UiPointInRect(m_widthThinRect, fx, fy))
|
||
{
|
||
style->lineWidth = kWidthThinPreset;
|
||
return true;
|
||
}
|
||
if (UiPointInRect(m_widthThickRect, fx, fy))
|
||
{
|
||
style->lineWidth = kWidthThickPreset;
|
||
return true;
|
||
}
|
||
if (UiPointInRect(m_widthMinusRect, fx, fy))
|
||
{
|
||
return StepLineWidth(-kWidthStep);
|
||
}
|
||
if (UiPointInRect(m_widthPlusRect, fx, fy))
|
||
{
|
||
return StepLineWidth(kWidthStep);
|
||
}
|
||
for (int index = 0; index < kModeOptionCount; ++index)
|
||
{
|
||
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
|
||
{
|
||
style->mode = kModeOptions[index];
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 点击样式区内其它位置(颜色面板之外)时收起自定义颜色面板。
|
||
if (m_colorPickerOpen && !UiPointInRect(m_pickerRect, fx, fy))
|
||
{
|
||
return CloseTransientUi();
|
||
}
|
||
}
|
||
|
||
// 列表行。
|
||
RowPart part = RowPart::NONE;
|
||
const int row = HitRow(x, y, part);
|
||
if (row < 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||
|
||
if (item.kind == RowKind::FILE)
|
||
{
|
||
if (part == RowPart::CHECK)
|
||
{
|
||
// 分组勾选:全选则清空,否则全选。
|
||
const bool allVisible = m_tab->FileAllVisible(*m_dataSet, item.fileIndex);
|
||
m_tab->SetFileVisible(*m_dataSet, item.fileIndex, !allVisible);
|
||
m_rowsDirty = true;
|
||
return true;
|
||
}
|
||
if (part == RowPart::CLOSE)
|
||
{
|
||
// 关闭文件属于数据层操作,交由宿主窗口处理。
|
||
if (m_host != nullptr)
|
||
{
|
||
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(item.fileIndex), 0);
|
||
}
|
||
return true;
|
||
}
|
||
// 折叠三角与行主体均视为切换折叠。
|
||
m_tab->ToggleCollapsed(item.fileIndex);
|
||
m_rowsDirty = true;
|
||
return true;
|
||
}
|
||
|
||
const SignalRef ref{item.fileIndex, item.signalIndex};
|
||
m_tab->SetSelected(ref);
|
||
|
||
if (part == RowPart::CHECK)
|
||
{
|
||
// 勾选/取消勾选会立即触发网格重新划分(取消后空网格随之消失)。
|
||
m_tab->ToggleVisible(*m_dataSet, ref);
|
||
m_rowsDirty = true;
|
||
}
|
||
else if (part == RowPart::LANE_BADGE)
|
||
{
|
||
const gd::RectF badge = SignalBadgeRect(row);
|
||
OpenLaneMenu(ref, static_cast<int>(badge.X), static_cast<int>(badge.Y + badge.Height));
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool SignalPanel::OnLButtonUp(int /*x*/, int /*y*/)
|
||
{
|
||
if (m_draggingSlider >= 0)
|
||
{
|
||
m_draggingSlider = -1;
|
||
return true;
|
||
}
|
||
if (m_draggingScrollBar)
|
||
{
|
||
m_draggingScrollBar = false;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool SignalPanel::OnDoubleClick(int x, int y)
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
RowPart part = RowPart::NONE;
|
||
const int row = HitRow(x, y, part);
|
||
if (row < 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||
if (item.kind == RowKind::FILE)
|
||
{
|
||
m_tab->ToggleCollapsed(item.fileIndex);
|
||
m_rowsDirty = true;
|
||
return true;
|
||
}
|
||
|
||
const SignalRef ref{item.fileIndex, item.signalIndex};
|
||
m_tab->SetSelected(ref);
|
||
m_tab->ToggleVisible(*m_dataSet, ref);
|
||
m_rowsDirty = true;
|
||
return true;
|
||
}
|
||
|
||
bool SignalPanel::OnRButtonDown(int x, int y)
|
||
{
|
||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
RowPart part = RowPart::NONE;
|
||
const int row = HitRow(x, y, part);
|
||
if (row < 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||
if (item.kind == RowKind::FILE)
|
||
{
|
||
OpenFileMenu(item.fileIndex, x, y);
|
||
return true;
|
||
}
|
||
|
||
const SignalRef ref{item.fileIndex, item.signalIndex};
|
||
m_tab->SetSelected(ref);
|
||
OpenSignalMenu(ref, x, y);
|
||
return true;
|
||
}
|
||
|
||
bool SignalPanel::OnMouseWheel(int delta)
|
||
{
|
||
if (m_dataSet == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const float previous = m_scrollOffset;
|
||
m_scrollOffset -= static_cast<float>(delta) / 120.0f * 54.0f * m_scale;
|
||
ClampScroll();
|
||
return std::fabs(previous - m_scrollOffset) > 0.01f;
|
||
}
|
||
|
||
bool SignalPanel::OnMouseLeave()
|
||
{
|
||
if (m_hoverRow < 0 && m_hoverStyle == StyleHit::NONE)
|
||
{
|
||
return false;
|
||
}
|
||
m_hoverRow = -1;
|
||
m_hoverPart = RowPart::NONE;
|
||
m_hoverStyle = StyleHit::NONE;
|
||
m_hoverStyleIndex = -1;
|
||
return true;
|
||
}
|
||
|
||
void SignalPanel::OpenSignalMenu(const SignalRef& ref, int x, int y)
|
||
{
|
||
if (m_menu == nullptr || m_tab == nullptr || m_dataSet == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const SignalStyle* style = m_tab->Style(ref);
|
||
const SignalData* signal = m_dataSet->Resolve(ref);
|
||
if (style == nullptr || signal == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
m_menuTargetSignal = ref;
|
||
m_menuTargetFile = -1;
|
||
|
||
std::vector<MenuItem> items;
|
||
|
||
MenuItem header;
|
||
header.kind = MenuItemKind::HEADER;
|
||
header.label = signal->name;
|
||
items.push_back(header);
|
||
|
||
MenuItem toggle;
|
||
toggle.label = style->visible ? L"隐藏该信号" : L"显示该信号";
|
||
toggle.id = CMD_SIGNAL_TOGGLE;
|
||
items.push_back(toggle);
|
||
|
||
MenuItem solo;
|
||
solo.label = L"仅显示该信号";
|
||
solo.id = CMD_SIGNAL_SOLO;
|
||
items.push_back(solo);
|
||
|
||
MenuItem showAll;
|
||
showAll.label = L"显示全部信号";
|
||
showAll.id = CMD_SIGNAL_SHOW_ALL;
|
||
items.push_back(showAll);
|
||
|
||
MenuItem hideAll;
|
||
hideAll.label = L"隐藏全部信号";
|
||
hideAll.id = CMD_SIGNAL_HIDE_ALL;
|
||
items.push_back(hideAll);
|
||
|
||
MenuItem separator;
|
||
separator.kind = MenuItemKind::SEPARATOR;
|
||
items.push_back(separator);
|
||
|
||
int paletteCount = 0;
|
||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||
MenuItem colors;
|
||
colors.kind = MenuItemKind::SWATCHES;
|
||
colors.id = CMD_COLOR_BASE;
|
||
colors.swatchCount = std::min(paletteCount, 16);
|
||
colors.checkedSwatch = -1;
|
||
for (int index = 0; index < colors.swatchCount; ++index)
|
||
{
|
||
colors.swatches[index] = palette[index];
|
||
if (style->color == palette[index])
|
||
{
|
||
colors.checkedSwatch = index;
|
||
}
|
||
}
|
||
items.push_back(colors);
|
||
|
||
MenuItem modeHeader;
|
||
modeHeader.kind = MenuItemKind::HEADER;
|
||
modeHeader.label = L"样式";
|
||
items.push_back(modeHeader);
|
||
|
||
for (int index = 0; index < kModeOptionCount; ++index)
|
||
{
|
||
MenuItem mode;
|
||
mode.label = DrawModeName(kModeOptions[index]);
|
||
mode.id = CMD_MODE_BASE + index;
|
||
mode.checked = (style->mode == kModeOptions[index]);
|
||
items.push_back(mode);
|
||
}
|
||
|
||
const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
|
||
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items);
|
||
}
|
||
|
||
void SignalPanel::OpenFileMenu(int fileIndex, int x, int y)
|
||
{
|
||
if (m_menu == nullptr || m_tab == nullptr || m_dataSet == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const DataFile* file = m_dataSet->FileAt(fileIndex);
|
||
if (file == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
m_menuTargetFile = fileIndex;
|
||
m_menuTargetSignal = SignalRef();
|
||
|
||
std::vector<MenuItem> items;
|
||
|
||
MenuItem header;
|
||
header.kind = MenuItemKind::HEADER;
|
||
header.label = file->fileName;
|
||
items.push_back(header);
|
||
|
||
MenuItem showAll;
|
||
showAll.label = L"选中该文件全部信号";
|
||
showAll.id = CMD_FILE_SHOW_ALL;
|
||
items.push_back(showAll);
|
||
|
||
MenuItem hideAll;
|
||
hideAll.label = L"取消该文件全部信号";
|
||
hideAll.id = CMD_FILE_HIDE_ALL;
|
||
items.push_back(hideAll);
|
||
|
||
MenuItem separator;
|
||
separator.kind = MenuItemKind::SEPARATOR;
|
||
items.push_back(separator);
|
||
|
||
MenuItem remove;
|
||
remove.label = L"关闭该文件(从工作区移除)";
|
||
remove.id = CMD_FILE_REMOVE;
|
||
items.push_back(remove);
|
||
|
||
const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
|
||
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items);
|
||
}
|
||
|
||
void SignalPanel::OpenLaneMenu(const SignalRef& ref, int x, int y)
|
||
{
|
||
if (m_menu == nullptr || m_tab == nullptr || m_dataSet == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const SignalData* signal = m_dataSet->Resolve(ref);
|
||
if (signal == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
m_menuTargetSignal = ref;
|
||
m_menuTargetFile = -1;
|
||
|
||
std::vector<MenuItem> items;
|
||
|
||
MenuItem header;
|
||
header.kind = MenuItemKind::HEADER;
|
||
header.label = L"所属网格";
|
||
items.push_back(header);
|
||
|
||
const std::vector<Lane>& lanes = m_tab->Lanes();
|
||
const int currentLane = m_tab->LaneOfSignal(ref);
|
||
for (std::size_t index = 0; index < lanes.size(); ++index)
|
||
{
|
||
MenuItem lane;
|
||
lane.label = L"网格 " + std::to_wstring(index + 1) + L"(" +
|
||
std::to_wstring(lanes[index].signals.size()) + L" 条信号)";
|
||
lane.id = CMD_LANE_BASE + static_cast<int>(index);
|
||
lane.checked = (currentLane == static_cast<int>(index));
|
||
items.push_back(lane);
|
||
}
|
||
|
||
MenuItem separator;
|
||
separator.kind = MenuItemKind::SEPARATOR;
|
||
items.push_back(separator);
|
||
|
||
MenuItem create;
|
||
create.label = L"新建网格";
|
||
create.id = CMD_LANE_NEW;
|
||
items.push_back(create);
|
||
|
||
const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
|
||
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items);
|
||
}
|
||
|
||
bool SignalPanel::OnMenuCommand(const MenuResult& result)
|
||
{
|
||
if (m_tab == nullptr || m_dataSet == nullptr || !result.valid)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
bool needRelayout = false;
|
||
|
||
switch (result.id)
|
||
{
|
||
case CMD_SIGNAL_TOGGLE:
|
||
if (m_menuTargetSignal.IsValid())
|
||
{
|
||
m_tab->ToggleVisible(*m_dataSet, m_menuTargetSignal);
|
||
m_rowsDirty = true;
|
||
needRelayout = true;
|
||
}
|
||
break;
|
||
case CMD_SIGNAL_SOLO:
|
||
if (m_menuTargetSignal.IsValid())
|
||
{
|
||
m_tab->SoloSignal(*m_dataSet, m_menuTargetSignal);
|
||
m_rowsDirty = true;
|
||
needRelayout = true;
|
||
}
|
||
break;
|
||
case CMD_SIGNAL_SHOW_ALL:
|
||
m_tab->SetAllVisible(*m_dataSet, true);
|
||
m_rowsDirty = true;
|
||
needRelayout = true;
|
||
break;
|
||
case CMD_SIGNAL_HIDE_ALL:
|
||
m_tab->SetAllVisible(*m_dataSet, false);
|
||
m_rowsDirty = true;
|
||
needRelayout = true;
|
||
break;
|
||
case CMD_FILE_SHOW_ALL:
|
||
if (m_menuTargetFile >= 0)
|
||
{
|
||
m_tab->SetFileVisible(*m_dataSet, m_menuTargetFile, true);
|
||
m_rowsDirty = true;
|
||
needRelayout = true;
|
||
}
|
||
break;
|
||
case CMD_FILE_HIDE_ALL:
|
||
if (m_menuTargetFile >= 0)
|
||
{
|
||
m_tab->SetFileVisible(*m_dataSet, m_menuTargetFile, false);
|
||
m_rowsDirty = true;
|
||
needRelayout = true;
|
||
}
|
||
break;
|
||
case CMD_FILE_REMOVE:
|
||
if (m_menuTargetFile >= 0 && m_host != nullptr)
|
||
{
|
||
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(m_menuTargetFile), 0);
|
||
}
|
||
break;
|
||
case CMD_LANE_NEW:
|
||
if (m_menuTargetSignal.IsValid())
|
||
{
|
||
m_tab->MoveSignalToLane(*m_dataSet, m_menuTargetSignal, -1);
|
||
m_rowsDirty = true;
|
||
needRelayout = true;
|
||
}
|
||
break;
|
||
default:
|
||
if (result.id >= CMD_LANE_BASE && result.id < CMD_LANE_NEW)
|
||
{
|
||
if (m_menuTargetSignal.IsValid())
|
||
{
|
||
m_tab->MoveSignalToLane(*m_dataSet, m_menuTargetSignal, result.id - CMD_LANE_BASE);
|
||
m_rowsDirty = true;
|
||
needRelayout = true;
|
||
}
|
||
}
|
||
else if (result.id == CMD_COLOR_BASE && result.swatchIndex >= 0)
|
||
{
|
||
if (SignalStyle* style = m_tab->Style(m_menuTargetSignal))
|
||
{
|
||
int paletteCount = 0;
|
||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||
style->color = palette[result.swatchIndex % std::max(1, paletteCount)];
|
||
}
|
||
}
|
||
else if (result.id >= CMD_MODE_BASE && result.id < CMD_MODE_BASE + kModeOptionCount)
|
||
{
|
||
if (SignalStyle* style = m_tab->Style(m_menuTargetSignal))
|
||
{
|
||
style->mode = kModeOptions[result.id - CMD_MODE_BASE];
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
return needRelayout;
|
||
}
|
||
|
||
} // namespace jdi
|