信号 X 按钮移至行尾并统一抬起响应,光标读数补秒单位,图例悬停高亮信号,去除筛选框内圈

1. 信号条目的「移除」X 按钮移到最右端
   - 与分组行关闭 X 同尺寸(18)、同右边距(同一列)、同行内垂直居中,描边宽度也统一。
   - 网格编号 GX 徽标让位到 X 左侧,二者恒在列表最右侧;名称可用宽度改按实际徽标是否显示计算,
     不再白留一段空间。

2. 所有 X 关闭按钮由"按下即响应"改为"左键抬起才响应"
   - 与常见的可取消按钮一致:按下只记录目标,按住后移开鼠标再松开视为放弃。
   - 覆盖文件分组 X、信号 X、筛选框清空 X、标签页 X 与实验性功能面板 X。
   - 筛选框的清空 X 在编辑态下走的是宿主的 EDIT_FIELD 分支(OnEditMouseDown/OnEditMouseUp),
     非编辑态才走面板的按下/抬起分支,两条路径都已改造。
   - 标签页 X 原先只要求"抬起时仍在同一标签内",现收紧为"仍在 X 上"。
   - PlotView::OnLButtonUp 增加横坐标参数以便判定抬起位置。

3. 时间光标读数补上 s 后缀
   - 新增 FormatCursorTime,标签宽度测量与文字绘制同时使用,避免文字被裁掉;
     内联编辑仍使用不带单位的秒值。

4. 鼠标悬停网格图例时高亮对应信号
   - 除图例条目的半高亮外,对命中信号绘制与"鼠标贴近曲线"相同的光晕,
     便于确认该条目对应哪条曲线。

5. 筛选信号输入框编辑态不再出现内圈
   - 把输入框绘制拆分为"底板+边框"与"仅内容"两部分;筛选框外框已自绘,
     编辑态改用仅内容版本,空内容时不再套一层多余的圆角内框;
     有输入时的清空 X 不受影响。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-10 11:30:07 +08:00
1 parent a1e30985b8
commit d3c83237f1
8 files changed
+252 -72

No files matched your search

+14 -4
View File
@@ -220,7 +220,7 @@ namespace jdi
* @brief 编辑态下松开鼠标左键:结束拖拽框选。
* @return 之前处于拖拽中返回 true。
*/
bool OnEditMouseUp();
bool OnEditMouseUp(int x, int y);
/**
* @brief 获取指定信号的显示外观(只读,全局共享)。
@@ -405,10 +405,10 @@ namespace jdi
gd::RectF SignalBadgeRect(int row) const;
/**
* @brief 计算信号行左侧「移除该信号」✕ 按钮的矩形。
* @brief 计算信号行右侧「移除该信号」✕ 按钮的矩形。
*
* 位置就信号行空着的折叠三角列,并与所属分组行的复选框**同一列**(左边界对齐),
* 因此始终位于复选框前方且与其垂直对齐。
* 位于信号条目的**最右端**,尺寸、样式与右边距与分组行的关闭 ✕ 完全一致,
* 并与它们在同一行内垂直居中(因此视觉上垂直对齐);网格徽标会让位到它的左侧。
*
* @param [IN] row 行下标。
* @return 按钮矩形;非信号行时返回空矩形。
@@ -641,6 +641,16 @@ namespace jdi
SignalRef m_pendingLaneRef; /**< 待弹出菜单的目标信号。 */
bool m_pendingColorPicker = false; /**< 左键抬起时是否弹出拾色器。 */
/**
* @brief 待确认的「关闭类」按钮(✕)。
*
* 关闭类按钮统一改为**左键抬起**时生效:按下只记录目标,抬起时若指针仍在同一个
* ✕ 上才真正执行;按住后移开再松开视为放弃(与常见"可取消按钮"交互一致)。
*/
RowPart m_pendingClosePart = RowPart::NONE; /**< 待确认的 ✕ 所在行内区域(CLOSE / REMOVE_SIGNAL)。 */
int m_pendingCloseRow = -1; /**< 待确认的 ✕ 所在行下标。 */
bool m_pendingFilterClear = false; /**< 待确认的筛选框清空 ✕。 */
bool m_layoutDirty = false; /**< 是否需要重新布局。 */
SignalRef m_menuTargetSignal; /**< 菜单操作的目标信号。 */
+118 -42
View File
@@ -23,13 +23,13 @@ namespace jdi
const float kDisclosureWidth = 16.0f; /**< 折叠三角占位宽度。 */
const float kIndent = 14.0f; /**< 信号行相对文件行的缩进。 */
const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */
const float kCloseButtonSize = 18.0f; /**< 文件行关闭按钮尺寸。 */
const float kCloseButtonSize = 18.0f; /**< 行尾关闭(✕)按钮尺寸(分组行与信号行共用)。 */
const float kGroupSeparatorGap = 7.0f; /**< 分组标签与分隔线/名称的间距。 */
const float kGroupSeparatorWidth = 1.0f; /**< 分组标签右侧分隔线宽度。 */
const float kFilterHeight = 26.0f; /**< 「筛选信号」输入框高度。 */
const float kFilterPadV = 5.0f; /**< 筛选框上下留白。 */
const float kFilterTextPad = 4.0f; /**< 筛选框内文字/输入框相对外框的内缩(四边一致)。 */
const float kRemoveButtonSize = 14.0f; /**< 信号行「移除该信号」✕ 按钮尺寸。 */
const float kRowTailGap = 6.0f; /**< 网格徽标与行尾 ✕ 之间的间距。 */
/**
* @brief 大小写无关的通配符匹配(`*` 匹配任意长度的任意字符序列)。
@@ -181,6 +181,9 @@ namespace jdi
m_editField = StyleEditField::NONE;
m_pendingLaneMenu = false;
m_pendingColorPicker = false;
m_pendingClosePart = RowPart::NONE;
m_pendingCloseRow = -1;
m_pendingFilterClear = false;
}
void SignalPanel::SetScale(float scale)
@@ -461,8 +464,10 @@ namespace jdi
const gd::RectF rect = RowRect(row);
const float width = kBadgeWidth * m_scale;
const float height = kBadgeHeight * m_scale;
return gd::RectF(rect.X + rect.Width - kPadding * m_scale - width,
rect.Y + (rect.Height - height) * 0.5f, width, height);
// 行尾现在被「移除该信号」✕ 占用(与分组 ✕ 同尺寸、同列),网格徽标因此整体前移;
// 两者都恒在列表最右侧,且始终保留固定的间隔,不会互相挤压。
const float right = SignalRemoveRect(row).X - kRowTailGap * m_scale;
return gd::RectF(right - width, rect.Y + (rect.Height - height) * 0.5f, width, height);
}
gd::RectF SignalPanel::SignalRemoveRect(int row) const
@@ -473,13 +478,12 @@ namespace jdi
return gd::RectF();
}
// 分组行的复选框矩形为 [rect.X + kPadding + kDisclosureWidth, +kCheckSize],
// 信号行的折叠三角列是空着的,正好用来放 ✕:左边界与分组复选框对齐
// (垂直方向同列),右边界仍留在三角列内,不会压到信号自己的复选框。
// 与分组行的关闭 ✕ **完全一致**(同尺寸、同右边距、同行内垂直居中),
// 只是作用对象是本行信号:位于信号条目的最右端。
const gd::RectF rect = RowRect(row);
const float left = rect.X + (kPadding + kDisclosureWidth) * m_scale;
const float size = kRemoveButtonSize * m_scale;
return gd::RectF(left, rect.Y + (rect.Height - size) * 0.5f, size, size);
const float size = kCloseButtonSize * m_scale;
return gd::RectF(rect.X + rect.Width - kPadding * m_scale - size,
rect.Y + (rect.Height - size) * 0.5f, size, size);
}
int SignalPanel::HitRow(int x, int y, RowPart& part)
@@ -790,9 +794,12 @@ namespace jdi
if (m_filterEditing)
{
// 外框(m_filterRect)已经画好了底板与强调色边框,这里只叠加文本内容。
// 若改用 UiDrawEditField,会在外框内部再套一个带边框的圆角框,
// 空内容时只剩一圈内框十分突兀(用户明确要求去掉这层"内圈")。
gd::RectF fieldRect = FilterFieldRect();
fieldRect.Width = std::max(8.0f, textWidth + pad);
UiDrawEditField(g, fieldRect, m_filterEdit, m_fonts->uiSmall, m_scale);
UiDrawEditFieldContent(g, fieldRect, m_filterEdit, m_fonts->uiSmall, m_scale);
}
else if (m_filterText.empty())
{
@@ -1010,18 +1017,6 @@ namespace jdi
const bool visible = m_tab->IsVisible(ref);
UiDrawCheckBox(g, checkRect, visible ? 1 : 0, 3.0f * m_scale);
// 「移除该信号」✕:位于复选框前方(分组复选框所在列),悬停时高亮为危险色。
{
const gd::RectF removeRect = SignalRemoveRect(row);
const bool removeHot = hovered && (m_hoverPart == RowPart::REMOVE_SIGNAL);
if (removeHot)
{
UiFillRoundRect(g, removeRect, 3.0f * m_scale, m_theme->pressed);
}
UiDrawCrossIcon(g, removeRect, removeHot ? m_theme->danger : m_theme->textMuted,
1.4f * m_scale);
}
// 颜色块:叠加不透明度,透明度过低时自动补描边(与样式区色块规则一致)。
const gd::RectF swatchRect(checkRect.X + checkRect.Width + 6.0f * m_scale,
centerY - kSwatchSize * m_scale * 0.5f, kSwatchSize * m_scale,
@@ -1029,12 +1024,13 @@ namespace jdi
UiDrawSwatch(g, swatchRect, style->color, visible ? style->opacity : 0.3f, 2.5f * m_scale,
m_scale);
// 网格徽标(可点击,用于调整所属网格)。
// 网格徽标(可点击,用于调整所属网格)。行尾 ✕ 恒在最右侧,徽标整体前移让位。
const int laneIndex = m_tab->LaneOfSignal(ref);
const bool badgeVisible = (laneIndex >= 0) && visible;
const gd::RectF badgeRect = SignalBadgeRect(row);
const gd::RectF removeRect = SignalRemoveRect(row);
float nameRightLimit = rect.X + rect.Width - kPadding * m_scale;
float nameRightLimit = removeRect.X - 6.0f * m_scale;
if (badgeVisible)
{
nameRightLimit = badgeRect.X - 6.0f * m_scale;
@@ -1064,6 +1060,18 @@ namespace jdi
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
}
// 「移除该信号」✕:位于信号条目最右端,尺寸/样式/右边距与分组行的关闭 ✕ 完全一致。
// 最后绘制,确保它永远压在最上层(与分组行 ✕ 的绘制顺序保持一致)。
{
const bool removeHot = hovered && (m_hoverPart == RowPart::REMOVE_SIGNAL);
if (removeHot)
{
UiFillRoundRect(g, removeRect, 3.0f * m_scale, m_theme->pressed);
}
UiDrawCrossIcon(g, removeRect, removeHot ? m_theme->danger : m_theme->textMuted,
1.5f * m_scale);
}
}
gd::RectF SignalPanel::ScrollThumbRect() const
@@ -1304,7 +1312,12 @@ namespace jdi
else
{
left = checkRight + 6.0f * m_scale + kSwatchSize * m_scale + 6.0f * m_scale;
right = SignalBadgeRect(row).X - 6.0f * m_scale;
// 行尾的可用右界:有网格徽标时让位给徽标,否则可直接延伸到行尾 ✕ 之前。
const SignalRef ref{m_rows[static_cast<std::size_t>(row)].fileIndex,
m_rows[static_cast<std::size_t>(row)].signalIndex};
const bool badgeVisible = (m_tab != nullptr) && m_tab->IsVisible(ref) &&
(m_tab->LaneOfSignal(ref) >= 0);
right = (badgeVisible ? SignalBadgeRect(row).X : SignalRemoveRect(row).X) - 6.0f * m_scale;
}
if (right <= left)
@@ -1721,11 +1734,12 @@ namespace jdi
{
// 编辑态下 ✕ 位于输入框内容区之内,必须优先于插入符定位处理,
// 否则点击会被当成"把光标移到此处"而清空不了内容。
// 与其它 ✕ 一致:按下只记录目标,抬起时仍在 ✕ 上才真正清空。
const gd::RectF clearRect = FilterClearRect();
if (clearRect.Width > 0.0f &&
UiPointInRect(clearRect, static_cast<float>(x), static_cast<float>(y)))
{
ClearFilter();
m_pendingFilterClear = true;
return true;
}
return UiEditFieldMouseDown(m_fonts->uiSmall, FilterFieldRect(), m_filterEdit, m_scale, x, y);
@@ -1750,10 +1764,24 @@ namespace jdi
return UiEditFieldMouseMove(m_fonts->ui, EditFieldRect(), m_edit, m_scale, x);
}
bool SignalPanel::OnEditMouseUp()
bool SignalPanel::OnEditMouseUp(int x, int y)
{
if (m_filterEditing)
{
// 筛选框右端的清空 ✕ 在编辑态下的**按下**走 OnEditMouseDown(优先于插入符定位),
// 抬起同样由宿主送进 EDIT_FIELD 分支,因此确认逻辑要在这里结算:
// 只有抬起时指针仍在 ✕ 上才真正清空,移开后松开视为放弃。
if (m_pendingFilterClear)
{
m_pendingFilterClear = false;
const gd::RectF clearRect = FilterClearRect();
if (clearRect.Width > 0.0f &&
UiPointInRect(clearRect, static_cast<float>(x), static_cast<float>(y)))
{
ClearFilter();
return true;
}
}
if (!m_filterEdit.Dragging())
{
return false;
@@ -1863,6 +1891,10 @@ namespace jdi
m_colorPicker->Close();
changed = true;
}
// 面板外点击:取消尚未抬起确认的关闭操作。
m_pendingClosePart = RowPart::NONE;
m_pendingCloseRow = -1;
m_pendingFilterClear = false;
return changed;
}
@@ -1979,13 +2011,19 @@ namespace jdi
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
// 新的按下开始:上一轮留下的"待确认关闭"一律作废。
m_pendingClosePart = RowPart::NONE;
m_pendingCloseRow = -1;
m_pendingFilterClear = false;
// 「筛选信号」输入框:单击进入编辑;点击其它位置则先结束筛选编辑(文本已实时生效)。
if (IsOnFilterBox(x, y))
{
// 右侧 ✕:清空筛选内容(不退出编辑态,便于继续输入)。
// 右侧 ✕:**抬起**时才清空(不退出编辑态,便于继续输入);
// 按下后移开再松开视为放弃。
if (UiPointInRect(FilterClearRect(), fx, fy))
{
ClearFilter();
m_pendingFilterClear = true;
return true;
}
BeginFilterEdit();
@@ -2111,11 +2149,10 @@ namespace jdi
}
if (part == RowPart::CLOSE)
{
// 关闭文件属于数据层操作,交由宿主窗口处理。
if (m_host != nullptr)
{
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(item.fileIndex), 0);
}
// 关闭文件属于数据层操作,交由宿主窗口处理;但**抬起时才生效**,
// 这里只记录目标(按住后移开再松开视为放弃)。
m_pendingClosePart = RowPart::CLOSE;
m_pendingCloseRow = row;
return true;
}
m_tab->ToggleCollapsed(item.fileIndex);
@@ -2126,15 +2163,12 @@ namespace jdi
const SignalRef ref{item.fileIndex, item.signalIndex};
// ✕:用户明确表示不再需要该信号 → 交由宿主从数据层移除并释放其采样内存。
// 必须在 SelectSignal 之前处理,避免选中一条即将被删除的信号。
// 必须在 SelectSignal 之前处理,避免选中一条即将被删除的信号;
// 且与其它 ✕ 一致,**抬起时才真正删除**(按住后移开再松开视为放弃)。
if (part == RowPart::REMOVE_SIGNAL)
{
if (m_host != nullptr)
{
// wParam = 文件下标,lParam = 文件内信号下标(见 Application 的消息处理)。
PostMessageW(m_host, WM_APP + 2, static_cast<WPARAM>(item.fileIndex),
static_cast<LPARAM>(item.signalIndex));
}
m_pendingClosePart = RowPart::REMOVE_SIGNAL;
m_pendingCloseRow = row;
return true;
}
@@ -2159,6 +2193,48 @@ namespace jdi
{
bool changed = false;
// ---- 关闭类按钮(✕):按下时记录目标,抬起时若指针仍在同一个 ✕ 上才执行 ----
if (m_pendingFilterClear)
{
m_pendingFilterClear = false;
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
const gd::RectF clearRect = FilterClearRect();
if (clearRect.Width > 0.0f && UiPointInRect(clearRect, fx, fy))
{
ClearFilter();
changed = true;
}
}
if (m_pendingClosePart != RowPart::NONE)
{
const RowPart pendingPart = m_pendingClosePart;
const int pendingRow = m_pendingCloseRow;
m_pendingClosePart = RowPart::NONE;
m_pendingCloseRow = -1;
RowPart part = RowPart::NONE;
const int row = HitRow(x, y, part);
if (row == pendingRow && part == pendingPart && row >= 0)
{
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
if (m_host != nullptr)
{
if (pendingPart == RowPart::CLOSE)
{
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(item.fileIndex), 0);
}
else
{
// wParam = 文件下标,lParam = 文件内信号下标(见 Application 的消息处理)。
PostMessageW(m_host, WM_APP + 2, static_cast<WPARAM>(item.fileIndex),
static_cast<LPARAM>(item.signalIndex));
}
}
changed = true;
}
}
if (m_draggingOpacity)
{
m_draggingOpacity = false;