fix(ui): 选区截取面板空白真正根因、拖拽指示线层序、新建分组修复、避让动画更顺

一、选区截取面板「条目区与按钮之间的空白」(连续三轮未修好的那个)
根因不是内容区几何,而是**底部固定区高度算了两行**:footerHeight 用
`buttonHeight * 2 + 6 + pad` 预留,而布局把「截取为新信号 / 原始信号截取」
两个按钮并排画在同一行 ⇒ 内容区下边界与按钮上边界之间空出一整行。
改为按一行算(两处:高度算式分支 + 布局几何分支同步修改)。
数值核对(scale=1.25、1 条可见信号):GAP 53.8px → 8.8px(= 一个 pad 的预期留白)。

二、拖拽插入指示线不再盖住被拖条目
PaintSignalDragGhost 的绘制顺序改为「插入指示线 → 阴影 → 幽灵行 → 描边」,
指示线始终位于跟随鼠标的那部分之下。

三、「移动到…」二级菜单
* 条目文案改为「组号 | 组名」(不再是括号形式)。
* 修复「新建分组」点了没反应:DataSet::AddGroup 拒收「无信号且无时间轴」的空分组,
  旧实现「先建空组再搬信号」必然在建组那一步失败。新增
  DataSet::MoveSignalToNewGroup 把建组 + 搬运时间轴 + 搬运信号做成原子操作(失败回滚)。
* TabConfig::RemapAfterSignalMove 在目标分组是新建时自行扩展 m_visible
  (调用方不再先 SyncToDataSet —— 那次同步会按旧下标清理引用)。
* Application::MoveSignalCore 增加 newGroupName 参数并返回 bool,避免失败仍报"已移动"。

四、信号拖拽避让动画更流畅
* 插值系数按真实帧间隔计算:1 - exp(-dt/22ms),dt 夹在 1~60ms,帧间隔抖动时不再忽快忽慢。
* 鼠标移动事件里也推进一次插值,快速拖动时让位效果跟得上指针。

验证:删 .o 全量重编译零 error / 零 warning,产物 875,008 B(< 1MB 预警阈值)。
本轮未占用鼠标/键盘/屏幕,未运行任何 GUI 探针;面板空白改为纯数值算式核对。
规则已同步到 Documents/agent_mem.md(含"改面板几何必须数值验证"的教训条目)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-03 20:09:01 +08:00
1 parent 97b361dedd
commit 328a3b3b59
10 files changed
+259 -75

No files matched your search

+5 -2
View File
@@ -255,10 +255,13 @@ namespace jdi
*
* @param [IN] fromGroup 源分组下标。
* @param [IN] fromSignal 源分组内的信号下标。
* @param [IN] toGroup 目标分组下标。
* @param [IN] toGroup 目标分组下标(负数 + `newGroupName` 非空 = 新建分组)。
* @param [IN] toIndex 目标组内下标(负数表示追加到末尾)。
* @param [IN] newGroupName 非空且 `toGroup < 0` 时,新建该名称的分组并把信号搬进去。
* @return 确实发生了移动返回 true。
*/
void MoveSignalCore(int fromGroup, int fromSignal, int toGroup, int toIndex);
bool MoveSignalCore(int fromGroup, int fromSignal, int toGroup, int toIndex,
const std::wstring* newGroupName);
/**
* @brief 信号列表拖拽排序的提交入口(组内重排或跨组移动)。
@@ -272,22 +272,36 @@ namespace jdi
return base;
}
void Application::MoveSignalCore(int fromGroup, int fromSignal, int toGroup, int toIndex)
bool Application::MoveSignalCore(int fromGroup, int fromSignal, int toGroup, int toIndex,
const std::wstring* newGroupName)
{
const SignalGroup* sourceGroup = m_dataSet.GroupAt(fromGroup);
const SignalGroup* targetGroup = m_dataSet.GroupAt(toGroup);
if (sourceGroup == nullptr || targetGroup == nullptr || fromSignal < 0 ||
if (sourceGroup == nullptr || fromSignal < 0 ||
static_cast<std::size_t>(fromSignal) >= sourceGroup->signals.size())
{
return;
return false;
}
const bool sameGroup = (fromGroup == toGroup);
// 「移动到全新分组」:目标分组尚不存在,它会被追加到分组列表末尾
//(下标 = 当前分组数,可在此之前预先算出,供引用改写使用)。
const bool createGroup = (toGroup < 0) && (newGroupName != nullptr);
const int resolvedTo = createGroup ? static_cast<int>(m_dataSet.GroupCount()) : toGroup;
const SignalGroup* targetGroup = createGroup ? nullptr : m_dataSet.GroupAt(resolvedTo);
if (!createGroup && targetGroup == nullptr)
{
return false;
}
const bool sameGroup = !createGroup && (fromGroup == resolvedTo);
// 目标下标:负数/越界表示追加到末尾(组内重排时"末尾"= 组内信号数-1,因为该信号会先被摘出)。
int position = toIndex;
const int targetCount = static_cast<int>(targetGroup->signals.size());
if (sameGroup)
const int targetCount = createGroup ? 0 : static_cast<int>(targetGroup->signals.size());
if (createGroup)
{
position = 0;
}
else if (sameGroup)
{
if (position < 0 || position >= targetCount)
{
@@ -295,7 +309,7 @@ namespace jdi
}
if (position == fromSignal)
{
return;
return false;
}
}
else if (position < 0 || position > targetCount)
@@ -307,36 +321,30 @@ namespace jdi
from.groupIndex = fromGroup;
from.signalIndex = fromSignal;
SignalRef to;
to.groupIndex = toGroup;
to.groupIndex = resolvedTo;
to.signalIndex = position;
const bool visible = m_tabs.empty() ? false : m_tabs.front().IsVisible(from);
// 移动会搬运大量采样缓冲:先暂停后台派生曲线计算,避免其读取正在被移动的内存。
m_plot.PauseDerivedAsync();
// 跨组且目标分组是多标签页里"新出现"的(例如刚建的分组)时,先补齐可见性表列,
// 否则 `RemapAfterSignalMove` 无法在新组里插入"沿用源可见性"的那一列,
// 移动过去后信号会变成未勾选、曲线直接消失。
if (!sameGroup)
{
for (TabConfig& tab : m_tabs)
{
tab.SyncToDataSet(m_dataSet);
}
for (TabConfig& tab : m_tabs)
{
tab.RemapAfterSignalMove(from, to, tab.IsVisible(from));
}
}
else
// 先按**旧下标**修正各标签页内的全部信号引用,再动数据层。
// ⚠️ 跨组(含新建分组)时目标分组那一行可见性表可能还不存在,
// `RemapAfterSignalMove` 内部会自行扩展 —— 这里**不能**先 `SyncToDataSet`,
// 因为那次同步会按旧下标清理/修正引用,随后下标一变就会指向错误的信号。
if (sameGroup)
{
for (TabConfig& tab : m_tabs)
{
tab.RemapAfterSignalReorder(fromGroup, fromSignal, position);
}
}
(void)visible;
else
{
for (TabConfig& tab : m_tabs)
{
tab.RemapAfterSignalMove(from, to, tab.IsVisible(from));
}
}
// 共享选中项:被移动的正是当前选中信号时,改为指向新位置。
if (m_dataSet.Selected().groupIndex == fromGroup)
@@ -372,15 +380,32 @@ namespace jdi
}
int newSignalIndex = -1;
// 参数已在此前校验过(分组与信号下标都合法),本调用不会失败。
m_dataSet.MoveSignal(fromGroup, fromSignal, toGroup, position, newSignalIndex);
int newGroupIndex = -1;
if (createGroup)
{
// 建组 + 搬运做成一次原子操作(`AddGroup` 拒收空组,见 `MoveSignalToNewGroup`)。
if (!m_dataSet.MoveSignalToNewGroup(fromGroup, fromSignal, *newGroupName, newGroupIndex,
newSignalIndex))
{
// 前面已校验过,理论不可达;若真发生(数据层拒绝),必须把已改写的引用
// 按数据层的真实状态重新对齐,避免留下指向不存在分组的悬空引用。
SyncTabsToDataSet();
return false;
}
}
else
{
// 参数已在此前校验过(分组与信号下标都合法),本调用不会失败。
m_dataSet.MoveSignal(fromGroup, fromSignal, resolvedTo, position, newSignalIndex);
}
SyncTabsToDataSet();
return true;
}
void Application::ReorderSignal(int fromGroup, int fromSignal, int toGroup, int toIndex)
{
MoveSignalCore(fromGroup, fromSignal, toGroup, toIndex);
(void)MoveSignalCore(fromGroup, fromSignal, toGroup, toIndex, nullptr);
}
void Application::MoveSignalToGroup(int fromGroup, int fromSignal, int toGroup)
@@ -393,37 +418,34 @@ namespace jdi
}
const std::wstring signalName = sourceGroup->signals[static_cast<std::size_t>(fromSignal)].name;
// 「新建分组」:先追加一个空分组(名字不重复),再按普通跨组移动处理。
int targetGroup = toGroup;
if (targetGroup < 0)
if (toGroup < 0)
{
SignalGroup group;
group.name = UniqueGroupName(L"新建分组");
if (!m_dataSet.AddGroup(std::move(group), targetGroup))
// 「移动到… → 新建分组」:组名固定为「新建分组」(重名自动加序号)。
const std::wstring name = UniqueGroupName(L"新建分组");
if (MoveSignalCore(fromGroup, fromSignal, -1, 0, &name))
{
return;
SetStatusMessage(L"已将信号「" + signalName + L"」移动到新建分组:" + name, false);
}
return;
}
if (targetGroup == fromGroup)
if (toGroup == fromGroup)
{
return; // 已经在该分组,无需移动
}
const SignalGroup* targetGroupData = m_dataSet.GroupAt(targetGroup);
if (targetGroupData == nullptr)
if (!MoveSignalCore(fromGroup, fromSignal, toGroup, -1, nullptr))
{
return;
}
MoveSignalCore(fromGroup, fromSignal, targetGroup,
static_cast<int>(targetGroupData->signals.size()));
const SignalGroup* destination = m_dataSet.GroupAt(targetGroup);
const SignalGroup* destination = m_dataSet.GroupAt(toGroup);
SetStatusMessage(L"已将信号「" + signalName + L"」移动到分组:" +
(destination != nullptr ? destination->name : std::wstring()),
false);
}
void Application::UpdateWindowTitle() {
void Application::UpdateWindowTitle()
{
// 标题栏 = 应用名称 + 分隔竖线 + 内存占用(单位在 KB/MB/GB 间自适应,保留两位小数)。
// 除内存占用外不再附带标签页、文件数量等无关信息。
// ⚠️ 内存口径 = **私有工作集**(与任务管理器"内存"列一致),不是总工作集。