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
@@ -80,6 +80,13 @@ namespace jdi
|
||||
* 勾选状态通过 `UpdateItemLabel` 回写。默认 false(点击即关):与既有行为一致。
|
||||
*/
|
||||
bool keepOpen = false;
|
||||
/**
|
||||
* @brief 二级子菜单可滚动条目区的**最大显示行数**(0 = 不限制)。
|
||||
*
|
||||
* 与 `MenuLayoutConstraint::maxScrollRows` 同义,只是作用于该子菜单自身:
|
||||
* 分组/网格数量可能上百,不限高时子菜单会长到屏幕之外。
|
||||
*/
|
||||
int submenuMaxRows = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -245,7 +252,12 @@ namespace jdi
|
||||
* @brief 菜单是否因内容过长而启用了滚动(条目区可滚动、前导标题与尾部条目固定)。
|
||||
* @return 启用滚动返回 true。
|
||||
*/
|
||||
bool IsScrollable() const { return m_open && m_scrollEnabled; }
|
||||
bool IsScrollable() const
|
||||
{
|
||||
// 二级子菜单展开且自身可滚动时同样算「可滚动」:宿主据此把滚轮转给菜单,
|
||||
// 而不是"滚轮即关闭菜单"(否则「移动到…」这类长短不定的子菜单无法翻页)。
|
||||
return m_open && (m_scrollEnabled || (m_subOpen && m_subScrollEnabled));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 滚动条**轨道**矩形(仅可滚动菜单有效,其余返回空矩形)。
|
||||
@@ -568,6 +580,18 @@ namespace jdi
|
||||
std::vector<gd::RectF> m_subItemRects; /**< 子菜单各项矩形。 */
|
||||
int m_subHoverIndex = -1; /**< 子菜单当前悬停项。 */
|
||||
int m_subPressedIndex = -1; /**< 子菜单按下时的项下标。 */
|
||||
/**
|
||||
* @brief 子菜单的可滚动状态(条目过多时启用)。
|
||||
*
|
||||
* 与主菜单的滚动机制**刻意保持独立**:子菜单在主菜单展开期间一直存在,
|
||||
* 若共用一套偏移,主菜单滚动会连带把子菜单的条目位置打乱。
|
||||
*/
|
||||
bool m_subScrollEnabled = false; /**< 子菜单条目区是否可滚动。 */
|
||||
float m_subScrollOffset = 0.0f; /**< 子菜单滚动偏移(像素,0 = 顶部)。 */
|
||||
float m_subScrollRegionHeight = 0.0f; /**< 子菜单内容总高(像素)。 */
|
||||
gd::RectF m_subViewport; /**< 子菜单条目可视区(同时是条目裁剪矩形)。 */
|
||||
gd::RectF m_subScrollBarRect; /**< 子菜单滚动条轨道矩形。 */
|
||||
float m_subMaxRowsHeight = 0.0f; /**< 子菜单可视区高度上限(0 = 不限制)。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
@@ -415,7 +415,21 @@ namespace jdi
|
||||
|
||||
bool PopupMenu::OnMouseWheel(int delta)
|
||||
{
|
||||
if (!m_open || !m_scrollEnabled || delta == 0)
|
||||
if (!m_open || delta == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 子菜单展开且可滚动时优先滚子菜单:鼠标此时一定悬停在子菜单条目上
|
||||
//(父项悬停即展开,滚轮自然是"翻这个列表"的意图)。
|
||||
if (m_subOpen && m_subScrollEnabled)
|
||||
{
|
||||
const float previous = m_subScrollOffset;
|
||||
m_subScrollOffset -= static_cast<float>(delta) / 120.0f * 54.0f * m_scale;
|
||||
// 重新排布(内部会夹取 `m_subScrollOffset` 并刷新条目矩形)。
|
||||
BuildSubLayout(m_subParentIndex);
|
||||
return std::fabs(m_subScrollOffset - previous) > 0.01f;
|
||||
}
|
||||
if (!m_scrollEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -447,10 +461,20 @@ namespace jdi
|
||||
m_subItemRects.clear();
|
||||
m_subHoverIndex = -1;
|
||||
m_subPressedIndex = -1;
|
||||
m_subScrollEnabled = false;
|
||||
m_subScrollOffset = 0.0f;
|
||||
m_subScrollRegionHeight = 0.0f;
|
||||
m_subViewport = gd::RectF();
|
||||
m_subScrollBarRect = gd::RectF();
|
||||
m_subMaxRowsHeight = 0.0f;
|
||||
}
|
||||
|
||||
void PopupMenu::BuildSubLayout(int parentIndex)
|
||||
{
|
||||
// `CloseSubmenu()` 会清零全部子菜单状态,但滚动(`OnMouseWheel`)需要重新排布时
|
||||
// **保留滚动位置**:只有切换到另一个父项才从顶部开始。
|
||||
const float savedScrollOffset = m_subScrollOffset;
|
||||
const int savedParentIndex = m_subParentIndex;
|
||||
CloseSubmenu();
|
||||
if (parentIndex < 0 || parentIndex >= static_cast<int>(m_items.size()))
|
||||
{
|
||||
@@ -463,6 +487,13 @@ namespace jdi
|
||||
}
|
||||
|
||||
m_subItems = parent.submenu;
|
||||
// 同一父项重新排布(滚动)时沿用原来的滚动位置。
|
||||
m_subScrollOffset = (savedParentIndex == parentIndex) ? savedScrollOffset : 0.0f;
|
||||
m_subScrollEnabled = false;
|
||||
m_subScrollRegionHeight = 0.0f;
|
||||
m_subViewport = gd::RectF();
|
||||
m_subScrollBarRect = gd::RectF();
|
||||
m_subMaxRowsHeight = 0.0f;
|
||||
|
||||
// 与主菜单一致:宽度取条目中的最大者并夹在上下限之间。
|
||||
float contentWidth = kMinWidth * m_scale;
|
||||
@@ -502,8 +533,28 @@ namespace jdi
|
||||
}
|
||||
contentWidth = std::min(contentWidth, kMaxWidth * m_scale);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 子菜单条目过多时可滚动(`MenuItem::submenuMaxRows`)。
|
||||
// 与主菜单的"头部固定 / 尾部固定"不同,这里**整段一起滚**:
|
||||
// 子菜单的条目形态单一(分组列表 + 末段分隔线 + 新建分组),整体滚动即可,
|
||||
// 且不会出现"两个固定区各占一半高度、中间只剩一两行"的糟糕观感。
|
||||
// ------------------------------------------------------------------
|
||||
const float subContentHeight = totalHeight - kPadV * 2.0f * m_scale;
|
||||
if (parent.submenuMaxRows > 0)
|
||||
{
|
||||
const float rowsLimit =
|
||||
static_cast<float>(parent.submenuMaxRows) * kRowHeight * m_scale;
|
||||
m_subMaxRowsHeight = rowsLimit;
|
||||
if (rowsLimit >= kRowHeight * m_scale && subContentHeight > rowsLimit + 0.5f)
|
||||
{
|
||||
m_subScrollEnabled = true;
|
||||
m_subScrollRegionHeight = subContentHeight;
|
||||
}
|
||||
}
|
||||
|
||||
const float width = contentWidth;
|
||||
const float height = totalHeight;
|
||||
const float height =
|
||||
m_subScrollEnabled ? (m_subMaxRowsHeight + kPadV * 2.0f * m_scale) : totalHeight;
|
||||
const gd::RectF& parentRect = m_itemRects[static_cast<std::size_t>(parentIndex)];
|
||||
const gd::RectF& bounds = m_buildBounds;
|
||||
|
||||
@@ -521,12 +572,28 @@ namespace jdi
|
||||
m_subRect = gd::RectF(left, top, width, height);
|
||||
m_subItemRects.assign(m_subItems.size(), gd::RectF());
|
||||
|
||||
float cursorY = top + kPadV * m_scale;
|
||||
// 条目可视区:整段滚动模式下就是内容区;让出右侧滚动条的位置。
|
||||
const float subScrollReserve =
|
||||
m_subScrollEnabled ? (kScrollBarWidth + kScrollBarGap) * m_scale : 0.0f;
|
||||
m_subViewport = gd::RectF(left + kPadH * m_scale, top + kPadV * m_scale,
|
||||
std::max(0.0f, width - kPadH * 2.0f * m_scale - subScrollReserve),
|
||||
m_subScrollEnabled ? m_subMaxRowsHeight : std::max(0.0f, height - kPadV * 2.0f * m_scale));
|
||||
if (m_subScrollEnabled)
|
||||
{
|
||||
// 偏移夹到合法范围(内容高 - 可视高)。
|
||||
m_subScrollOffset = std::max(
|
||||
0.0f, std::min(m_subScrollOffset, std::max(0.0f, subContentHeight - m_subMaxRowsHeight)));
|
||||
m_subScrollBarRect =
|
||||
gd::RectF(left + width - kPadH * m_scale - kScrollBarWidth * m_scale,
|
||||
top + kPadV * m_scale, kScrollBarWidth * m_scale, m_subMaxRowsHeight);
|
||||
}
|
||||
|
||||
float cursorY = top + kPadV * m_scale - m_subScrollOffset;
|
||||
for (std::size_t index = 0; index < m_subItems.size(); ++index)
|
||||
{
|
||||
const float itemHeight = ItemHeight(m_subItems[index]);
|
||||
m_subItemRects[index] = gd::RectF(left + kPadH * m_scale, cursorY,
|
||||
std::max(0.0f, width - kPadH * 2.0f * m_scale), itemHeight);
|
||||
std::max(0.0f, m_subViewport.Width), itemHeight);
|
||||
cursorY += itemHeight;
|
||||
}
|
||||
|
||||
@@ -544,6 +611,12 @@ namespace jdi
|
||||
}
|
||||
const float fx = static_cast<float>(x);
|
||||
const float fy = static_cast<float>(y);
|
||||
// 滚动中的子菜单:可视区之外的条目矩形仍然存在(只是被裁掉),必须先按视口过滤,
|
||||
// 否则滚出视野的条目还能被点中(表现为"点到看不见的东西")。
|
||||
if (m_subScrollEnabled && !UiPointInRect(m_subViewport, fx, fy))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
for (std::size_t index = 0; index < m_subItems.size(); ++index)
|
||||
{
|
||||
const MenuItem& item = m_subItems[index];
|
||||
|
||||
@@ -305,6 +305,16 @@ namespace jdi
|
||||
UiFillRoundRect(g, m_subRect, kCorner * m_scale, m_theme->cardBg);
|
||||
UiDrawRoundRect(g, m_subRect, kCorner * m_scale, m_theme->borderStrong, 1.0f);
|
||||
|
||||
// 滚动中的子菜单:条目必须裁剪在可视区内(否则会画到面板圆角之外)。
|
||||
int subClipState = -1;
|
||||
if (m_subScrollEnabled)
|
||||
{
|
||||
subClipState = g.Save();
|
||||
g.SetClip(gd::RectF(m_subRect.X + 1.0f * m_scale, m_subRect.Y + 1.0f * m_scale,
|
||||
m_subRect.Width - 2.0f * m_scale, m_subRect.Height - 2.0f * m_scale),
|
||||
gd::CombineModeIntersect);
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < m_subItems.size(); ++index)
|
||||
{
|
||||
const MenuItem& item = m_subItems[index];
|
||||
@@ -395,6 +405,34 @@ namespace jdi
|
||||
gd::RectF(textLeft, rect.Y, std::max(8.0f, textWidth), rect.Height), textColor,
|
||||
TextAlign::LEFT);
|
||||
}
|
||||
|
||||
if (m_subScrollEnabled)
|
||||
{
|
||||
if (subClipState >= 0)
|
||||
{
|
||||
g.Restore(subClipState);
|
||||
}
|
||||
// 滚动条:轨道 + 滑块(与主菜单同一套配色与算法,粗细取 `kScrollBarWidth`)。
|
||||
if (m_subScrollBarRect.Width > 0.0f && m_subScrollRegionHeight > 0.0f &&
|
||||
m_subViewport.Height > 0.0f)
|
||||
{
|
||||
const float radius = kScrollBarWidth * m_scale * 0.5f;
|
||||
UiFillRoundRect(g, m_subScrollBarRect, radius, m_theme->gridWeak);
|
||||
const float viewportHeight = m_subViewport.Height;
|
||||
const float ratio = std::min(1.0f, viewportHeight / m_subScrollRegionHeight);
|
||||
const float thumbHeight =
|
||||
std::max(kScrollBarMinThumb * m_scale, m_subScrollBarRect.Height * ratio);
|
||||
const float maxScroll = std::max(0.0f, m_subScrollRegionHeight - viewportHeight);
|
||||
const float progress = (maxScroll > 0.0f) ? (m_subScrollOffset / maxScroll) : 0.0f;
|
||||
const float thumbTop =
|
||||
m_subScrollBarRect.Y +
|
||||
std::max(0.0f, std::min(1.0f, progress)) *
|
||||
std::max(0.0f, m_subScrollBarRect.Height - thumbHeight);
|
||||
UiFillRoundRect(
|
||||
g, gd::RectF(m_subScrollBarRect.X, thumbTop, m_subScrollBarRect.Width, thumbHeight),
|
||||
radius, m_theme->borderStrong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
Reference in new issue
Block a user