diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index 46b64cf..76d4e55 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -1846,7 +1846,7 @@ namespace jdi m_activeTarget = Target::NONE; m_draggingSplitter = false; m_panel.OnLButtonUp(0, 0); - m_plot.OnLButtonUp(0); + m_plot.OnLButtonUp(0, 0); } switch (message) @@ -1971,7 +1971,7 @@ namespace jdi // 拾色器接管输入后,取消底层组件可能仍在进行的拖拽。 m_panel.OnLButtonUp(0, 0); - m_plot.OnLButtonUp(0); + m_plot.OnLButtonUp(0, 0); m_activeTarget = Target::NONE; m_draggingSplitter = false; @@ -2305,7 +2305,7 @@ namespace jdi { // 结束输入框内的拖拽框选(选区保留)。 bool changed = m_tabBar.OnEditMouseUp(); - changed = m_panel.OnEditMouseUp() || changed; + changed = m_panel.OnEditMouseUp(x, y) || changed; changed = m_plot.OnEditMouseUp() || changed; changed = m_colorPicker.OnEditMouseUp() || changed; m_activeTarget = Target::NONE; @@ -2340,7 +2340,7 @@ namespace jdi m_panel.OnLButtonUp(x, y); break; case Target::PLOT: - m_plot.OnLButtonUp(y); + m_plot.OnLButtonUp(x, y); break; case Target::NONE: default: diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 1e74b84..536bc29 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -160,7 +160,7 @@ namespace jdi * @param [IN] y 客户区纵坐标。 * @return 需要重绘返回 true。 */ - bool OnLButtonUp(int y); + bool OnLButtonUp(int x, int y); /** * @brief 处理鼠标中键按下:与左键在常规模式下一致,进入自由平移(水平平移时间轴 + @@ -1224,6 +1224,16 @@ namespace jdi int m_pendingMeanFilterLane = -1; /**< 待构建的均值滤波信号所属网格下标(-1 表示无请求)。 */ int m_pendingMeanFilterRadius = 0; /**< 待构建的均值滤波半径 N。 */ + /** + * @brief 实验性功能浮动面板的 ✕ 处于"已按下、等待抬起确认"状态。 + * + * 关闭类 ✕ 统一改为左键**抬起**时生效:按下只记录目标,抬起时指针仍在该 ✕ 上才真正 + * 关闭对应功能;按住后移开再松开视为放弃。 + */ + bool m_pendingPanelClose = false; /**< 是否有待确认的面板关闭请求。 */ + int m_pendingPanelCloseLane = -1; /**< 待关闭面板所属网格下标。 */ + EdgePanelKind m_pendingPanelCloseKind = EdgePanelKind::COUNT; /**< 待关闭面板的功能类型。 */ + int m_hoverLane = -1; /**< 当前悬停网格下标。 */ int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */ int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */ diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 46a300e..13fd72e 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -364,6 +364,20 @@ namespace jdi return L"Δt:" + FormatDurationCompound(seconds); } + /** + * @brief 时间光标的读数文本("数值 + s 后缀")。 + * + * 时间轴本身就是秒,但与幅值读数放在一起时没有单位容易误读, + * 因此读数恒带 "s" 后缀;内联编辑仍使用不带单位的纯数值(见 BeginEdit 调用处)。 + * + * @param [IN] seconds 光标所在时刻(秒)。 + * @return 形如 "1.25s" 的文本。 + */ + std::wstring FormatCursorTime(double seconds) + { + return FormatValue(seconds) + L"s"; + } + /** * @brief 把时间间隔格式化为"秒"数值文本(不含单位),用于内联编辑。 * @@ -1313,7 +1327,7 @@ namespace jdi const auto rawRect = [this](int index) -> gd::RectF { const double time = (index == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime(); - const std::wstring text = FormatValue(time); + const std::wstring text = FormatCursorTime(time); const float width = std::max(64.0f * m_scale, UiMeasureTextWidth(m_fonts->monoSmall, text) + 16.0f * m_scale); const float height = 20.0f * m_scale; @@ -3338,6 +3352,9 @@ namespace jdi m_cursorGrabOffset = 0; // 新的交互开始:清掉上一次留下的提示(拖动过程中会重新设置粘滞提示)。 SetHint(std::wstring()); + // 上一轮尚未抬起确认的面板关闭请求一律作废。 + m_pendingPanelClose = false; + m_pendingPanelCloseLane = -1; if (m_scrollBarVisible && UiPointInRect(m_scrollBarRect, static_cast(x), static_cast(y))) @@ -3416,22 +3433,11 @@ namespace jdi if (UiPointInRect(layout.close, fx, fy)) { - // ✕ 关闭面板的同时关闭该网格对应的实验性功能。 - switch (kind) - { - case EdgePanelKind::CENTER: - SetLaneEdgeCenterCount(index, false); - break; - case EdgePanelKind::MEAN: - SetLaneMeanFilter(index, false); - break; - case EdgePanelKind::COUNT: - default: - SetLaneEdgeCount(index, false); - break; - } - m_dragMode = PlotDragMode::NONE; - return true; + // ✕:**抬起**时才关闭该网格对应的实验性功能,这里只记录目标; + // 按住后移开再松开视为放弃(与面板/标签栏的关闭 ✕ 语义一致)。 + m_pendingPanelClose = true; + m_pendingPanelCloseLane = index; + m_pendingPanelCloseKind = kind; } if ((kind == EdgePanelKind::CENTER || kind == EdgePanelKind::MEAN) && @@ -3584,10 +3590,42 @@ namespace jdi return changed; } - bool PlotView::OnLButtonUp(int y) + bool PlotView::OnLButtonUp(int x, int y) { bool changed = (m_dragMode != PlotDragMode::NONE); + // 实验性功能面板的 ✕:只有"按下后仍在同一个 ✕ 上抬起"才真正关闭该功能。 + if (m_pendingPanelClose) + { + const int lane = m_pendingPanelCloseLane; + const EdgePanelKind kind = m_pendingPanelCloseKind; + m_pendingPanelClose = false; + m_pendingPanelCloseLane = -1; + + if (lane >= 0 && lane < static_cast(m_lanes.size())) + { + const EdgePanelLayout layout = EdgePanelLayoutFor(lane, kind); + if (layout.close.Width > 0.0f && + UiPointInRect(layout.close, static_cast(x), static_cast(y))) + { + switch (kind) + { + case EdgePanelKind::CENTER: + SetLaneEdgeCenterCount(lane, false); + break; + case EdgePanelKind::MEAN: + SetLaneMeanFilter(lane, false); + break; + case EdgePanelKind::COUNT: + default: + SetLaneEdgeCount(lane, false); + break; + } + changed = true; + } + } + } + switch (m_dragMode) { case PlotDragMode::RUBBER_SELECT: @@ -4370,6 +4408,19 @@ namespace jdi const int saveState = g.Save(); g.SetClip(m_area, gd::CombineModeIntersect); + + // 除了图例条目本身,还要把**对应的信号曲线**一并高亮:用户在网格图例上移动时 + // 一眼就能确认这条条目对应哪条曲线(与"鼠标贴近曲线"时的光晕提示一致)。 + const ResolvedSignal resolved = ResolveSignal(m_hoverLegend); + if (resolved.data != nullptr && resolved.style != nullptr && resolved.visible) + { + const LaneVisual& lane = m_lanes[static_cast(laneIndex)]; + if (const PolylineCacheEntry* cached = AcquirePolyline(m_hoverLegend, resolved, lane)) + { + PaintSignalHalo(g, resolved, cached->points, cached->breaks); + } + } + for (const LegendHit& hit : m_legendHits) { if (hit.ref == m_hoverLegend) @@ -5656,7 +5707,7 @@ namespace jdi highlighted ? color : WithAlpha(color, 0xE0)); const bool dark = IsDarkColor(color); DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark); - UiText(g, FormatValue(which == 2 ? Tab().CursorTimeAlt() : Tab().CursorTime()), + UiText(g, FormatCursorTime(which == 2 ? Tab().CursorTimeAlt() : Tab().CursorTime()), m_fonts->monoSmall, labelRect, dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER); } diff --git a/Components/SignalPanel/Inc/SignalPanel.hpp b/Components/SignalPanel/Inc/SignalPanel.hpp index 83e1f8a..867a65d 100644 --- a/Components/SignalPanel/Inc/SignalPanel.hpp +++ b/Components/SignalPanel/Inc/SignalPanel.hpp @@ -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; /**< 菜单操作的目标信号。 */ diff --git a/Components/SignalPanel/Src/SignalPanel.cpp b/Components/SignalPanel/Src/SignalPanel.cpp index d45f202..a461bb9 100644 --- a/Components/SignalPanel/Src/SignalPanel.cpp +++ b/Components/SignalPanel/Src/SignalPanel.cpp @@ -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(row)].fileIndex, + m_rows[static_cast(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(x), static_cast(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(x), static_cast(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(x); const float fy = static_cast(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(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(item.fileIndex), - static_cast(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(x); + const float fy = static_cast(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(row)]; + if (m_host != nullptr) + { + if (pendingPart == RowPart::CLOSE) + { + PostMessageW(m_host, WM_APP + 1, static_cast(item.fileIndex), 0); + } + else + { + // wParam = 文件下标,lParam = 文件内信号下标(见 Application 的消息处理)。 + PostMessageW(m_host, WM_APP + 2, static_cast(item.fileIndex), + static_cast(item.signalIndex)); + } + } + changed = true; + } + } + if (m_draggingOpacity) { m_draggingOpacity = false; diff --git a/Components/TabBar/Src/TabBar.cpp b/Components/TabBar/Src/TabBar.cpp index f45ca2f..ad305b6 100644 --- a/Components/TabBar/Src/TabBar.cpp +++ b/Components/TabBar/Src/TabBar.cpp @@ -380,8 +380,14 @@ namespace jdi if (wasClose) { - result.type = TabBarResult::Type::CLOSE; - result.index = index; + // 关闭 ✕ 只在"按下后仍在 ✕ 上抬起"时生效:按住后移开再松开视为放弃 + // (与信号面板的文件/信号 ✕ 保持一致的交互语义)。 + if (UiPointInRect(m_tabs[static_cast(index)].closeRect, + static_cast(x), static_cast(y))) + { + result.type = TabBarResult::Type::CLOSE; + result.index = index; + } } else { diff --git a/Utilities/Inc/PaintUtil.hpp b/Utilities/Inc/PaintUtil.hpp index 8154ac5..7c577d8 100644 --- a/Utilities/Inc/PaintUtil.hpp +++ b/Utilities/Inc/PaintUtil.hpp @@ -278,6 +278,21 @@ namespace jdi void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale); + /** + * @brief 只绘制输入框的**内容**(选区高亮 / 文本 / 插入符),不绘制底板与边框。 + * + * 用于"宿主已经画好了外框"的场景(例如信号面板的「筛选信号」输入框):若直接调用 + * UiDrawEditField,会在外框内部再套一个带边框的圆角框,形成一圈多余的内圈。 + * + * @param [IN,OUT] g 绘图上下文。 + * @param [IN] rect 输入框矩形(内容区,几何与 UiDrawEditField 完全一致)。 + * @param [IN] edit 输入框编辑模型。 + * @param [IN] font 文本字体。 + * @param [IN] scale DPI 缩放比例。 + */ + void UiDrawEditFieldContent(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font, + float scale); + /** * @brief 绘制一个带边框的复选框。 * @param [IN,OUT] g 绘图上下文。 diff --git a/Utilities/Src/PaintUtil.cpp b/Utilities/Src/PaintUtil.cpp index e371c1a..749175e 100644 --- a/Utilities/Src/PaintUtil.cpp +++ b/Utilities/Src/PaintUtil.cpp @@ -550,6 +550,18 @@ namespace jdi UiFillRoundRect(g, rect, corner, theme.cardBg); UiDrawRoundRect(g, rect, corner, theme.accent, 1.6f); + UiDrawEditFieldContent(g, rect, edit, font, scale); + } + + void UiDrawEditFieldContent(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font, + float scale) + { + if (rect.Width <= 0.0f || rect.Height <= 0.0f || font == nullptr) + { + return; + } + + const Theme& theme = SolarizedLight(); const std::wstring& text = edit.Text(); const float innerLeft = rect.X + 6.0f * scale;