// SignalPanel 实现分片 —— 内联编辑(筛选框、组名/信号名/数值输入、样式按钮)。 // // 本文件只包含 SignalPanel 的成员函数定义(从 SignalPanelInteraction.cpp 机械搬移,行为不变)。 // 放进本分片的判据:**与"把用户输入落到数据层"有关**的代码(BeginEdit/EndEdit/OnEdit*、 // 应用颜色与透明度、筛选框)。交互命中与手势派发仍在 SignalPanelInteraction.cpp。 #include "SignalPanelInternal.hpp" namespace jdi { bool SignalPanel::IsOnFilterBox(int x, int y) const { return UiPointInRect(m_filterRect, static_cast(x), static_cast(y)); } void SignalPanel::ClearFilter() { if (m_filterText.empty() && !m_filterEditing) { return; } m_filterText.clear(); if (m_filterEditing) { // 编辑态下一并重置输入模型,避免残留旧文本(Begin 会整段选中/清空)。 m_filterEdit.Begin(std::wstring(), false, EditFilter::ANY, 0u); } // 退出筛选:筛选模式下临时折起的状态与动画一并作废(行集合会整体重建)。 m_filterCollapsed.clear(); m_groupAnims.clear(); m_rowShift.clear(); // 清空后回到完整列表:重建行集合、回到顶部。 m_rowsDirty = true; m_scrollOffset = 0.0f; m_layoutDirty = true; } HCURSOR SignalPanel::QueryCursor(int x, int y) { // 拖拽排序进行中:用"移动"光标提示当前处于拖动状态(此时不应再切成手型)。 if (m_signalDragging) { return LoadCursorW(nullptr, IDC_SIZEALL); } // 清空按钮 ✕ 是独立可点击控件,用手型光标给出除高亮之外的额外交互提示。 const gd::RectF clearRect = FilterClearRect(); if (clearRect.Width > 0.0f && UiPointInRect(clearRect, static_cast(x), static_cast(y))) { return LoadCursorW(nullptr, IDC_HAND); } // 信号行尾的「移除该信号」✕ 与分组行尾的「关闭该分组」✕ 视觉完全一致 ⇒ 光标必须一致。 RowPart part = RowPart::NONE; if (HitRow(x, y, part) >= 0 && (part == RowPart::REMOVE_SIGNAL || part == RowPart::CLOSE)) { return LoadCursorW(nullptr, IDC_HAND); } // 样式区的可点击控件(「调整」行三个按钮、线宽/透明度数值框)也用手型光标。 const float fx = static_cast(x); const float fy = static_cast(y); // ⚠️ 样式区控件只在**存在编辑目标**时才可点(用户 2026-11-02 报告:没有选中信号时, // 鼠标掠过「系数/偏移 | 镜像 | 绝对值」仍会变成手型,而点了什么也不会发生)。 if (HasStyleTarget()) { // 样式区顶部「目标」行的信号名与 GX 标签:与信号列表行一致,用手型光标提示可交互。 UpdateTargetRowGeometry(); if ((m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy)) || (m_targetBadgeRect.Width > 0.0f && UiPointInRect(m_targetBadgeRect, fx, fy))) { return LoadCursorW(nullptr, IDC_HAND); } if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) || UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) || UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy) || UiPointInRect(m_widthValueRect, fx, fy) || UiPointInRect(m_opacityValueRect, fx, fy) || (m_widthResetRect.Width > 0.0f && UiPointInRect(m_widthResetRect, fx, fy)) || (m_opacityResetRect.Width > 0.0f && UiPointInRect(m_opacityResetRect, fx, fy))) { return LoadCursorW(nullptr, IDC_HAND); } } return nullptr; } void SignalPanel::BeginFilterEdit() { if (m_filterEditing) { return; } m_filterEditing = true; // 单击进入编辑:光标落在末尾,不做全选(用户可以继续追加筛选词)。 m_filterEdit.Begin(m_filterText, false, EditFilter::ANY, 0u); } bool SignalPanel::EndFilterEdit() { if (!m_filterEditing) { return false; } // **必须**在 `End()` 之前取出文本:`TextEdit::End()` 会清空内部缓冲区。 const std::wstring finalText = Trim(m_filterEdit.Text()); m_filterEditing = false; m_filterEdit.End(); // ⚠️ 必须走 `ApplyFilterText`(而不是直接赋值):只赋值会让 `m_filterText` 变成 // “普通模式”而行集合仍是筛选后的结果(输入纯空格后回车 → 列表仍然缺分组/信号)。 ApplyFilterText(finalText); return true; } bool SignalPanel::ApplyWidthFromX(float x) { // ⚠️ 判的是**实际被解引用**的指针:本函数只经 `StyleOf()` 碰 `m_dataSetWritable`, // 与 `m_tab` 无关(旧实现在这里判 `m_tab`,真出现“有 tab、无 dataset”时仍会空指针)。 if (m_dataSetWritable == nullptr) { return false; } SignalStyle* style = StyleOf(SelectedRef()); if (style == nullptr) { return false; } const double ratio = UiSliderRatioFromX(m_widthTrackRect, x); float value = static_cast(kWidthMin + ratio * (kWidthMax - kWidthMin)); value = std::round(value * 10.0f) / 10.0f; value = ClampFloat(value, kWidthMin, kWidthMax); if (std::fabs(value - style->lineWidth) < 0.001f) { return false; } style->lineWidth = value; return true; } bool SignalPanel::BeginEdit(StyleEditField field, int row) { // `m_dataSet` 在下面的 `Resolve()` / `GroupAt()` 里被直接解引用,必须一并判空。 if (m_tab == nullptr || m_dataSet == nullptr || field == StyleEditField::NONE) { return false; } if (m_editField == field && m_editRow == row) { return false; } std::wstring initial; switch (field) { case StyleEditField::WIDTH: { const SignalStyle* style = StyleOf(SelectedRef()); if (style == nullptr) { return false; } initial = FormatNumber(style->lineWidth, 1); break; } case StyleEditField::OPACITY: { const SignalStyle* style = StyleOf(SelectedRef()); if (style == nullptr) { return false; } initial = std::to_wstring(static_cast(style->opacity * 100.0f + 0.5f)); break; } case StyleEditField::SIGNAL_NAME: { // `row < 0`:样式区顶部「目标」行 —— 编辑对象就是当前选中的(目标)信号。 if (row < 0) { const SignalData* signal = SelectedRef().IsValid() ? m_dataSet->Resolve(SelectedRef()) : nullptr; if (signal == nullptr) { return false; } initial = signal->name; break; } if (row >= static_cast(m_rows.size())) { return false; } const ListRow& item = m_rows[static_cast(row)]; const SignalData* signal = m_dataSet->Resolve(SignalRef{item.groupIndex, item.signalIndex}); if (signal == nullptr) { return false; } initial = signal->name; break; } case StyleEditField::GROUP_TITLE: { if (row < 0 || row >= static_cast(m_rows.size())) { return false; } const SignalGroup* group = m_dataSet->GroupAt(m_rows[static_cast(row)].groupIndex); if (group == nullptr) { return false; } initial = group->name; break; } default: return false; } m_editField = field; m_editRow = row; if (field == StyleEditField::SIGNAL_NAME && row < 0) { // 「目标」行的输入框与行内名称矩形同源,必须先按当前目标算好几何。 UpdateTargetRowGeometry(); } // 名称类字段可输入任意字符,数值类字段限制为数字与小数点。 const bool nameField = (field == StyleEditField::SIGNAL_NAME) || (field == StyleEditField::GROUP_TITLE); // 不限长度:名称与数值都由各自的提交逻辑处理超长内容。 m_edit.Begin(std::move(initial), true, nameField ? EditFilter::ANY : EditFilter::NUMBER, 0u); return true; } bool SignalPanel::EndEdit(bool commit) { if (m_editField == StyleEditField::NONE) { return false; } const StyleEditField field = m_editField; const int row = m_editRow; const std::wstring text = m_edit.Text(); m_editField = StyleEditField::NONE; m_editRow = -1; m_edit.End(); if (!commit || m_tab == nullptr || m_dataSet == nullptr) { return true; } const std::wstring trimmed = Trim(text); switch (field) { case StyleEditField::WIDTH: { if (trimmed.empty()) { break; } wchar_t* stop = nullptr; const double parsed = std::wcstod(trimmed.c_str(), &stop); if (stop == trimmed.c_str() || parsed != parsed) { break; } if (SignalStyle* style = StyleOf(SelectedRef())) { const float value = ClampFloat(static_cast(parsed), kWidthMin, kWidthMax); style->lineWidth = std::round(value * 10.0f) / 10.0f; } break; } case StyleEditField::OPACITY: { if (trimmed.empty()) { break; } wchar_t* stop = nullptr; const double parsed = std::wcstod(trimmed.c_str(), &stop); if (stop == trimmed.c_str() || parsed != parsed) { break; } if (SignalStyle* style = StyleOf(SelectedRef())) { style->opacity = ClampFloat(static_cast(parsed) / 100.0f, 0.0f, 1.0f); } break; } case StyleEditField::SIGNAL_NAME: { if (m_dataSetWritable == nullptr) { break; } if (row < 0) { // 样式区「目标」行:重命名当前选中的(目标)信号。 const SignalRef& target = SelectedRef(); if (target.IsValid()) { m_dataSetWritable->RenameSignal(target.groupIndex, target.signalIndex, trimmed); } // 名称变化会改变「目标」行的文本宽度 ⇒ 几何缓存作废。 m_targetGeomValid = false; break; } if (row >= static_cast(m_rows.size())) { break; } const ListRow& item = m_rows[static_cast(row)]; m_dataSetWritable->RenameSignal(item.groupIndex, item.signalIndex, trimmed); break; } case StyleEditField::GROUP_TITLE: { if (row < 0 || row >= static_cast(m_rows.size()) || m_dataSetWritable == nullptr) { break; } m_dataSetWritable->RenameGroup(m_rows[static_cast(row)].groupIndex, trimmed); break; } default: break; } return true; } bool SignalPanel::OnEditChar(wchar_t character) { // 「筛选信号」输入框:字符实时进入筛选(无需回车即可看到结果变化)。 if (m_filterEditing) { switch (character) { case 0x03: // Ctrl+C m_filterEdit.CopyToClipboard(m_host); return false; case 0x18: // Ctrl+X m_filterEdit.CutToClipboard(m_host); ApplyFilterText(m_filterEdit.Text()); return true; case 0x16: // Ctrl+V m_filterEdit.PasteFromClipboard(m_host); ApplyFilterText(m_filterEdit.Text()); return true; case 0x01: // Ctrl+A m_filterEdit.SelectAll(); return true; default: break; } if (character == L'\r' || character == L'\n') { return EndFilterEdit(); } // 退格/删除已由 WM_KEYDOWN 的 OnEditKey 处理,这里忽略控制字符避免重复删除。 if (character < 0x20 || character == 0x7F) { return false; } if (!m_filterEdit.Insert(character)) { return false; } ApplyFilterText(m_filterEdit.Text()); return true; } if (m_editField == StyleEditField::NONE) { return false; } // 剪贴板与全选快捷键经 WM_CHAR 送达(Ctrl+C/X/V/A 分别对应 3/24/22/1)。 switch (character) { case 0x03: // Ctrl+C m_edit.CopyToClipboard(m_host); return false; case 0x18: // Ctrl+X return m_edit.CutToClipboard(m_host); case 0x16: // Ctrl+V return m_edit.PasteFromClipboard(m_host); case 0x01: // Ctrl+A m_edit.SelectAll(); return true; default: break; } if (character == L'\r' || character == L'\n') { return EndEdit(true); } // 退格/删除已由 WM_KEYDOWN 的 OnEditKey 处理:这里必须忽略控制字符, // 否则同一次按键会被处理两次(一次删除两个字符)。 if (character < 0x20 || character == 0x7F) { return false; } // 百分比字段允许直接输入 % 但会被忽略,其余控制字符一律丢弃。 if (m_editField == StyleEditField::OPACITY && character == L'%') { return false; } return m_edit.Insert(character); } bool SignalPanel::OnEditKey(WPARAM key) { // 「筛选信号」输入框的键盘处理(回车结束编辑,Esc 清空并结束)。 if (m_filterEditing) { const bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0; const bool control = (GetKeyState(VK_CONTROL) & 0x8000) != 0; switch (key) { case VK_RETURN: return EndFilterEdit(); case VK_ESCAPE: { const bool changed = m_filterEdit.Text() != m_filterText; EndFilterEdit(); return changed; } case VK_BACK: { if (!m_filterEdit.Backspace()) { return false; } ApplyFilterText(m_filterEdit.Text()); return true; } case VK_DELETE: { if (!m_filterEdit.DeleteForward()) { return false; } ApplyFilterText(m_filterEdit.Text()); return true; } case VK_LEFT: return m_filterEdit.MoveCaret(-1, shift); case VK_RIGHT: return m_filterEdit.MoveCaret(1, shift); case VK_HOME: return m_filterEdit.MoveHome(shift); case VK_END: return m_filterEdit.MoveEnd(shift); case 'A': if (control) { m_filterEdit.SelectAll(); return true; } break; default: break; } return true; } if (m_editField == StyleEditField::NONE) { return false; } const bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0; const bool control = (GetKeyState(VK_CONTROL) & 0x8000) != 0; switch (key) { case VK_RETURN: return EndEdit(true); case VK_ESCAPE: return EndEdit(false); case VK_BACK: return m_edit.Backspace(); case VK_DELETE: return m_edit.DeleteForward(); case VK_LEFT: return m_edit.MoveCaret(-1, shift); case VK_RIGHT: return m_edit.MoveCaret(1, shift); case VK_HOME: return m_edit.MoveHome(shift); case VK_END: return m_edit.MoveEnd(shift); case 'A': if (control) { m_edit.SelectAll(); return true; } break; default: break; } return true; } bool SignalPanel::CollapseEditSelectionToEnd() { if (m_filterEditing) { return m_filterEdit.DeselectToEnd(); } if (m_editField == StyleEditField::NONE) { return false; } return m_edit.DeselectToEnd(); } bool SignalPanel::OnEditMouseDown(int x, int y) { if (m_filterEditing) { // 编辑态下 ✕ 位于输入框内容区之内,必须优先于插入符定位处理, // 否则点击会被当成"把光标移到此处"而清空不了内容。 // 与其它 ✕ 一致:按下只记录目标,抬起时仍在 ✕ 上才真正清空。 const gd::RectF clearRect = FilterClearRect(); if (clearRect.Width > 0.0f && UiPointInRect(clearRect, static_cast(x), static_cast(y))) { m_pendingFilterClear = true; return true; } return UiEditFieldMouseDown(m_fonts->uiSmall, FilterFieldRect(), m_filterEdit, m_scale, x, y); } if (m_editField == StyleEditField::NONE) { return false; } // ⚠️⚠️ **编辑态下,按在被编辑的那一行名称区域内(含输入框上下各 3px 的行内边距) // 都必须当作“文本操作”**,不能穿透成行点击。否则:容器行高 > 输入框高度, // 鼠标起点只要落在内边距上就会走 `OnLButtonDown` 的分组分支 ⇒ **分组被展开/收起** //(用户 2026-10-19 报告:“框选编辑文本时组会展开、收起”)。 // 横向仍限定在名称区域内:同一行的复选框 / 「N 项」/ GX 徽标 / ✕ 继续可点。 const gd::RectF field = EditFieldRect(); if (field.Width > 0.0f) { const gd::RectF absorb = NameEditAbsorbRect(field); if (UiPointInRect(absorb, static_cast(x), static_cast(y)) && !UiPointInRect(field, static_cast(x), static_cast(y))) { // 把落点夹回输入框,保证插入符换算与绘制使用同一套几何 //(`UiEditFieldMouseDown` 自己会再算一次文本起点)。 const int clampedX = static_cast(std::lround( std::max(field.X + 1.0f, std::min(static_cast(x), field.X + field.Width - 2.0f)))); const int clampedY = static_cast(std::lround( std::max(field.Y + 1.0f, std::min(static_cast(y), field.Y + field.Height - 2.0f)))); return UiEditFieldMouseDown(EditFieldFont(), field, m_edit, m_scale, clampedX, clampedY); } } return UiEditFieldMouseDown(EditFieldFont(), field, m_edit, m_scale, x, y); } bool SignalPanel::OnEditMouseMove(int x) { if (m_filterEditing) { return UiEditFieldMouseMove(m_fonts->uiSmall, FilterFieldRect(), m_filterEdit, m_scale, x); } if (m_editField == StyleEditField::NONE) { return false; } return UiEditFieldMouseMove(EditFieldFont(), EditFieldRect(), m_edit, m_scale, x); } 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; } m_filterEdit.EndDrag(); return true; } if (!m_edit.Dragging()) { return false; } m_edit.EndDrag(); return true; } void SignalPanel::SelectSignal(const SignalRef& ref) { if (m_dataSetWritable != nullptr) { m_dataSetWritable->SetSelected(ref); } } bool SignalPanel::ApplyColor(std::uint32_t color) { // 同 `ApplyWidthFromX`:只经 `StyleOf()` 使用 `m_dataSetWritable`。 if (m_dataSetWritable == nullptr) { return false; } SignalStyle* style = StyleOf(SelectedRef()); if (style == nullptr) { return false; } // 同时记住最后一次自定义取值,便于切换到预设色后一键回到该颜色。 const bool changed = (style->color != color) || (style->customColor != color); style->color = color; style->customColor = color; return changed; } bool SignalPanel::ApplyOpacityFromX(float x) { // 同 `ApplyWidthFromX`:只经 `StyleOf()` 使用 `m_dataSetWritable`。 if (m_dataSetWritable == nullptr) { return false; } SignalStyle* style = StyleOf(SelectedRef()); if (style == nullptr) { return false; } const float value = static_cast(UiSliderRatioFromX(m_opacityTrackRect, x)); if (std::fabs(value - style->opacity) < 0.005f) { return false; } style->opacity = value; return true; } bool SignalPanel::ResetStyleLineWidth() { if (m_dataSet == nullptr || m_dataSetWritable == nullptr) { return false; } SignalStyle* style = StyleOf(SelectedRef()); if (style == nullptr) { return false; } const SignalData* signal = m_dataSet->Resolve(SelectedRef()); if (signal == nullptr) { return false; } // 默认线与新建信号同源(`DefaultSignalLineWidth`):单数据点信号用最大线宽, // 其余用 `kDefaultSignalLineWidth`(用户明确要求"单值信号和普通信号的默认线宽不一样")。 const float target = DefaultSignalLineWidth(signal->finiteCount); if (std::fabs(style->lineWidth - target) < 0.001f) { return false; } style->lineWidth = target; return true; } bool SignalPanel::ResetStyleOpacity() { if (m_dataSetWritable == nullptr) { return false; } SignalStyle* style = StyleOf(SelectedRef()); if (style == nullptr) { return false; } if (std::fabs(style->opacity - 1.0f) < 0.001f) { return false; } style->opacity = 1.0f; return true; } } // namespace jdi