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 探针,仅编译验证 + 源码级核对。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-03 19:27:43 +08:00
1 parent 21a9865365
commit 97b361dedd
23 files changed
+1581 -74

No files matched your search

+60 -1
View File
@@ -232,6 +232,39 @@ namespace jdi
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;
@@ -589,8 +622,34 @@ namespace jdi
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_LANE_BASE && result.id < CMD_LANE_NEW)
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())
{