一、采样间隔/频率显示不一致(用户报告 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 探针,仅编译验证 + 源码级核对。
717 lines
20 KiB
C++
717 lines
20 KiB
C++
// 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<float>(x), static_cast<float>(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<float>(x), static_cast<float>(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<float>(x);
|
||
const float fy = static_cast<float>(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<float>(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<int>(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<int>(m_rows.size()))
|
||
{
|
||
return false;
|
||
}
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(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<int>(m_rows.size()))
|
||
{
|
||
return false;
|
||
}
|
||
const SignalGroup* group = m_dataSet->GroupAt(m_rows[static_cast<std::size_t>(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<float>(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<float>(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<int>(m_rows.size()))
|
||
{
|
||
break;
|
||
}
|
||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||
m_dataSetWritable->RenameSignal(item.groupIndex, item.signalIndex, trimmed);
|
||
break;
|
||
}
|
||
case StyleEditField::GROUP_TITLE:
|
||
{
|
||
if (row < 0 || row >= static_cast<int>(m_rows.size()) || m_dataSetWritable == nullptr)
|
||
{
|
||
break;
|
||
}
|
||
m_dataSetWritable->RenameGroup(m_rows[static_cast<std::size_t>(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<float>(x), static_cast<float>(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<float>(x), static_cast<float>(y)) &&
|
||
!UiPointInRect(field, static_cast<float>(x), static_cast<float>(y)))
|
||
{
|
||
// 把落点夹回输入框,保证插入符换算与绘制使用同一套几何
|
||
//(`UiEditFieldMouseDown` 自己会再算一次文本起点)。
|
||
const int clampedX = static_cast<int>(std::lround(
|
||
std::max(field.X + 1.0f, std::min(static_cast<float>(x), field.X + field.Width - 2.0f))));
|
||
const int clampedY = static_cast<int>(std::lround(
|
||
std::max(field.Y + 1.0f, std::min(static_cast<float>(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<float>(x), static_cast<float>(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<float>(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
|
||
|