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:
1 parent
21a9865365
commit
97b361dedd
23 files changed
+1581
-74
No files matched your search
@@ -99,6 +99,26 @@ namespace jdi
|
||||
{
|
||||
return ApplyWidthFromX(static_cast<float>(x));
|
||||
}
|
||||
// 信号条目拖拽:按下后越过阈值才真正进入拖拽态(之前只是"可能是点击")。
|
||||
if (m_signalDragPending || m_signalDragging)
|
||||
{
|
||||
const int dx = x - m_signalDragStartX;
|
||||
const int dy = y - m_signalDragStartY;
|
||||
if (!m_signalDragging && (dx * dx + dy * dy) >= 16)
|
||||
{
|
||||
m_signalDragging = true;
|
||||
m_signalDragShift.assign(m_rows.size(), 0.0f);
|
||||
m_signalDragShiftCur.assign(m_rows.size(), 0.0f);
|
||||
m_hoverRow = -1;
|
||||
m_hoverPart = RowPart::NONE;
|
||||
}
|
||||
if (m_signalDragging)
|
||||
{
|
||||
UpdateSignalDrag(x, y);
|
||||
return true;
|
||||
}
|
||||
return false; // 仍在阈值内:按普通移动继续处理(悬停高亮等)
|
||||
}
|
||||
if (m_draggingScrollBar)
|
||||
{
|
||||
const gd::RectF thumb = ScrollThumbRect();
|
||||
@@ -574,6 +594,9 @@ namespace jdi
|
||||
// 点击信号名(行主体):除了选中它,还要让绘图区的**目标网格**完整出现在可视区内
|
||||
//(上百个独立网格时,旧有高亮根本无法定位)。悬停不触发这一步。
|
||||
RequestPlotLaneScroll(ref);
|
||||
// 同时登记"可能开始的拖拽排序":越过后阈值才真正进入拖拽态,
|
||||
// 因此单击仍然是单击(用户 2026-11-03 要求按住可拖动排序)。
|
||||
BeginSignalDrag(ref, row, x, y);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -582,6 +605,15 @@ namespace jdi
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
// ---- 信号条目拖拽 ----
|
||||
// ⚠️ 必须放在最前面:拖拽结束时列表条目位置可能变化,不能再走下方的 ✕ / 菜单判定。
|
||||
if (m_signalDragging)
|
||||
{
|
||||
FinishSignalDrag();
|
||||
return true;
|
||||
}
|
||||
m_signalDragPending = false;
|
||||
|
||||
// ---- 关闭类按钮(✕):按下时记录目标,抬起时若指针仍在同一个 ✕ 上才执行 ----
|
||||
if (m_pendingFilterClear)
|
||||
{
|
||||
@@ -722,6 +754,10 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 双击进入改名:必须先把"可能开始的拖拽"作废,否则第二次按下会被当成新的拖拽起点,
|
||||
// 改名后轻微移动就会拖着信号跑(用户 2026-11-03 要求拖拽排序,改名不能被它干扰)。
|
||||
CancelSignalDrag();
|
||||
|
||||
const float fx = static_cast<float>(x);
|
||||
const float fy = static_cast<float>(y);
|
||||
|
||||
@@ -958,9 +994,324 @@ namespace jdi
|
||||
m_hoverStyleIndex = -1;
|
||||
// 鼠标离开面板 → 取消绘图区的联动高亮(用户要求"移开鼠标时恢复")。
|
||||
m_hoveredSignal = SignalRef();
|
||||
// ⚠️ 拖拽中**不取消**:鼠标被拖到列表外(甚至面板外)时条目应继续跟随,
|
||||
// 松手才提交(用户 2026-11-03 要求"拖拽时条目跟随鼠标")。
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 信号条目拖拽排序(用户 2026-11-03 要求)
|
||||
//
|
||||
// 交互:左键按住信号行主体 → 越过 4px 阈值才进入拖拽(避免与单击选中/联动滚动抢事件);
|
||||
// 拖拽中被拖条目**脱离原位置**跟随鼠标,其它信号行/分组行按插入位置做避让位移
|
||||
//(目标值由 `UpdateSignalDragTargets` 给出,实际位移逐帧插值到 `m_signalDragShiftCur`);
|
||||
// 松手时把(源分组、源下标、目标分组、目标下标)提交给宿主。
|
||||
// ======================================================================
|
||||
|
||||
void SignalPanel::BeginSignalDrag(const SignalRef& ref, int row, int x, int y)
|
||||
{
|
||||
if (m_dataSet == nullptr || m_tab == nullptr || !ref.IsValid() || row < 0 ||
|
||||
row >= static_cast<int>(m_rows.size()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_signalDragPending = true;
|
||||
m_signalDragging = false;
|
||||
m_signalDragRef = ref;
|
||||
m_signalDragRow = row;
|
||||
m_signalDragStartX = x;
|
||||
m_signalDragStartY = y;
|
||||
m_signalDragMouseY = y;
|
||||
m_signalDragDropGroup = -1;
|
||||
m_signalDragDropIndex = -1;
|
||||
m_signalDragDropRow = -1;
|
||||
m_signalDragDropBefore = false;
|
||||
m_signalDragShift.clear();
|
||||
m_signalDragShiftCur.clear();
|
||||
}
|
||||
|
||||
bool SignalPanel::UpdateSignalDrag(int x, int y)
|
||||
{
|
||||
(void)x;
|
||||
if (!m_signalDragging)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_signalDragMouseY = y;
|
||||
|
||||
// 拖到列表上下边缘时自动滚动:跨组移动往往需要越过很长的列表
|
||||
//(上百个信号的 MF4 里拖到另一个分组)。
|
||||
const float edge = kPadding * m_scale;
|
||||
if (static_cast<float>(y) < m_listRect.Y + edge)
|
||||
{
|
||||
m_scrollOffset -= 6.0f * m_scale;
|
||||
ClampScroll();
|
||||
}
|
||||
else if (static_cast<float>(y) > m_listRect.Y + m_listRect.Height - edge)
|
||||
{
|
||||
m_scrollOffset += 6.0f * m_scale;
|
||||
ClampScroll();
|
||||
}
|
||||
|
||||
UpdateSignalDragTargets();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SignalPanel::UpdateSignalDragTargets()
|
||||
{
|
||||
const std::size_t rowCount = m_rows.size();
|
||||
m_signalDragDropGroup = -1;
|
||||
m_signalDragDropIndex = -1;
|
||||
m_signalDragDropRow = -1;
|
||||
m_signalDragDropBefore = false;
|
||||
m_signalDragShift.assign(rowCount, 0.0f);
|
||||
if (m_signalDragShiftCur.size() != rowCount)
|
||||
{
|
||||
m_signalDragShiftCur.resize(rowCount, 0.0f);
|
||||
}
|
||||
if (m_dataSet == nullptr || !m_signalDragging || m_signalDragRow < 0 ||
|
||||
m_signalDragRow >= static_cast<int>(rowCount))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float mouseY = static_cast<float>(m_signalDragMouseY);
|
||||
|
||||
// 鼠标所在的"自然行":`RowRect` 已叠加避让位移,这里减回去才代表用户看到的静止布局。
|
||||
int hoverRow = -1;
|
||||
bool below = false;
|
||||
for (std::size_t index = 0; index < rowCount; ++index)
|
||||
{
|
||||
if (static_cast<int>(index) == m_signalDragRow)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const gd::RectF rect = RowRect(static_cast<int>(index));
|
||||
const float top = rect.Y - m_signalDragShiftCur[index];
|
||||
const float height = m_rows[index].height;
|
||||
if (mouseY >= top && mouseY < top + height)
|
||||
{
|
||||
hoverRow = static_cast<int>(index);
|
||||
below = (mouseY >= top + height * 0.5f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int dstRow = -1;
|
||||
int dropGroup = -1;
|
||||
int dropIndex = -1;
|
||||
if (hoverRow < 0)
|
||||
{
|
||||
if (mouseY < m_listRect.Y && !m_rows.empty())
|
||||
{
|
||||
// 列表上方:落到第一个分组的最前面。
|
||||
dstRow = 0;
|
||||
dropGroup = m_rows.front().groupIndex;
|
||||
dropIndex = 0;
|
||||
}
|
||||
else if (!m_rows.empty())
|
||||
{
|
||||
// 列表下方:落到最后一个含信号的分组的末尾。
|
||||
for (int row = static_cast<int>(rowCount) - 1; row >= 0; --row)
|
||||
{
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
if (item.kind != RowKind::SIGNAL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const SignalGroup* group = m_dataSet->GroupAt(item.groupIndex);
|
||||
dstRow = static_cast<int>(rowCount);
|
||||
dropGroup = item.groupIndex;
|
||||
dropIndex = (group != nullptr) ? static_cast<int>(group->signals.size()) : 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(hoverRow)];
|
||||
if (item.kind == RowKind::GROUP)
|
||||
{
|
||||
// 组名行:插入到该组第一个信号之前(组名本身不让位)。
|
||||
dstRow = hoverRow + 1;
|
||||
dropGroup = item.groupIndex;
|
||||
dropIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
dstRow = below ? (hoverRow + 1) : hoverRow;
|
||||
dropGroup = item.groupIndex;
|
||||
dropIndex = item.signalIndex + (below ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (dstRow < 0 || dropGroup < 0 || dropIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 同组时补回"源信号被摘出"造成的下标偏移(数据层先删除再插入)。
|
||||
if (dropGroup == m_signalDragRef.groupIndex && dropIndex > m_signalDragRef.signalIndex)
|
||||
{
|
||||
--dropIndex;
|
||||
}
|
||||
|
||||
m_signalDragDropGroup = dropGroup;
|
||||
m_signalDragDropIndex = dropIndex;
|
||||
m_signalDragDropRow = dstRow;
|
||||
|
||||
// 避让:被跨过的行整体让出 / 补上一个行高。
|
||||
const int srcRow = m_signalDragRow;
|
||||
const float laneHeight = m_rows[static_cast<std::size_t>(srcRow)].height;
|
||||
if (dstRow > srcRow)
|
||||
{
|
||||
for (int row = srcRow + 1; row < dstRow && row < static_cast<int>(rowCount); ++row)
|
||||
{
|
||||
m_signalDragShift[static_cast<std::size_t>(row)] = -laneHeight;
|
||||
}
|
||||
}
|
||||
else if (dstRow < srcRow)
|
||||
{
|
||||
for (int row = dstRow; row < srcRow; ++row)
|
||||
{
|
||||
m_signalDragShift[static_cast<std::size_t>(row)] = laneHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalPanel::CancelSignalDrag()
|
||||
{
|
||||
m_signalDragPending = false;
|
||||
m_signalDragging = false;
|
||||
m_signalDragRow = -1;
|
||||
m_signalDragDropGroup = -1;
|
||||
m_signalDragDropIndex = -1;
|
||||
m_signalDragDropRow = -1;
|
||||
m_signalDragDropBefore = false;
|
||||
m_signalDragShift.clear();
|
||||
m_signalDragShiftCur.clear();
|
||||
}
|
||||
|
||||
void SignalPanel::FinishSignalDrag()
|
||||
{
|
||||
const SignalRef ref = m_signalDragRef;
|
||||
const int dropGroup = m_signalDragDropGroup;
|
||||
const int dropIndex = m_signalDragDropIndex;
|
||||
const bool moved = m_signalDragging && dropGroup >= 0 && dropIndex >= 0 &&
|
||||
(dropGroup != ref.groupIndex || dropIndex != ref.signalIndex);
|
||||
CancelSignalDrag();
|
||||
|
||||
if (!moved || m_host == nullptr || !ref.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// wParam = (源分组 << 16) | 源信号下标;lParam = (目标下标 << 16) | 目标分组。
|
||||
// 目标下标用 0xFFFF 表示"末尾"(与本消息的约定一致,见 Application 的消息处理)。
|
||||
const LONG_PTR from = (static_cast<LONG_PTR>(ref.groupIndex) << 16) |
|
||||
(static_cast<LONG_PTR>(ref.signalIndex) & 0xFFFF);
|
||||
const LONG_PTR to = (static_cast<LONG_PTR>(dropIndex) << 16) |
|
||||
(static_cast<LONG_PTR>(dropGroup) & 0xFFFF);
|
||||
PostMessageW(m_host, WM_APP + 8, static_cast<WPARAM>(from), static_cast<LPARAM>(to));
|
||||
}
|
||||
|
||||
float SignalPanel::SignalDragRowShiftFor(int row) const
|
||||
{
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= m_signalDragShiftCur.size())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
return m_signalDragShiftCur[static_cast<std::size_t>(row)];
|
||||
}
|
||||
|
||||
bool SignalPanel::AdvanceSignalDragAnimation()
|
||||
{
|
||||
if (!m_signalDragPending && !m_signalDragging)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_signalDragShiftCur.size() != m_signalDragShift.size())
|
||||
{
|
||||
m_signalDragShiftCur.resize(m_signalDragShift.size(), 0.0f);
|
||||
}
|
||||
// 逐帧向目标位移插值(宿主约 10ms 一帧 ⇒ 约 5 帧收敛,观感是"平滑让位")。
|
||||
const float factor = 0.32f;
|
||||
for (std::size_t index = 0; index < m_signalDragShiftCur.size(); ++index)
|
||||
{
|
||||
const float target = m_signalDragShift[index];
|
||||
float& current = m_signalDragShiftCur[index];
|
||||
const float delta = target - current;
|
||||
if (std::fabs(delta) < 0.5f)
|
||||
{
|
||||
current = target;
|
||||
}
|
||||
else
|
||||
{
|
||||
current += delta * factor;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SignalPanel::PaintSignalDragGhost(gd::Graphics& g)
|
||||
{
|
||||
if (!m_signalDragging || m_signalDragRow < 0 ||
|
||||
m_signalDragRow >= static_cast<int>(m_rows.size()) || m_theme == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const gd::RectF natural = RowRect(m_signalDragRow);
|
||||
const float height = m_rows[static_cast<std::size_t>(m_signalDragRow)].height;
|
||||
if (height <= 0.5f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 被拖条目跟随鼠标(鼠标位于条目中部)。
|
||||
float top = static_cast<float>(m_signalDragMouseY) - height * 0.5f;
|
||||
top = std::max(m_listRect.Y, std::min(top, m_listRect.Y + m_listRect.Height - height));
|
||||
|
||||
const int state = g.Save();
|
||||
g.SetClip(m_listRect, gd::CombineModeIntersect);
|
||||
|
||||
// 浮起的阴影:让条目明显"脱离"列表本体。
|
||||
UiFillRoundRect(g, gd::RectF(natural.X + 2.0f * m_scale, top + 3.0f * m_scale,
|
||||
natural.Width, height),
|
||||
4.0f * m_scale, 0x33000000u);
|
||||
|
||||
// 复用行绘制:临时把行顶部覆盖到鼠标位置(与「组名悬浮」同一机制),
|
||||
// 因此名称、复选框、GX 徽标、✕ 等全部随行一起移动,无需另写一套绘制。
|
||||
m_rowYOverrideActive = true;
|
||||
m_rowYOverrideY = top;
|
||||
PaintSignalRow(g, m_signalDragRow);
|
||||
m_rowYOverrideActive = false;
|
||||
|
||||
UiDrawRoundRect(g, gd::RectF(natural.X + 1.0f * m_scale, top + 1.0f * m_scale,
|
||||
natural.Width - 2.0f * m_scale, height - 2.0f * m_scale),
|
||||
4.0f * m_scale, m_theme->accent, 1.6f * m_scale);
|
||||
|
||||
// 插入位置指示线:明确告诉用户"松手后落在哪里"。
|
||||
if (m_signalDragDropRow >= 0)
|
||||
{
|
||||
float lineY = 0.0f;
|
||||
if (m_signalDragDropRow < static_cast<int>(m_rows.size()))
|
||||
{
|
||||
lineY = RowRect(m_signalDragDropRow).Y;
|
||||
}
|
||||
else if (!m_rows.empty())
|
||||
{
|
||||
const gd::RectF last = RowRect(static_cast<int>(m_rows.size()) - 1);
|
||||
lineY = last.Y + last.Height;
|
||||
}
|
||||
if (lineY >= m_listRect.Y - 1.0f && lineY <= m_listRect.Y + m_listRect.Height + 1.0f)
|
||||
{
|
||||
UiDrawLine(g, m_listRect.X + 4.0f * m_scale, lineY,
|
||||
m_listRect.X + m_listRect.Width - 4.0f * m_scale, lineY,
|
||||
m_theme->accent, 2.0f * m_scale);
|
||||
}
|
||||
}
|
||||
|
||||
g.Restore(state);
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
Reference in new issue
Block a user