一、采样间隔/频率显示不一致(用户报告 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 探针,仅编译验证 + 源码级核对。
668 lines
23 KiB
C++
668 lines
23 KiB
C++
// 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
|