原文件混杂了标签页、派生信号、菜单、工具栏、键盘鼠标、布局调试绘制六类职责,
是本项目最大的“大杂烩”单文件之一。按职责纯搬移(零逻辑改动):
- ApplicationTabs.cpp (355 行): 标签页新建/关闭/切换/重排、视图同步、窗口标题、
标签栏结果回调、标签页右键菜单、待提交改名。
- ApplicationDerive.cpp (786 行): 派生烘焙、重采样、选区截取(新建/就地)、克隆(含带调整)。
- ApplicationMenus.cpp (409 行): 绘图区右键菜单、菜单数值回写、主题切换、设置菜单、
菜单结果派发。
- ApplicationCommands.cpp (642 行): 分组/信号增删、移动与重排、文件对话框、工具栏动作、
键盘鼠标分发、布局边界绘制。
Makefile 按目录通配收集,无需登记新文件。
验证:release 零 error 零 warning;产物 793,088 -> 794,112 B(+1,024,LTO 内联边界变化,
< 0.13%,属结构性拆分不可避免的噪声)。
647 lines
20 KiB
C++
647 lines
20 KiB
C++
// Application 实现分片 —— 本文件保留:**数据命令**(分组/信号增删、移动与重排)、
|
||
// 文件对话框、工具栏动作、键盘与鼠标分发、布局边界调试绘制。
|
||
//
|
||
// 共用的常量与工具函数在 ApplicationInternal.hpp(命名空间 `appdetail`,全部 inline)。
|
||
// 本文件原为 2,165 行的“大杂烩”,2026-11-04 代码审查按职责拆出:
|
||
// 标签页 → `ApplicationTabs.cpp`;派生/重采样/截取/克隆 → `ApplicationDerive.cpp`;
|
||
// 上下文菜单与设置菜单 → `ApplicationMenus.cpp`。划分见 `Code_Review_Plan.md` 的 P11。
|
||
#include "ApplicationInternal.hpp"
|
||
|
||
#include "UiDebugBounds.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
void Application::RemoveGroup(int groupIndex)
|
||
{
|
||
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
|
||
if (group == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
const std::wstring name = group->name;
|
||
|
||
// 移除分组会释放大量采样内存:先暂停后台派生曲线计算,避免其读取已释放内存。
|
||
m_plot.PauseDerivedAsync();
|
||
// 先按旧下标修正各标签页配置与共享选中项,再从数据层移除,最后统一对齐。
|
||
for (TabConfig& tab : m_tabs)
|
||
{
|
||
tab.RemapAfterGroupRemoval(groupIndex);
|
||
}
|
||
m_dataSet.RemapSelectionAfterGroupRemoval(groupIndex);
|
||
m_dataSet.RemoveGroup(groupIndex);
|
||
SyncTabsToDataSet();
|
||
|
||
SetStatusMessage(L"已从工作区移除:" + name, false);
|
||
UpdateWindowTitle();
|
||
}
|
||
|
||
void Application::RemoveSignal(int groupIndex, int signalIndex)
|
||
{
|
||
SignalRef target;
|
||
target.groupIndex = groupIndex;
|
||
target.signalIndex = signalIndex;
|
||
|
||
const SignalData* signal = m_dataSet.Resolve(target);
|
||
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
|
||
if (signal == nullptr || group == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
const std::wstring name = signal->name;
|
||
|
||
if (group->signals.size() <= 1)
|
||
{
|
||
// 组内只剩这一条信号:直接移除整个分组,避免在列表里留下一个空分组。
|
||
RemoveGroup(groupIndex);
|
||
return;
|
||
}
|
||
|
||
// 移除信号会释放采样内存:先暂停后台派生曲线计算,避免其读取已释放内存。
|
||
m_plot.PauseDerivedAsync();
|
||
// 先按旧下标修正各标签页配置与共享选中项,再从数据层移除(释放采样内存),最后对齐。
|
||
for (TabConfig& tab : m_tabs)
|
||
{
|
||
tab.RemapAfterSignalRemoval(groupIndex, signalIndex);
|
||
}
|
||
m_dataSet.RemapSelectionAfterSignalRemoval(groupIndex, signalIndex);
|
||
m_dataSet.RemoveSignal(groupIndex, signalIndex);
|
||
SyncTabsToDataSet();
|
||
|
||
SetStatusMessage(L"已从工作区移除信号:" + name, false);
|
||
}
|
||
|
||
std::wstring Application::UniqueGroupName(const std::wstring& base) const
|
||
{
|
||
const auto taken = [this](const std::wstring& candidate)
|
||
{
|
||
for (const SignalGroup& group : m_dataSet.Groups())
|
||
{
|
||
if (group.name == candidate)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
if (!taken(base))
|
||
{
|
||
return base;
|
||
}
|
||
for (int suffix = 2; suffix < 1000; ++suffix)
|
||
{
|
||
const std::wstring candidate = base + L" (" + std::to_wstring(suffix) + L")";
|
||
if (!taken(candidate))
|
||
{
|
||
return candidate;
|
||
}
|
||
}
|
||
return base;
|
||
}
|
||
|
||
bool Application::MoveSignalCore(int fromGroup, int fromSignal, int toGroup, int toIndex,
|
||
const std::wstring* newGroupName)
|
||
{
|
||
const SignalGroup* sourceGroup = m_dataSet.GroupAt(fromGroup);
|
||
if (sourceGroup == nullptr || fromSignal < 0 ||
|
||
static_cast<std::size_t>(fromSignal) >= sourceGroup->signals.size())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 「移动到全新分组」:目标分组尚不存在,它会被追加到分组列表末尾
|
||
//(下标 = 当前分组数,可在此之前预先算出,供引用改写使用)。
|
||
const bool createGroup = (toGroup < 0) && (newGroupName != nullptr);
|
||
const int resolvedTo = createGroup ? static_cast<int>(m_dataSet.GroupCount()) : toGroup;
|
||
const SignalGroup* targetGroup = createGroup ? nullptr : m_dataSet.GroupAt(resolvedTo);
|
||
if (!createGroup && targetGroup == nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const bool sameGroup = !createGroup && (fromGroup == resolvedTo);
|
||
|
||
// 目标下标:负数/越界表示追加到末尾(组内重排时"末尾"= 组内信号数-1,因为该信号会先被摘出)。
|
||
int position = toIndex;
|
||
const int targetCount = createGroup ? 0 : static_cast<int>(targetGroup->signals.size());
|
||
if (createGroup)
|
||
{
|
||
position = 0;
|
||
}
|
||
else if (sameGroup)
|
||
{
|
||
if (position < 0 || position >= targetCount)
|
||
{
|
||
position = targetCount - 1;
|
||
}
|
||
if (position == fromSignal)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
else if (position < 0 || position > targetCount)
|
||
{
|
||
position = targetCount;
|
||
}
|
||
|
||
SignalRef from;
|
||
from.groupIndex = fromGroup;
|
||
from.signalIndex = fromSignal;
|
||
SignalRef to;
|
||
to.groupIndex = resolvedTo;
|
||
to.signalIndex = position;
|
||
|
||
// 移动会搬运大量采样缓冲:先暂停后台派生曲线计算,避免其读取正在被移动的内存。
|
||
m_plot.PauseDerivedAsync();
|
||
|
||
// 先按**旧下标**修正各标签页内的全部信号引用,再动数据层。
|
||
// ⚠️ 跨组(含新建分组)时目标分组那一行可见性表可能还不存在,
|
||
// `RemapAfterSignalMove` 内部会自行扩展 —— 这里**不能**先 `SyncToDataSet`,
|
||
// 因为那次同步会按旧下标清理/修正引用,随后下标一变就会指向错误的信号。
|
||
if (sameGroup)
|
||
{
|
||
for (TabConfig& tab : m_tabs)
|
||
{
|
||
tab.RemapAfterSignalReorder(fromGroup, fromSignal, position);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
for (TabConfig& tab : m_tabs)
|
||
{
|
||
tab.RemapAfterSignalMove(from, to, tab.IsVisible(from));
|
||
}
|
||
}
|
||
|
||
// 共享选中项:被移动的正是当前选中信号时,改为指向新位置。
|
||
if (m_dataSet.Selected().groupIndex == fromGroup)
|
||
{
|
||
if (sameGroup)
|
||
{
|
||
// 与 `RemapAfterSignalReorder` 同一套线性映射。
|
||
int sel = m_dataSet.Selected().signalIndex;
|
||
if (sel == fromSignal)
|
||
{
|
||
sel = position;
|
||
}
|
||
else if (fromSignal < position)
|
||
{
|
||
if (sel > fromSignal && sel <= position)
|
||
{
|
||
--sel;
|
||
}
|
||
}
|
||
else if (sel >= position && sel < fromSignal)
|
||
{
|
||
++sel;
|
||
}
|
||
SignalRef next;
|
||
next.groupIndex = fromGroup;
|
||
next.signalIndex = sel;
|
||
m_dataSet.SetSelected(next);
|
||
}
|
||
else if (m_dataSet.Selected().signalIndex == fromSignal)
|
||
{
|
||
m_dataSet.SetSelected(to);
|
||
}
|
||
}
|
||
|
||
int newSignalIndex = -1;
|
||
int newGroupIndex = -1;
|
||
if (createGroup)
|
||
{
|
||
// 建组 + 搬运做成一次原子操作(`AddGroup` 拒收空组,见 `MoveSignalToNewGroup`)。
|
||
if (!m_dataSet.MoveSignalToNewGroup(fromGroup, fromSignal, *newGroupName, newGroupIndex,
|
||
newSignalIndex))
|
||
{
|
||
// 前面已校验过,理论不可达;若真发生(数据层拒绝),必须把已改写的引用
|
||
// 按数据层的真实状态重新对齐,避免留下指向不存在分组的悬空引用。
|
||
SyncTabsToDataSet();
|
||
return false;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 参数已在此前校验过(分组与信号下标都合法),本调用不会失败。
|
||
m_dataSet.MoveSignal(fromGroup, fromSignal, resolvedTo, position, newSignalIndex);
|
||
}
|
||
|
||
SyncTabsToDataSet();
|
||
return true;
|
||
}
|
||
|
||
void Application::ReorderSignal(int fromGroup, int fromSignal, int toGroup, int toIndex)
|
||
{
|
||
(void)MoveSignalCore(fromGroup, fromSignal, toGroup, toIndex, nullptr);
|
||
}
|
||
|
||
void Application::MoveSignalToGroup(int fromGroup, int fromSignal, int toGroup)
|
||
{
|
||
const SignalGroup* sourceGroup = m_dataSet.GroupAt(fromGroup);
|
||
if (sourceGroup == nullptr || fromSignal < 0 ||
|
||
static_cast<std::size_t>(fromSignal) >= sourceGroup->signals.size())
|
||
{
|
||
return;
|
||
}
|
||
const std::wstring signalName = sourceGroup->signals[static_cast<std::size_t>(fromSignal)].name;
|
||
|
||
if (toGroup < 0)
|
||
{
|
||
// 「移动到… → 新建分组」:组名固定为「新建分组」(重名自动加序号)。
|
||
const std::wstring name = UniqueGroupName(L"新建分组");
|
||
if (MoveSignalCore(fromGroup, fromSignal, -1, 0, &name))
|
||
{
|
||
SetStatusMessage(L"已将信号「" + signalName + L"」移动到新建分组:" + name, false);
|
||
}
|
||
return;
|
||
}
|
||
if (toGroup == fromGroup)
|
||
{
|
||
return; // 已经在该分组,无需移动
|
||
}
|
||
|
||
if (!MoveSignalCore(fromGroup, fromSignal, toGroup, -1, nullptr))
|
||
{
|
||
return;
|
||
}
|
||
|
||
const SignalGroup* destination = m_dataSet.GroupAt(toGroup);
|
||
SetStatusMessage(L"已将信号「" + signalName + L"」移动到分组:" +
|
||
(destination != nullptr ? destination->name : std::wstring()),
|
||
false);
|
||
}
|
||
|
||
void Application::OpenFileDialog()
|
||
{
|
||
// 允许一次选择多个文件,便于跨文件对比信号。
|
||
std::vector<wchar_t> buffer(static_cast<std::size_t>(MAX_PATH) * 16, L'\0');
|
||
|
||
OPENFILENAMEW dialog = {};
|
||
dialog.lStructSize = sizeof(dialog);
|
||
dialog.hwndOwner = m_hwnd;
|
||
dialog.lpstrFilter = SignalFileDialogFilter();
|
||
dialog.lpstrFile = buffer.data();
|
||
dialog.nMaxFile = static_cast<DWORD>(buffer.size());
|
||
dialog.lpstrTitle = L"打开测量数据文件(可多选)";
|
||
dialog.lpstrDefExt = L"csv";
|
||
dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_EXPLORER | OFN_NOCHANGEDIR |
|
||
OFN_ALLOWMULTISELECT;
|
||
|
||
if (GetOpenFileNameW(&dialog) == FALSE)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 多选时首段为目录,其余为文件名;单选时整段即为完整路径。
|
||
const std::wstring first(buffer.data());
|
||
const wchar_t* cursor = buffer.data() + first.size() + 1;
|
||
|
||
int loaded = 0;
|
||
if (*cursor == L'\0')
|
||
{
|
||
if (LoadFile(first))
|
||
{
|
||
++loaded;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
while (*cursor != L'\0')
|
||
{
|
||
const std::wstring name(cursor);
|
||
if (LoadFile(first + L"\\" + name))
|
||
{
|
||
++loaded;
|
||
}
|
||
cursor += name.size() + 1;
|
||
}
|
||
}
|
||
|
||
if (loaded > 0)
|
||
{
|
||
SetStatusMessage(L"已加载 " + std::to_wstring(loaded) +
|
||
L" 个文件,请在左侧勾选需要查看的信号。",
|
||
false);
|
||
}
|
||
}
|
||
|
||
void Application::ExecuteToolbarAction(ToolbarAction action)
|
||
{
|
||
switch (action)
|
||
{
|
||
case ToolbarAction::SETTINGS:
|
||
// 齿轮按钮只负责**展开菜单**:不改变任何业务状态,因此不必走后面的"整帧失效",
|
||
// 只需要把浮层画出来(菜单打开后 `Render` 会因 `overlaysVisible` 自动整帧重绘)。
|
||
OpenSettingsMenu();
|
||
if (m_hwnd != nullptr)
|
||
{
|
||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||
}
|
||
return;
|
||
case ToolbarAction::OPEN_FILE:
|
||
OpenFileDialog();
|
||
break;
|
||
case ToolbarAction::FIT_ALL:
|
||
// 自适应同时回到初始状态:退出缩放模式并显示全部信号范围。
|
||
m_plot.FitAll();
|
||
SetStatusMessage(L"已恢复自适应视图。", false);
|
||
break;
|
||
case ToolbarAction::AUTO_FIT_Y:
|
||
{
|
||
// 与网格右键菜单中的「选区 Y 轴自适应(全局)」同一开关:
|
||
// 打开后立即按当前视图自适应一次,之后每次时间轴选区放大都会自动匹配。
|
||
const bool enable = !m_plot.AllLanesAutoFitToSelection();
|
||
m_plot.SetAllLanesAutoFitToSelection(enable, true);
|
||
SetStatusMessage(enable ? L"已开启选区 Y 轴自适应:时间轴选区放大后自动匹配各网格 Y 轴。"
|
||
: L"已关闭选区 Y 轴自适应。",
|
||
false);
|
||
break;
|
||
}
|
||
case ToolbarAction::ZOOM_SELECT:
|
||
m_plot.ToggleZoomMode(PlotZoomMode::SELECT);
|
||
break;
|
||
case ToolbarAction::CURSOR:
|
||
m_plot.CycleCursorMode();
|
||
break;
|
||
case ToolbarAction::MERGE_LANES:
|
||
if (!m_dataSet.Empty())
|
||
{
|
||
ActiveTab().SetMode(LaneMode::MERGED);
|
||
SetStatusMessage(L"已将当前标签页的全部可见信号合并到同一网格。", false);
|
||
}
|
||
break;
|
||
case ToolbarAction::SPLIT_LANES:
|
||
if (!m_dataSet.Empty())
|
||
{
|
||
ActiveTab().SetMode(LaneMode::PER_SIGNAL);
|
||
SetStatusMessage(L"当前标签页已切换为「每信号一格」。", false);
|
||
}
|
||
break;
|
||
case ToolbarAction::CLEAR_ALL:
|
||
if (!m_dataSet.Empty())
|
||
{
|
||
ActiveTab().SetAllVisible(false);
|
||
m_panel.InvalidateRows();
|
||
SetStatusMessage(L"已清空当前标签页的全部网格。", false);
|
||
}
|
||
break;
|
||
case ToolbarAction::NONE:
|
||
default:
|
||
break;
|
||
}
|
||
|
||
if (m_hwnd != nullptr)
|
||
{
|
||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||
}
|
||
}
|
||
|
||
bool Application::HandleKeyDown(WPARAM key)
|
||
{
|
||
const bool control = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
|
||
|
||
if (control && key == L'O')
|
||
{
|
||
OpenFileDialog();
|
||
return true;
|
||
}
|
||
if (control && key == L'T')
|
||
{
|
||
NewTab(true);
|
||
return true;
|
||
}
|
||
if (control && key == L'W')
|
||
{
|
||
CloseTab(m_activeTab);
|
||
return true;
|
||
}
|
||
if (control && key == VK_TAB)
|
||
{
|
||
if (m_tabs.size() > 1)
|
||
{
|
||
const bool backward = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
|
||
const int count = static_cast<int>(m_tabs.size());
|
||
SetActiveTab((m_activeTab + (backward ? count - 1 : 1)) % count);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
switch (key)
|
||
{
|
||
case VK_ESCAPE:
|
||
{
|
||
// Esc 的语义:
|
||
// 1) 放弃当前正在进行的拖拽(典型场景:缩放的框选选区还没松开左键);
|
||
// 取消后不应用选区、视图范围保持不变;
|
||
// 2) 同时退出全部缩放模式,使工具栏「时间轴选区」「Y 轴选区」回到未选中态。
|
||
// Esc 不与时间光标关联:关闭光标请用工具栏「光标」按钮,避免误关指针。
|
||
bool changed = m_plot.CancelDrag();
|
||
changed = m_plot.ExitZoomModes() || changed;
|
||
return changed;
|
||
}
|
||
case VK_ADD:
|
||
case VK_OEM_PLUS:
|
||
m_plot.ZoomTimeStep(1.4);
|
||
return true;
|
||
case VK_SUBTRACT:
|
||
case VK_OEM_MINUS:
|
||
m_plot.ZoomTimeStep(1.0 / 1.4);
|
||
return true;
|
||
case VK_LEFT:
|
||
m_plot.PanTimeByFraction(-0.12);
|
||
return true;
|
||
case VK_RIGHT:
|
||
m_plot.PanTimeByFraction(0.12);
|
||
return true;
|
||
case VK_UP:
|
||
m_plot.ScrollLanesByFraction(-0.12);
|
||
return true;
|
||
case VK_DOWN:
|
||
m_plot.ScrollLanesByFraction(0.12);
|
||
return true;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
if (key == L'F')
|
||
{
|
||
m_plot.FitAll();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void Application::UpdateDragCursor(int x, int y)
|
||
{
|
||
// 鼠标捕获期间不再有 WM_SETCURSOR,只能自己按当前状态设置光标。
|
||
HCURSOR cursor = nullptr;
|
||
switch (m_activeTarget)
|
||
{
|
||
case Target::PLOT:
|
||
cursor = m_plot.QueryCursor(x, y);
|
||
break;
|
||
case Target::SPLITTER:
|
||
// 拖分隔条 → 左右箭头(否则按下瞬间光标会跳回箭头)。
|
||
cursor = LoadCursorW(nullptr, IDC_SIZEWE);
|
||
break;
|
||
case Target::EDIT_FIELD:
|
||
// 输入框内拖拽框选 → I 形光标。
|
||
cursor = LoadCursorW(nullptr, IDC_IBEAM);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
SetCursor(cursor != nullptr ? cursor : LoadCursorW(nullptr, IDC_ARROW));
|
||
}
|
||
|
||
bool Application::HandleMouseMove(int x, int y, bool buttonDown)
|
||
{
|
||
if (buttonDown && m_activeTarget != Target::NONE)
|
||
{
|
||
switch (m_activeTarget)
|
||
{
|
||
case Target::SPLITTER:
|
||
if (m_draggingSplitter && m_scale > 0.0f)
|
||
{
|
||
const float desired = static_cast<float>(x - m_splitterGrabOffset) / m_scale;
|
||
// ⚠️ 上限必须与 `Relayout()` 里**同一个公式**(`min(kMaxPanelWidth, 客户区宽 × 0.45)`):
|
||
// 旧实现用 `min(kMaxPanelWidth, kMinClientWidth / m_scale)` 恒等于 `kMaxPanelWidth`,
|
||
// 窄窗口下能把面板拖到 400 逻辑像素,随后 `Relayout` 又悄悄夹回来 ⇒ 拖动时宽度回弹。
|
||
const float clientWidth = static_cast<float>(std::max(0, m_backWidth)) / m_scale;
|
||
const float maximum = std::min(kMaxPanelWidth, std::max(0.0f, clientWidth) * 0.45f);
|
||
m_panelWidth = std::max(kMinPanelWidth, std::min(desired, maximum));
|
||
Relayout();
|
||
return true;
|
||
}
|
||
return false;
|
||
case Target::PLOT:
|
||
return m_plot.OnMouseMove(x, y);
|
||
case Target::PANEL:
|
||
return m_panel.OnMouseMove(x, y);
|
||
case Target::TOOLBAR:
|
||
return m_toolbar.OnMouseMove(x, y);
|
||
case Target::TAB_BAR:
|
||
return m_tabBar.OnMouseMove(x, y);
|
||
case Target::EDIT_FIELD:
|
||
{
|
||
// 输入框拖拽框选:按当前正在编辑的组件分发。
|
||
bool changed = m_tabBar.OnEditMouseMove(x);
|
||
changed = m_panel.OnEditMouseMove(x) || changed;
|
||
changed = m_plot.OnEditMouseMove(x) || changed;
|
||
changed = m_colorPicker.OnEditMouseMove(x) || changed;
|
||
return changed;
|
||
}
|
||
case Target::NONE:
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
bool changed = false;
|
||
changed =
|
||
(m_tabBar.HitTest(x, y) ? m_tabBar.OnMouseMove(x, y) : m_tabBar.OnMouseLeave()) || changed;
|
||
// ⚠️ 按住工具栏按钮期间**绝不能**走 `OnMouseLeave()`:它会清掉按下态(`m_pressedIndex`),
|
||
// 于是随后在按钮上松手时什么也不会发生 —— 这正是“快速点击工具栏按钮时状态没切换”
|
||
// 的另一个成因(鼠标带着按下态从按钮边缘移出时会命中这里)。
|
||
if (m_toolbar.HitTest(x, y))
|
||
{
|
||
changed = m_toolbar.OnMouseMove(x, y) || changed;
|
||
}
|
||
else if (m_activeTarget != Target::TOOLBAR)
|
||
{
|
||
changed = m_toolbar.OnMouseLeave() || changed;
|
||
}
|
||
changed = (m_panel.HitTest(x, y) ? m_panel.OnMouseMove(x, y) : m_panel.OnMouseLeave()) || changed;
|
||
changed = (m_plot.HitTest(x, y) ? m_plot.OnMouseMove(x, y) : m_plot.OnMouseLeave()) || changed;
|
||
return changed;
|
||
}
|
||
|
||
bool Application::ApplyPendingPanelScroll()
|
||
{
|
||
SignalRef ref;
|
||
if (!m_plot.TakePanelScrollRequest(ref) || !ref.IsValid())
|
||
{
|
||
return false;
|
||
}
|
||
// 平滑滚动(带动画):用户要求"是滚动的而不是立刻翻到目标信号位置"。
|
||
m_panel.ScrollToSignalAnimated(ref);
|
||
return true;
|
||
}
|
||
|
||
bool Application::ApplyPendingPlotLaneScroll()
|
||
{
|
||
int laneIndex = -1;
|
||
if (!m_panel.TakePlotLaneScrollRequest(laneIndex))
|
||
{
|
||
return false;
|
||
}
|
||
// 目标网格不在绘图区可视区内时才真正滚动(已完整可见则不动视图)。
|
||
return m_plot.ScrollLaneIntoView(laneIndex);
|
||
}
|
||
|
||
int Application::ComponentLayer(const UiComponent* component) const
|
||
{
|
||
if (component == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
for (std::size_t index = 0; index < m_components.size(); ++index)
|
||
{
|
||
if (m_components[index].component == component)
|
||
{
|
||
return static_cast<int>(index) * kLayersPerComponent;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
void Application::PaintLayoutBounds(gd::Graphics& g, int width, int height)
|
||
{
|
||
// ⚠️ 复用成员容器(不是局部变量):文本几何已经在**绘制期间**由 `PaintUtil`
|
||
// 通过 `UiSetTextRectSink(&m_debugBoxes)` 收进来了,这里只负责补组件结构。
|
||
//
|
||
// 层号与 `Render` 里下发的"文本层"**完全同源**(组件表下标 / `OverlayLayer`):
|
||
// 只有两者一致,遮挡矩形才能正确地把"浮层下面的内容"(含文字框)挖掉,
|
||
// 而不是反过来让下层文字穿透到浮层之上(用户 2026-10-29 报告的缺陷)。
|
||
m_debugBoxes.SetLayer(0);
|
||
|
||
// 客户区外框:便于确认"窗口边缘 == 最外层边界"。
|
||
m_debugBoxes.Add(gd::RectF(0.0f, 0.0f, static_cast<float>(width),
|
||
static_cast<float>(height)), 0);
|
||
|
||
// 组件区域(底 → 顶;组件自己负责补齐内部结构)。
|
||
// ⚠️ 三点都很重要:
|
||
// 1. `SetClip(组件区域)`:组件内部的元素可能落在自己地盘之外(典型:滚出可视区的
|
||
// 网格 `lane.rect` 的 Y 为负数),不裁剪会把边框画到工具栏/标签栏上。
|
||
// 2. 每个组件收集前**显式**把层号设成它在组件表里的下标:不能依赖 `PushLayer()`
|
||
// 累加 —— 组件内部可能为了自己的浮层临时提层(`PlotView` 的浮动面板),
|
||
// 一旦它没有复位,后面所有组件的层号会被整体推高,遮挡关系全乱。
|
||
// 3. 层号必须与 `Render` 里给文本下发的层号同源(见 `ComponentLayer`)。
|
||
for (std::size_t index = 0; index < m_components.size(); ++index)
|
||
{
|
||
const ComponentEntry& entry = m_components[index];
|
||
m_debugBoxes.SetLayer(static_cast<int>(index) * kLayersPerComponent);
|
||
if (entry.component != nullptr)
|
||
{
|
||
m_debugBoxes.SetClip(entry.component->Area());
|
||
entry.component->BuildBoxes(m_debugBoxes);
|
||
}
|
||
}
|
||
|
||
// 不属于任何组件的两块:面板分隔条(由宿主直接填充色块)、菜单 / 拾色器(浮层)。
|
||
// ⚠️ 浮层**不设裁剪矩形**:菜单的二级子菜单会展开到菜单本体之外,设裁剪会把它裁掉;
|
||
// 它们自己会把本体登记为"遮挡矩形"(见 `PopupMenu::BuildBoxes`),
|
||
// 由绘制阶段把更低层的 box 从浮层区域里挖掉。
|
||
m_debugBoxes.SetClip(gd::RectF());
|
||
m_debugBoxes.SetLayer(OverlayLayer(0));
|
||
m_debugBoxes.Add(m_splitterRect, 0);
|
||
m_debugBoxes.SetLayer(OverlayLayer(1));
|
||
m_menu.BuildBoxes(m_debugBoxes);
|
||
m_debugBoxes.SetLayer(OverlayLayer(2));
|
||
m_colorPicker.BuildBoxes(m_debugBoxes);
|
||
|
||
UiDebugPaintBoxList(g, m_debugBoxes);
|
||
}
|
||
|
||
} // namespace jdi
|