Files
Just-DataInspector/Components/SignalPanel/Src/SignalPanelMenu.cpp
T
DeepSeek V4.1 Flash 97b361dedd fix(ui): 采样统计显示一致化、GX 拖拽在布局边界下可用、信号「移动到…」与拖拽排序
一、采样间隔/频率显示不一致(用户报告 1ms vs 1000us、100us 0ns)
* FormatDurationCompound 改为「先量化到整数皮秒、再整数分解」,
  彻底消除同一数值因浮点噪声落到不同单位/留下零尾数的问题;
  秒级分支与亚秒分支共用 totalPs 方案,次级进位/单位升级集中在一处。
* FormatFrequency 的单位选择改为按「舍入后的显示值」判定(999.9999999Hz
  与 1000Hz 均为 1.000kHz),并做向下校正避免 0.001kHz。

二、选区截取面板
* 提示文本改为用户指定原文「启用双光标模式以进行选区截取」。
* EdgePanelWidth 计入该提示的实际文本宽度;绘制改用 UiTextClipped 不再省略。
* viewHeight 补回漏减的 bottomPad,修掉条目区与按钮之间的空白。

三、未展开的信号条目栏悬停不再变成手型光标。

四、开启「显示布局边界」后 GX 标签可正常拖拽排序
* 抽出 PaintLaneReorderOverlay(吸附槽位 + 被拖轮廓),贴图与整幅重绘共用。
* 整幅重绘在 LANE_REORDER 时按 LaneDragOffset 逐格平移重画(被拖网格最后画),
  修掉调试模式下不跟随鼠标、无吸附提示、只能盲拖的问题。

五、信号右键新增「移动到…」二级菜单(位于「仅显示该信号」之上)
* 条目为「分组 N(组名)」,当前组打勾;无标题行;底部「新建分组」。
* MenuItem::submenuMaxRows + PopupMenu 独立子菜单滚动(滚动条、滚轮、视口命中过滤),
  IsScrollable 计入子菜单可滚状态。
* 数据层 DataSet::MoveSignalToGroup/MoveSignal 让时间轴随信号搬运
  (独占则移动、共享则复制);TabConfig::RemapAfterSignalMove 改写引用而非先删后插。
* Application::MoveSignalCore 统一入口,保证派生计算暂停、引用修正、选中迁移与同步一致。

六、信号列表支持按住拖拽排序(组内重排 / 跨组移动)
* 4px 阈值进入拖拽;条目脱离原位跟随鼠标(阴影 + 强调色描边 + 插入指示线)。
* 其它信号行/分组行按插入位置做避让位移动画(RowRect 统一叠加插值位移,
  宿主 kScrollTimerId 驱动 AdvanceSignalDragAnimation)。
* 数据层 DataSet::MoveSignal 支持任意位置;TabConfig::RemapAfterSignalReorder 线性重映射。
* 拖拽中禁用组名悬浮;双击改名与 InvalidateRows 均正确终止拖拽态。

验证:删 .o 全量重编译零 error / 零 warning;产物 873,984 B(< 1MB 预警阈值)。
本轮未使用鼠标/键盘/屏幕,未运行任何 GUI 探针,仅编译验证 + 源码级核对。
2026-11-03 19:27:43 +08:00

668 lines
23 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.
// SignalPanel 实现分片 —— 弹出菜单(系数/偏移、镜像、信号/分组/网格右键菜单、命令回执)。
//
// 本文件只包含 SignalPanel 的成员函数定义(机械搬移,行为不变)。
// ⚠️ 菜单是"打开即模态"的浮层:`CloseTransientUi` 必须在点击别处时收起它,
// 否则菜单会一直挂在那里吃掉后续点击。
#include "SignalPanelInternal.hpp"
namespace jdi
{
bool SignalPanel::OpenGainMenu()
{
if (m_menu == nullptr || m_tab == nullptr)
{
return false;
}
const SignalStyle* style = StyleOf(SelectedRef());
if (style == nullptr)
{
return false;
}
// 二级面板:三组“标题(带还原图标)+ 输入框”的组合:
// 1. 「公式映射」:`Y =` + 表达式输入框(自变量写作 f(t));
// 2. 「系数调整」:一整行系数输入框;
// 3. 「偏移调整」:下方两行分别为「Y轴」与「时间轴」的偏移输入框。
//(与网格 Y 轴上下限的菜单内联编辑同一套做法:提交后面板保持打开,可连续微调。)
// ⚠️ 结算顺序固定为「公式映射 → 系数 → 偏移」(用户 2026-11-02 明确要求)。
std::vector<MenuItem> items;
// 「公式映射」:默认 `f(t)`(= 等值照搬)。
MenuItem formulaHeader;
formulaHeader.kind = MenuItemKind::HEADER_ACTION;
formulaHeader.label = L"公式映射";
formulaHeader.id = CMD_FORMULA_RESET; // 点击还原图标 → 恢复为 f(t)
items.push_back(formulaHeader);
const std::wstring formulaText =
style->formula.empty() ? std::wstring(L"f(t)") : style->formula;
MenuItem formulaEdit;
formulaEdit.kind = MenuItemKind::EDIT;
// ⚠️ **`Y=` 前缀画在输入框**内部(非编辑态),编辑态只显示公式本身。
// 因此这里不写 `label`(空 label = 输入框占满整行,宽度与下方「系数调整」完全一致),
// 而是在值文本前拼上前缀;进入编辑时 `PopupMenu` 会改用 `editValue`(无前缀)。
formulaEdit.label.clear();
formulaEdit.id = CMD_FORMULA_EDIT;
formulaEdit.value = FormulaDisplayText(style->formula);
formulaEdit.editValue = formulaText;
formulaEdit.filter = EditFilter::ANY;
items.push_back(formulaEdit);
MenuItem gainHeader;
gainHeader.kind = MenuItemKind::HEADER_ACTION;
gainHeader.label = L"系数调整";
gainHeader.id = CMD_GAIN_RESET; // 点击标题栏右侧的还原图标 → 系数重置为 1
items.push_back(gainHeader);
// 非编辑态带正号(用户明确要求);进入编辑后的初始文本用不带符号的纯数值。
const std::wstring gainText = FormatGainText(style->gain);
const std::wstring gainEditText = FormatNumberText(static_cast<double>(style->gain));
MenuItem gainEdit;
gainEdit.kind = MenuItemKind::EDIT;
gainEdit.label.clear(); // 不显示说明文字:输入框独占整行
gainEdit.id = CMD_GAIN_EDIT;
gainEdit.value = gainText;
gainEdit.editValue = gainEditText;
gainEdit.filter = EditFilter::NUMBER;
items.push_back(gainEdit);
MenuItem offsetHeader;
offsetHeader.kind = MenuItemKind::HEADER_ACTION;
offsetHeader.label = L"偏移调整";
offsetHeader.id = CMD_OFFSET_RESET; // 点击还原图标 → 两个偏移都恢复默认值
items.push_back(offsetHeader);
// 「Y轴」:对绘制值加上一个常数偏移(波形上下移动),非编辑态带正负号。
MenuItem offsetY;
offsetY.kind = MenuItemKind::EDIT;
offsetY.label = L"Y轴";
offsetY.id = CMD_OFFSET_Y;
offsetY.value = FormatSignedNumber(static_cast<double>(style->offsetY));
offsetY.editValue = FormatNumberText(static_cast<double>(style->offsetY));
offsetY.filter = EditFilter::NUMBER;
items.push_back(offsetY);
// 「时间轴」:把采样沿时间轴平移(画在 t + offsetTime)。
// 非编辑态显示与双光标差值同一套复合单位文本,并**带正负号**(用户明确要求);
// 编辑态显示以秒为单位的数值,并在输入框最右侧提示单位 `s`。
MenuItem offsetTime;
offsetTime.kind = MenuItemKind::EDIT;
offsetTime.label = L"时间轴";
offsetTime.id = CMD_OFFSET_TIME;
offsetTime.value = FormatOffsetTimeText(style->offsetTime);
offsetTime.editValue = FormatNumberText(style->offsetTime);
offsetTime.editSuffix = L"s";
offsetTime.filter = EditFilter::NUMBER;
items.push_back(offsetTime);
// 锚定在按钮**左上角的上方一点**:二级面板**向上展开**,其左下角对准按钮左上角,
// 并再往上让出 `kAdjustMenuGap`(用户明确要求)。
//(旧实现在按钮下沿向右下展开,面板会盖住下方内容。)
const gd::RectF& button = m_adjustRects[kAdjustGain];
const gd::RectF bounds = PanelBounds();
const int anchorY =
static_cast<int>(std::lround(button.Y - kAdjustMenuGap * m_scale));
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, static_cast<int>(button.X), anchorY, items,
true);
// ⚠️ 菜单一旦打开就会接管鼠标事件,样式区不会再收到 WM_MOUSEMOVE →
// 必须在这里主动清掉按钮的悬停态,否则按钮会"像鼠标一直按着"一样持续高亮。
ClearStyleHover();
return true;
}
bool SignalPanel::OpenMirrorMenu()
{
if (m_menu == nullptr || m_tab == nullptr)
{
return false;
}
const SignalStyle* style = StyleOf(SelectedRef());
if (style == nullptr)
{
return false;
}
// 两行带复选框的选项:点击即切换、且**保持菜单打开**(可连续勾选两项)。
std::vector<MenuItem> items;
MenuItem horizontal;
horizontal.kind = MenuItemKind::NORMAL;
horizontal.label = L"水平镜像";
horizontal.id = CMD_MIRROR_HORIZONTAL;
horizontal.checked = style->mirrorHorizontal;
horizontal.keepOpen = true;
items.push_back(horizontal);
MenuItem vertical;
vertical.kind = MenuItemKind::NORMAL;
vertical.label = L"垂直镜像";
vertical.id = CMD_MIRROR_VERTICAL;
vertical.checked = style->mirrorVertical;
vertical.keepOpen = true;
items.push_back(vertical);
const gd::RectF& button = m_adjustRects[kAdjustMirror];
const gd::RectF bounds = PanelBounds();
// 与「系数/偏移」面板同一套锛定:向上展开,左下角 = 按钮左上角再上移一点。
const int anchorY =
static_cast<int>(std::lround(button.Y - kAdjustMenuGap * m_scale));
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, static_cast<int>(button.X), anchorY, items,
true);
ClearStyleHover();
return true;
}
void SignalPanel::ClearStyleHover()
{
if (m_hoverStyle == StyleHit::NONE && m_hoverStyleIndex < 0)
{
return;
}
m_hoverStyle = StyleHit::NONE;
m_hoverStyleIndex = -1;
}
bool SignalPanel::CloseTransientUi()
{
bool changed = false;
if (m_editField != StyleEditField::NONE)
{
changed = EndEdit(true) || changed;
}
// 点击面板之外也要结束「筛选信号」的编辑状态。
if (m_filterEditing)
{
changed = EndFilterEdit() || changed;
}
if (m_colorPicker != nullptr && m_colorPicker->IsOpen())
{
m_colorPicker->Close();
changed = true;
}
// 面板外点击:取消尚未抬起确认的关闭操作。
m_pendingClosePart = RowPart::NONE;
m_pendingCloseRow = -1;
m_pendingFilterClear = false;
m_pendingGainMenu = false;
m_pendingMirrorMenu = false;
m_pendingTargetLaneMenu = false;
return changed;
}
void SignalPanel::OpenSignalMenu(const SignalRef& ref, int x, int y)
{
if (m_menu == nullptr || m_tab == nullptr || m_dataSet == nullptr)
{
return;
}
const SignalStyle* style = StyleOf(ref);
const SignalData* signal = m_dataSet->Resolve(ref);
if (style == nullptr || signal == nullptr)
{
return;
}
(void)style; // 菜单已不再需要外观:颜色 / 线宽 / 透明度 / 绘制样式都在下方样式区编辑。
m_menuTargetSignal = ref;
m_menuTargetGroup = -1;
// ⚠️ 信号右键菜单**刻意保持极简**(用户 2026-10-29 要求):
// 颜色、绘制样式、可见性、显示全部/隐藏全部这些原来放在这里的条目,现在
// 都已被下方**样式自定义区**与分组右键菜单覆盖,重复的入口只会让菜单变长、变乱。
// 现在只保留三件事:
// * 「克隆该信号」(在「克隆该信号(带调整)」**之上**):原始数据 + 只沿用时间轴偏移;
// * 「克隆该信号(带调整)」:原始数据 + 沿用源信号的**全部**调整参数(用户 2026-11-03 要求);
// * 「仅显示该信号」。
std::vector<MenuItem> items;
MenuItem header;
header.kind = MenuItemKind::HEADER;
header.label = signal->name;
items.push_back(header);
MenuItem clone;
clone.label = L"克隆该信号";
clone.id = CMD_SIGNAL_CLONE;
items.push_back(clone);
MenuItem cloneAdjusted;
cloneAdjusted.label = L"克隆该信号(带调整)";
cloneAdjusted.id = CMD_SIGNAL_CLONE_ADJUSTED;
items.push_back(cloneAdjusted);
// 「移动到…」二级菜单(用户 2026-11-03 要求,位置在「仅显示该信号」**之上**):
// 条目为「分组 N(组名)」+ 当前所在分组打勾;末尾分割线之下是「新建分组」。
// 结构与信号名右侧 GX 徽标点击后的「所属网格」菜单保持一致,但**没有标题行**,
// 且底部动作换成「新建分组」(新建分组后把信号移进去)。
MenuItem moveMenu;
moveMenu.kind = MenuItemKind::SUBMENU;
moveMenu.label = L"移动到...";
moveMenu.id = CMD_GROUP_MOVE_BASE; // 父项本身不可点击,id 仅用于区分
for (std::size_t index = 0; index < m_dataSet->GroupCount(); ++index)
{
const SignalGroup* group = m_dataSet->GroupAt(static_cast<int>(index));
if (group == nullptr)
{
continue;
}
MenuItem entry;
entry.label = L"分组 " + std::to_wstring(index + 1) + L"(" + group->name + L")";
entry.id = CMD_GROUP_MOVE_BASE + static_cast<int>(index);
entry.checked = (static_cast<int>(index) == ref.groupIndex);
moveMenu.submenu.push_back(entry);
}
MenuItem moveSeparator;
moveSeparator.kind = MenuItemKind::SEPARATOR;
moveMenu.submenu.push_back(moveSeparator);
MenuItem moveNewGroup;
moveNewGroup.label = L"新建分组";
moveNewGroup.id = CMD_GROUP_MOVE_NEW;
moveMenu.submenu.push_back(moveNewGroup);
// 分组很多(例如一次加载上百个通道组)时二级菜单同样要能滚动:
// 与「所属网格」菜单同一上限(用户 2026-10-16 要求保留 10 行)。
moveMenu.submenuMaxRows = kLaneMenuVisibleRows;
items.push_back(moveMenu);
MenuItem solo;
solo.label = L"仅显示该信号";
solo.id = CMD_SIGNAL_SOLO;
items.push_back(solo);
const gd::RectF bounds = PanelBounds();
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items);
}
void SignalPanel::OpenGroupMenu(int groupIndex, int x, int y)
{
if (m_menu == nullptr || m_tab == nullptr || m_dataSet == nullptr)
{
return;
}
const SignalGroup* group = m_dataSet->GroupAt(groupIndex);
if (group == nullptr)
{
return;
}
m_menuTargetGroup = groupIndex;
m_menuTargetSignal = SignalRef();
std::vector<MenuItem> items;
MenuItem header;
header.kind = MenuItemKind::HEADER;
header.label = group->name;
items.push_back(header);
MenuItem showAll;
showAll.label = L"选中该分组全部信号";
showAll.id = CMD_GROUP_SHOW_ALL;
items.push_back(showAll);
MenuItem hideAll;
hideAll.label = L"取消该分组全部信号";
hideAll.id = CMD_GROUP_HIDE_ALL;
items.push_back(hideAll);
MenuItem separator;
separator.kind = MenuItemKind::SEPARATOR;
items.push_back(separator);
MenuItem remove;
remove.label = L"关闭该分组(从工作区移除)";
remove.id = CMD_GROUP_REMOVE;
items.push_back(remove);
const gd::RectF bounds = PanelBounds();
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_menuTargetGroup = -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 = PanelBounds();
// ⚠️ 网格可能是**上百个**(例如把 MF4 的全部信号各自放进独立网格):
// 不限制高度时菜单会长到屏幕之外、且逐条绘制上百行会明显卡顿。
// 这里限制可滚动条目区**最多显示 10 行**(用户 2026-10-16 要求):
// 超出部分用滚轮 / 右侧滚动条翻动,而顶部「所属网格」标题与底部的
// 「新建网格」**始终固定不动**。
MenuLayoutConstraint constraint;
constraint.maxScrollRows = kLaneMenuVisibleRows;
// 额外加一道硬上限:窗口很矮时也不得超出客户区(与行数上限取更严格者)。
constraint.maxHeight = std::max(180.0f, bounds.Height * 0.8f);
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items, false, constraint);
}
bool SignalPanel::OnMenuCommand(const MenuResult& result)
{
if (m_tab == nullptr || m_dataSet == nullptr || !result.valid)
{
return false;
}
// 「系数」/「偏移」二级面板里的输入框提交:就地更新,面板保持打开
//(与网格 Y 轴上下限的菜单内联编辑一致),并把夹取后的真实值回写到输入框上。
if (result.editCommit)
{
SignalStyle* style = StyleOf(SelectedRef());
if (style != nullptr)
{
const std::wstring trimmed = Trim(result.editText);
wchar_t* stop = nullptr;
const double parsed =
trimmed.empty() ? 0.0 : std::wcstod(trimmed.c_str(), &stop);
// 非法/空输入保持原值(只把真实值回写到输入框,避免用户看到错误的数字)。
const bool valid = !trimmed.empty() && stop != trimmed.c_str() && std::isfinite(parsed);
switch (result.id)
{
case CMD_FORMULA_EDIT:
{
// 「公式映射」:把自变量写法统一为 `f(t)`、常量统一为小写,
// 非法表达式**不写入**并给出警告(警告走自绘浮层,见 `TakePendingHint`;
// 不使用 Windows 消息框)。
// ⚠️ **等效于 `f(t)` 的公式按“未启用”处理**(清空):它逐点恒等,
// 但却会让 `HasFormula()` 为真 ⇒ 关闭粗粒度包络层、逼得自适应量程
// 每帧逐点扫描整条信号(千万级采样 ⇒ 界面完全卡死,用户 2026-11-03 报告)。
const std::wstring normalized = NormalizeFormulaText(trimmed);
if (normalized.empty() || FormulaIsIdentity(normalized))
{
style->formula.clear();
}
else
{
std::wstring error;
if (!ValidateFormula(normalized, &error))
{
m_pendingHint = L"公式无法解析:" + error;
}
else
{
style->formula = normalized;
}
}
if (m_menu != nullptr)
{
m_menu->UpdateItemValue(CMD_FORMULA_EDIT, FormulaDisplayText(style->formula),
FormulaEditText(style->formula));
}
return false;
}
case CMD_GAIN_EDIT:
{
if (valid)
{
style->gain =
static_cast<float>(std::max(-kGainLimit, std::min(parsed, kGainLimit)));
}
if (m_menu != nullptr)
{
m_menu->UpdateItemValue(CMD_GAIN_EDIT, FormatGainText(style->gain),
FormatNumberText(static_cast<double>(style->gain)));
}
return false;
}
case CMD_OFFSET_Y:
{
if (valid)
{
style->offsetY = static_cast<float>(
std::max(-static_cast<double>(kGainLimit), std::min(parsed, kGainLimit)));
}
if (m_menu != nullptr)
{
m_menu->UpdateItemValue(
CMD_OFFSET_Y, FormatSignedNumber(static_cast<double>(style->offsetY)),
FormatNumberText(static_cast<double>(style->offsetY)));
}
return false;
}
case CMD_OFFSET_TIME:
{
if (valid)
{
style->offsetTime =
std::max(-kGainLimit, std::min(parsed, kGainLimit));
}
if (m_menu != nullptr)
{
m_menu->UpdateItemValue(CMD_OFFSET_TIME,
FormatOffsetTimeText(style->offsetTime),
FormatNumberText(style->offsetTime));
}
return false;
}
default:
break;
}
}
else
{
return false;
}
}
// 标题栏右侧的「还原」图标:公式恢复为默认的 `f(t)`(= 等值照搬)。
if (result.id == CMD_FORMULA_RESET)
{
if (SignalStyle* style = StyleOf(SelectedRef()))
{
style->formula.clear();
if (m_menu != nullptr)
{
m_menu->UpdateItemValue(CMD_FORMULA_EDIT, FormulaDisplayText(style->formula),
FormulaEditText(style->formula));
}
}
return false;
}
// 标题栏右侧的「还原」图标:系数重置为默认值 1(面板保持打开,输入框立即回显)。
if (result.id == CMD_GAIN_RESET)
{
if (SignalStyle* style = StyleOf(SelectedRef()))
{
style->gain = 1.0f;
if (m_menu != nullptr)
{
m_menu->UpdateItemValue(CMD_GAIN_EDIT, FormatGainText(style->gain),
FormatNumberText(static_cast<double>(style->gain)));
}
}
return false;
}
// 「偏移调整」的「还原」图标:Y 与时间偏移都恢复为默认值 0。
if (result.id == CMD_OFFSET_RESET)
{
if (SignalStyle* style = StyleOf(SelectedRef()))
{
style->offsetY = 0.0f;
style->offsetTime = 0.0;
if (m_menu != nullptr)
{
m_menu->UpdateItemValue(CMD_OFFSET_Y, FormatSignedNumber(0.0),
FormatNumberText(0.0));
m_menu->UpdateItemValue(CMD_OFFSET_TIME, FormatOffsetTimeText(0.0),
FormatNumberText(0.0));
}
}
return false;
}
// 「镜像」二级面板:两行带复选框的切换项(面板保持打开,勾选状态就地回写)。
if (result.id == CMD_MIRROR_HORIZONTAL || result.id == CMD_MIRROR_VERTICAL)
{
if (SignalStyle* style = StyleOf(SelectedRef()))
{
const bool horizontal = (result.id == CMD_MIRROR_HORIZONTAL);
bool& flag = horizontal ? style->mirrorHorizontal : style->mirrorVertical;
flag = !flag;
if (!horizontal)
{
// ⚠️ Y 偏移以**未镜像空间**为准:垂直镜像开关变化时同步取反偏移量,
// 使绘制等价于「先加上偏移再镜像」(用户明确要求)。
style->offsetY = -style->offsetY;
if (m_menu != nullptr)
{
m_menu->UpdateItemValue(
CMD_OFFSET_Y, FormatSignedNumber(static_cast<double>(style->offsetY)),
FormatNumberText(static_cast<double>(style->offsetY)));
}
}
if (m_menu != nullptr)
{
m_menu->UpdateItemLabel(result.id, horizontal ? L"水平镜像" : L"垂直镜像", flag, true);
}
}
return false;
}
bool needRelayout = false;
switch (result.id)
{
case CMD_SIGNAL_CLONE:
// 「克隆该信号」:**不烘值**,原始数据 + 沿用源信号的时间轴偏移(用户 2026-11-03)。
// 具体构建在宿主里完成(需要访问 DataSet 与全部标签页)。
if (m_menuTargetSignal.IsValid() && m_host != nullptr)
{
PostMessageW(m_host, WM_APP + 5,
static_cast<WPARAM>(m_menuTargetSignal.groupIndex),
static_cast<LPARAM>(m_menuTargetSignal.signalIndex));
}
break;
case CMD_SIGNAL_CLONE_ADJUSTED:
// 「克隆该信号(带调整)」:**不烘值**,原始数据 + 沿用源信号的**全部**调整参数。
if (m_menuTargetSignal.IsValid() && m_host != nullptr)
{
PostMessageW(m_host, WM_APP + 6,
static_cast<WPARAM>(m_menuTargetSignal.groupIndex),
static_cast<LPARAM>(m_menuTargetSignal.signalIndex));
}
break;
case CMD_SIGNAL_SOLO:
if (m_menuTargetSignal.IsValid())
{
m_tab->SoloSignal(m_menuTargetSignal);
m_rowsDirty = true;
needRelayout = true;
}
break;
case CMD_GROUP_SHOW_ALL:
if (m_menuTargetGroup >= 0)
{
m_tab->SetGroupVisible(m_menuTargetGroup, true);
m_rowsDirty = true;
needRelayout = true;
}
break;
case CMD_GROUP_HIDE_ALL:
if (m_menuTargetGroup >= 0)
{
m_tab->SetGroupVisible(m_menuTargetGroup, false);
m_rowsDirty = true;
needRelayout = true;
}
break;
case CMD_GROUP_REMOVE:
if (m_menuTargetGroup >= 0 && m_host != nullptr)
{
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(m_menuTargetGroup), 0);
}
break;
case CMD_LANE_NEW:
if (m_menuTargetSignal.IsValid())
{
m_tab->MoveSignalToLane(*m_dataSet, m_menuTargetSignal, -1);
m_rowsDirty = true;
needRelayout = true;
}
break;
case CMD_GROUP_MOVE_NEW:
// 「移动到… → 新建分组」:由宿主新建分组并把信号移过去(需要访问全部标签页)。
if (m_menuTargetSignal.IsValid() && m_host != nullptr)
{
PostMessageW(m_host, WM_APP + 7,
static_cast<WPARAM>(m_menuTargetSignal.groupIndex),
static_cast<LPARAM>(((static_cast<LONG_PTR>(m_menuTargetSignal.signalIndex) << 16) |
0xFFFF)));
}
break;
default:
if (result.id >= CMD_GROUP_MOVE_BASE && result.id < CMD_GROUP_MOVE_NEW)
{
// 「移动到… → 分组 N」:同样交给宿主(跨分组移动会重建时间轴归属)。
if (m_menuTargetSignal.IsValid() && m_host != nullptr)
{
const int targetGroup = result.id - CMD_GROUP_MOVE_BASE;
if (targetGroup != m_menuTargetSignal.groupIndex)
{
PostMessageW(m_host, WM_APP + 7,
static_cast<WPARAM>(m_menuTargetSignal.groupIndex),
static_cast<LPARAM>(
(static_cast<LONG_PTR>(m_menuTargetSignal.signalIndex) << 16) |
(static_cast<LONG_PTR>(targetGroup) & 0xFFFF)));
}
}
}
else 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;
}
}
break;
}
return needRelayout;
}
} // namespace jdi