原文件混杂了标签页、派生信号、菜单、工具栏、键盘鼠标、布局调试绘制六类职责,
是本项目最大的“大杂烩”单文件之一。按职责纯搬移(零逻辑改动):
- 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%,属结构性拆分不可避免的噪声)。
410 lines
15 KiB
C++
410 lines
15 KiB
C++
// Application 实现分片 —— **菜单**:绘图区右键菜单(含「移动到…」子菜单)、
|
|
// 菜单数值回写、主题切换、设置菜单,以及菜单结果的统一派发。
|
|
//
|
|
// 共用的常量与工具函数在 ApplicationInternal.hpp(命名空间 `appdetail`,全部 inline)。
|
|
// 本文件由 2026-11-04 代码审查从 `ApplicationCommands.cpp`(原 2,165 行)按职责拆出,
|
|
// **纯搬移、零逻辑改动**。分片划分见 `Documents/Code_Review_Plan.md` 的 P11。
|
|
#include "ApplicationInternal.hpp"
|
|
|
|
namespace jdi
|
|
{
|
|
|
|
void Application::ShowPlotContextMenu(int laneIndex, int x, int y)
|
|
{
|
|
if (laneIndex < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_plotMenuLane = laneIndex;
|
|
|
|
std::vector<MenuItem> items;
|
|
|
|
MenuItem header;
|
|
header.kind = MenuItemKind::HEADER;
|
|
header.label = L"网格 " + std::to_wstring(laneIndex + 1);
|
|
items.push_back(header);
|
|
|
|
// 当前网格的 Y 轴上下限值直接在菜单内的输入框里显示,单击即可就地编辑(菜单不关闭)。
|
|
double laneYMin = 0.0;
|
|
double laneYMax = 1.0;
|
|
m_plot.LaneRange(laneIndex, laneYMin, laneYMax);
|
|
|
|
MenuItem maxItem;
|
|
maxItem.kind = MenuItemKind::EDIT;
|
|
maxItem.label = L"Y 轴最大值";
|
|
maxItem.id = PLOT_CMD_LANE_Y_MAX;
|
|
maxItem.value = FormatValue(laneYMax);
|
|
maxItem.filter = EditFilter::NUMBER;
|
|
items.push_back(maxItem);
|
|
|
|
MenuItem minItem;
|
|
minItem.kind = MenuItemKind::EDIT;
|
|
minItem.label = L"Y 轴最小值";
|
|
minItem.id = PLOT_CMD_LANE_Y_MIN;
|
|
minItem.value = FormatValue(laneYMin);
|
|
minItem.filter = EditFilter::NUMBER;
|
|
items.push_back(minItem);
|
|
|
|
// 选区自适应(切换项):勾选后每次本网格的时间轴选区放大都会自动匹配 Y 轴幅值。
|
|
MenuItem autoViewItem;
|
|
autoViewItem.label = L"选区 Y 轴自适应";
|
|
autoViewItem.id = PLOT_CMD_LANE_AUTO_VIEW;
|
|
autoViewItem.checked = m_plot.LaneAutoFitToSelection(laneIndex);
|
|
items.push_back(autoViewItem);
|
|
|
|
// 时间轴自适应到本网格信号范围(原先位于分隔线下方,现移到分隔线上方)。
|
|
MenuItem fitLaneTime;
|
|
fitLaneTime.label = L"按网格时间轴展开";
|
|
fitLaneTime.id = PLOT_CMD_LANE_FIT_TIME;
|
|
items.push_back(fitLaneTime);
|
|
|
|
// 「拆分当前网格」:把该网格内的信号按个拆成独立网格,插到其下方依次排列。
|
|
// 可点击时在后方追加「(N个信号)」说明将拆出的网格数;只有 1 条信号时置灰且不加尾缀。
|
|
MenuItem splitLane;
|
|
const std::size_t laneSignalCount = m_plot.LaneSignalCount(laneIndex);
|
|
splitLane.label = (laneSignalCount > 1)
|
|
? (L"拆分当前网格(" + std::to_wstring(laneSignalCount) + L"个信号)")
|
|
: L"拆分当前网格";
|
|
splitLane.id = PLOT_CMD_LANE_SPLIT;
|
|
splitLane.enabled = laneSignalCount > 1;
|
|
items.push_back(splitLane);
|
|
|
|
// 实验性功能(二级菜单):横线上方,鼠标悬停即向右展开。
|
|
MenuItem experimental; experimental.kind = MenuItemKind::SUBMENU;
|
|
experimental.label = L"实验性功能";
|
|
{
|
|
MenuItem edgeCount;
|
|
edgeCount.label = L"边沿计数";
|
|
edgeCount.id = PLOT_CMD_LANE_EDGE_COUNT;
|
|
edgeCount.checked = m_plot.LaneEdgeCount(laneIndex);
|
|
experimental.submenu.push_back(edgeCount);
|
|
|
|
MenuItem edgeCenter;
|
|
edgeCenter.label = L"边沿计数(中心轴)";
|
|
edgeCenter.id = PLOT_CMD_LANE_EDGE_CENTER;
|
|
edgeCenter.checked = m_plot.LaneEdgeCenterCount(laneIndex);
|
|
experimental.submenu.push_back(edgeCenter);
|
|
|
|
MenuItem meanFilter;
|
|
meanFilter.label = L"均值滤波";
|
|
meanFilter.id = PLOT_CMD_LANE_MEAN_FILTER;
|
|
meanFilter.checked = m_plot.LaneMeanFilter(laneIndex);
|
|
experimental.submenu.push_back(meanFilter);
|
|
|
|
MenuItem weakPeak;
|
|
weakPeak.label = L"弱峰调整";
|
|
weakPeak.id = PLOT_CMD_LANE_WEAK_PEAK;
|
|
weakPeak.checked = m_plot.LaneWeakPeak(laneIndex);
|
|
experimental.submenu.push_back(weakPeak);
|
|
|
|
MenuItem freeMarker;
|
|
freeMarker.label = L"自由标记";
|
|
freeMarker.id = PLOT_CMD_LANE_FREE_MARKER;
|
|
freeMarker.checked = m_plot.LaneFreeMarker(laneIndex);
|
|
|
|
// 「重采样」位于「自由标记」**上方**(用户 2026-11-02 要求)。
|
|
MenuItem resample;
|
|
resample.label = L"重采样";
|
|
resample.id = PLOT_CMD_LANE_RESAMPLE;
|
|
resample.checked = m_plot.LaneResample(laneIndex);
|
|
experimental.submenu.push_back(resample);
|
|
|
|
// 「选区截取」位于「重采样」下方、「自由标记」上方(用户 2026-11-03 要求)。
|
|
MenuItem clip;
|
|
clip.label = L"选区截取";
|
|
clip.id = PLOT_CMD_LANE_CLIP;
|
|
clip.checked = m_plot.LaneClip(laneIndex);
|
|
experimental.submenu.push_back(clip);
|
|
|
|
experimental.submenu.push_back(freeMarker);
|
|
}
|
|
items.push_back(experimental);
|
|
|
|
MenuItem separator;
|
|
separator.kind = MenuItemKind::SEPARATOR;
|
|
items.push_back(separator);
|
|
|
|
// 全局开关:一次性打开/关闭全部网格各自的「选区 Y 轴自适应」。
|
|
MenuItem autoFitAllItem;
|
|
autoFitAllItem.label = L"选区 Y 轴自适应(全局)";
|
|
autoFitAllItem.id = PLOT_CMD_AUTO_FIT_ALL;
|
|
autoFitAllItem.checked = m_plot.AllLanesAutoFitToSelection();
|
|
items.push_back(autoFitAllItem);
|
|
|
|
MenuItem fitItem;
|
|
fitItem.label = L"全部自适应";
|
|
fitItem.id = PLOT_CMD_FIT_ALL;
|
|
items.push_back(fitItem);
|
|
|
|
RECT client = {};
|
|
GetClientRect(m_hwnd, &client);
|
|
const gd::RectF bounds(0.0f, 0.0f, static_cast<float>(client.right - client.left),
|
|
static_cast<float>(client.bottom - client.top));
|
|
|
|
m_menu.Open(MenuScope::PLOT_VIEW, bounds, x, y, items);
|
|
}
|
|
|
|
void Application::RefreshPlotMenuValues(int laneIndex)
|
|
{
|
|
if (laneIndex < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
double laneYMin = 0.0;
|
|
double laneYMax = 1.0;
|
|
m_plot.LaneRange(laneIndex, laneYMin, laneYMax);
|
|
m_menu.UpdateItemValue(PLOT_CMD_LANE_Y_MAX, FormatValue(laneYMax));
|
|
m_menu.UpdateItemValue(PLOT_CMD_LANE_Y_MIN, FormatValue(laneYMin));
|
|
|
|
// 「选区 Y 轴自适应」(本网格)与「(全局)」都是切换项,实时反映各自的勾选状态。
|
|
m_menu.UpdateItemLabel(PLOT_CMD_LANE_AUTO_VIEW, L"选区 Y 轴自适应",
|
|
m_plot.LaneAutoFitToSelection(laneIndex), true);
|
|
m_menu.UpdateItemLabel(PLOT_CMD_AUTO_FIT_ALL, L"选区 Y 轴自适应(全局)",
|
|
m_plot.AllLanesAutoFitToSelection(), true);
|
|
}
|
|
|
|
void Application::ApplyTheme(ThemeKind kind)
|
|
{
|
|
if (kind == m_themeKind && m_theme.windowBg == ThemeFor(kind).windowBg)
|
|
{
|
|
return; // 幂等:同一个主题重复点击不必作废缓存
|
|
}
|
|
m_themeKind = kind;
|
|
m_theme = ThemeFor(kind);
|
|
SetCurrentTheme(&m_theme);
|
|
|
|
// 主题色烘进了多处位图缓存,必须整体作废(只作废场景缓存是不够的:
|
|
// 宽基底图层、装饰层、浮动面板层、信号列表行位图里都存着旧配色的像素)。
|
|
m_plot.OnThemeChanged();
|
|
m_panel.OnThemeChanged();
|
|
|
|
// 装饰区(标签栏 / 工具栏 / 面板)全部重画,并整窗回送。
|
|
m_chromeDirty = true;
|
|
m_panelDirty = true;
|
|
InvalidateRect(m_hwnd, nullptr, TRUE);
|
|
}
|
|
|
|
void Application::OpenSettingsMenu()
|
|
{
|
|
// 锚点取齿轮按钮的**实际矩形**:按钮宽度由控件实测,宿主不应自己推算几何。
|
|
gd::RectF anchor;
|
|
if (!m_toolbar.ButtonRect(ToolbarAction::SETTINGS, anchor))
|
|
{
|
|
// 尚未布局(极端情形):退化为从菜单快捷键位置展开,而不是什么都不发生。
|
|
anchor = m_toolbarRect;
|
|
}
|
|
|
|
std::vector<MenuItem> items;
|
|
|
|
// ── 调试 ──────────────────────────────────────────────
|
|
MenuItem debugHeader;
|
|
debugHeader.kind = MenuItemKind::HEADER;
|
|
debugHeader.label = L"调试";
|
|
items.push_back(debugHeader);
|
|
|
|
MenuItem boundsItem;
|
|
boundsItem.label = L"显示布局边界";
|
|
boundsItem.id = kAppCmdShowLayoutBounds;
|
|
boundsItem.checked = m_showLayoutBounds;
|
|
// 切换项:点击后菜单保持打开,勾选态由宿主回写(与既有「镜像」等切换项一致)。
|
|
boundsItem.keepOpen = true;
|
|
items.push_back(boundsItem);
|
|
|
|
// ── 主题配色(二级菜单:鼠标悬停展开,带勾选态)──────────────
|
|
// 顺序固定为 Light / Solarised Light / Dark / Solarised Dark(用户 2026-10-29 指定)。
|
|
// `keepOpen` 让点击主题后菜单不关闭,勾选态由宿主回写 —— 便于连续预览几种配色。
|
|
MenuItem themeItem;
|
|
themeItem.kind = MenuItemKind::SUBMENU;
|
|
themeItem.label = L"主题配色";
|
|
for (int index = 0; index <= static_cast<int>(ThemeKind::SOLARIZED_DARK); ++index)
|
|
{
|
|
const ThemeKind kind = static_cast<ThemeKind>(index);
|
|
MenuItem option;
|
|
option.kind = MenuItemKind::NORMAL;
|
|
option.label = ThemeName(kind);
|
|
option.id = kAppCmdThemeBase + index;
|
|
option.checked = (m_themeKind == kind);
|
|
option.keepOpen = true;
|
|
themeItem.submenu.push_back(option);
|
|
}
|
|
items.push_back(themeItem);
|
|
|
|
RECT client = {};
|
|
GetClientRect(m_hwnd, &client);
|
|
const gd::RectF bounds(0.0f, 0.0f, static_cast<float>(client.right - client.left),
|
|
static_cast<float>(client.bottom - client.top));
|
|
|
|
// 右下展开,锚在按钮**下沿**(留 4 逻辑像素,视觉上不贴着按钮边框)。
|
|
const int x = static_cast<int>(anchor.X);
|
|
const int y = static_cast<int>(anchor.Y + anchor.Height + 4.0f * m_scale);
|
|
m_menu.Open(MenuScope::APP_SETTINGS, bounds, x, y, items);
|
|
}
|
|
|
|
void Application::DispatchMenuResult(const MenuResult& result)
|
|
{
|
|
if (!result.valid)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (result.scope)
|
|
{
|
|
case MenuScope::APP_SETTINGS:
|
|
{
|
|
// 应用设置菜单:全部条目都是**带勾选态的切换项**,菜单保持打开,
|
|
// 勾选变化通过 `UpdateItemLabel` 立即回写(连续切换多项时不用反复打开菜单)。
|
|
if (result.id == kAppCmdShowLayoutBounds)
|
|
{
|
|
m_showLayoutBounds = !m_showLayoutBounds;
|
|
m_menu.UpdateItemLabel(kAppCmdShowLayoutBounds, L"显示布局边界", m_showLayoutBounds);
|
|
// 边界比组件矩形外扩 1px 且跨越所有装饰区 ⇒ 必须整帧重绘。
|
|
m_chromeDirty = true;
|
|
}
|
|
else if (result.id >= kAppCmdThemeBase &&
|
|
result.id <= kAppCmdThemeBase + static_cast<int>(ThemeKind::SOLARIZED_DARK))
|
|
{
|
|
const ThemeKind kind = static_cast<ThemeKind>(result.id - kAppCmdThemeBase);
|
|
ApplyTheme(kind);
|
|
// 回写勾选态:四项里只有当前主题打勾(子菜单项要用专用接口才会重建布局)。
|
|
for (int index = 0; index <= static_cast<int>(ThemeKind::SOLARIZED_DARK); ++index)
|
|
{
|
|
const ThemeKind option = static_cast<ThemeKind>(index);
|
|
m_menu.UpdateSubmenuItemLabel(kAppCmdThemeBase + index, ThemeName(option),
|
|
m_themeKind == option);
|
|
}
|
|
}
|
|
InvalidateRect(m_hwnd, nullptr, TRUE);
|
|
break;
|
|
}
|
|
case MenuScope::SIGNAL_PANEL:
|
|
{
|
|
const bool needRelayout = m_panel.OnMenuCommand(result);
|
|
// 「公式映射」等就地输入可能产生**非模态**警告(不使用 Windows 消息框),
|
|
// 由绘图区底部的自绘提示浮层显示(与加载失败提示同一套)。
|
|
{
|
|
std::wstring hint;
|
|
if (m_panel.TakePendingHint(hint))
|
|
{
|
|
m_plot.ShowPersistentHint(hint);
|
|
}
|
|
}
|
|
if (needRelayout)
|
|
{
|
|
Relayout();
|
|
}
|
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
|
break;
|
|
}
|
|
case MenuScope::PLOT_VIEW:
|
|
{
|
|
// 菜单内联输入框的提交:就地更新网格 Y 轴上下限,菜单保持打开,
|
|
// 并把夹取后的真实值回写到菜单上(输入值可能被合法区间修正)。
|
|
if (result.editCommit)
|
|
{
|
|
const int laneIndex = m_plotMenuLane;
|
|
if (laneIndex >= 0)
|
|
{
|
|
m_plot.ApplyLaneValueText(laneIndex, result.id == PLOT_CMD_LANE_Y_MAX, result.editText);
|
|
RefreshPlotMenuValues(laneIndex);
|
|
}
|
|
SetFocus(m_hwnd);
|
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
|
break;
|
|
}
|
|
|
|
const int laneIndex = m_plotMenuLane;
|
|
m_plotMenuLane = -1;
|
|
switch (result.id)
|
|
{
|
|
case PLOT_CMD_LANE_AUTO_VIEW:
|
|
// 切换本网格的「选区 Y 轴自适应」。
|
|
m_plot.ToggleLaneAutoFitToSelection(laneIndex);
|
|
break;
|
|
case PLOT_CMD_AUTO_FIT_ALL:
|
|
{
|
|
// 全局切换:一次打开/关闭全部网格各自的开关;打开时立即生效一次。
|
|
const bool enable = !m_plot.AllLanesAutoFitToSelection();
|
|
m_plot.SetAllLanesAutoFitToSelection(enable, true);
|
|
break;
|
|
}
|
|
case PLOT_CMD_LANE_FIT_TIME:
|
|
// 「按网格时间轴展开」:展开时间轴后同时把该网格的 Y 轴自适应一次。
|
|
m_plot.FitTimeToLane(laneIndex);
|
|
break;
|
|
case PLOT_CMD_LANE_EDGE_COUNT:
|
|
// 「实验性功能 → 边沿计数」:切换本网格的横向光标与统计面板。
|
|
m_plot.ToggleLaneEdgeCount(laneIndex);
|
|
break;
|
|
case PLOT_CMD_LANE_EDGE_CENTER:
|
|
// 「实验性功能 → 边沿计数(中心轴)」:切换中心平均值曲线与统计面板。
|
|
m_plot.ToggleLaneEdgeCenterCount(laneIndex);
|
|
break;
|
|
case PLOT_CMD_LANE_MEAN_FILTER:
|
|
// 「实验性功能 → 均值滤波」:切换均值滤波曲线与参数面板。
|
|
m_plot.ToggleLaneMeanFilter(laneIndex);
|
|
break;
|
|
case PLOT_CMD_LANE_WEAK_PEAK:
|
|
// 「实验性功能 → 弱峰调整」:切换弱峰调整曲线与参数面板。
|
|
m_plot.ToggleLaneWeakPeak(laneIndex);
|
|
break;
|
|
case PLOT_CMD_LANE_FREE_MARKER:
|
|
// 「实验性功能 → 自由标记」:切换网格内跟随鼠标的纵向光标与固定数据点面板。
|
|
m_plot.ToggleLaneFreeMarker(laneIndex);
|
|
break;
|
|
case PLOT_CMD_LANE_RESAMPLE:
|
|
// 「实验性功能 → 重采样」:切换重采样参数面板。
|
|
m_plot.ToggleLaneResample(laneIndex);
|
|
break;
|
|
case PLOT_CMD_LANE_CLIP:
|
|
// 「实验性功能 → 选区截取」:切换选区截取面板。
|
|
m_plot.ToggleLaneClip(laneIndex);
|
|
break;
|
|
case PLOT_CMD_LANE_SPLIT:
|
|
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
|
|
m_plot.SplitLane(laneIndex);
|
|
break;
|
|
case PLOT_CMD_FIT_ALL:
|
|
m_plot.FitAll();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
SetFocus(m_hwnd);
|
|
InvalidateRect(m_hwnd, nullptr, TRUE);
|
|
break;
|
|
}
|
|
case MenuScope::TAB_BAR:
|
|
{
|
|
const int index = m_tabMenuIndex;
|
|
if (result.id == kMenuTabNew)
|
|
{
|
|
NewTab(true);
|
|
}
|
|
else if (result.id == kMenuTabClose)
|
|
{
|
|
CloseTab(index);
|
|
}
|
|
else if (result.id == kMenuTabCloseOthers)
|
|
{
|
|
if (index >= 0 && index < static_cast<int>(m_tabs.size()) && m_tabs.size() > 1)
|
|
{
|
|
const TabConfig keep = m_tabs[static_cast<std::size_t>(index)];
|
|
m_tabs.clear();
|
|
m_tabs.push_back(keep);
|
|
SetActiveTab(0);
|
|
}
|
|
}
|
|
m_tabMenuIndex = -1;
|
|
InvalidateRect(m_hwnd, nullptr, TRUE);
|
|
break;
|
|
}
|
|
case MenuScope::NONE:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
} // namespace jdi
|