新增主题配色(4 套)+ 信号右键菜单精简与「克隆该信号」+ 布局边界层级根治 + 单点信号吸附修复
## 为什么
用户 2026-10-29 报告/要求五件事:
1. 派生信号的「时间轴偏移保留在样式上、其它调整烘值」维持现状(确认无需改动);
2. 信号名右键菜单里很多功能已被下方样式自定义区覆盖 ⇒ 只保留「仅显示该信号」,
并在它**上方**新增「克隆该信号」;
3. 设置菜单新增「主题配色」二级菜单(带 √ 状态),顺序 Light / Solarised Light /
Dark / Solarised Dark,默认主题维持现状;
4. 布局边界:中键平移视界时浮动面板上的边界**闪烁**;光标取值胶囊的**文字**框会**穿透**
到浮动面板上;并请核对网格内固定构件(图例 / Y 轴标尺 / GX 标签 / 回到最上方 /
浮动面板)的层级关系;
5. 单数据点信号在拖动光标时的**吸附**功能消失。
## 根因
### 布局边界层级(两个都很隐蔽)
- **文本层号写死**:文本是在绘制过程中由 `UiTextRectSink` 收集的,而宿主把所有文本都记在
固定层 2;组件几何层却是"组件表下标"(`PlotView` = 0)。面板层 = PlotView 层 + 1 = 1,
**低于**文本层 2 ⇒ 面板的遮挡矩形挖不到文本 ⇒ 胶囊文字画在面板表面。
- **面板像素缓存**:面板"先画进图层、命中缓存就贴图",贴图那一帧不画字 ⇒ 文本框凭空消失
⇒ 面板上的边界闪烁。
- 顺带发现 `PlotView::BuildBoxes` 用 `PushLayer()` 却**从不复位**,会把宿主后续所有组件的
层号整体推高。
### 单点信号吸附
`MinorCellSparse` 在"把判定范围收窄到数据实际时间跨度"的分支里有
`if (!(dataMax > dataMin)) return false;`。单个有效采样点时 `dataMax == dataMin`,
于是被判定成"不稀疏" ⇒ 不吸附。
## 改了什么
### 1. 布局边界层级
- `UiTextRectSink` 新增 `OnTextLayer(int)` / `TextLayer()`;新增 `UiSetTextLayer` /
`UiGetTextLayer`;`UiBoxTable` 实现为改 `m_layer`,并暴露 `Layer()`。
- `Application::Render` 在绘制每个组件前下发 `UiSetTextLayer(ComponentLayer(&component))`,
浮层用 `OverlayLayer(1/2)`;新增 `ComponentLayer()` / `OverlayLayer()` /
`kLayersPerComponent = 3`,与 `PaintLayoutBounds` 的层号**完全同源**。
- `PaintLayoutBounds` 改为**显式** `SetLayer(下标 × 3)`(不再依赖 `PushLayer` 累加)。
- `PlotView`:`BuildBoxes` 用 `baseLayer + 1` 放浮动面板(并登记遮挡)、
`baseLayer + 2` 放绘制在面板之上的滚动条 / 回到最上方 / 输入框,结束**复位**层号;
`paintOverlays` 同步分两次抬高文本层。
- 面板像素图层新增 `PanelSlot::textRects` + 转发型收集器 `PanelTextCollector`:
渲染时存文本框,命中缓存贴图时回放 ⇒ 几何与缓存命中无关(消除闪烁)。
### 2. 信号右键菜单 + 克隆
- 菜单只剩:标题、**克隆该信号**、**仅显示该信号**;删除显示/隐藏、显示全部/隐藏全部、
预设色块行、样式三项,以及对应的 `CMD_SIGNAL_TOGGLE/SHOW_ALL/HIDE_ALL`、
`CMD_COLOR_BASE`、`CMD_MODE_BASE` 及其分支(死代码)。
- 新增 `CMD_SIGNAL_CLONE`(post `WM_APP + 5`)→ `Application::CloneSignal`:
把「调整」(系数 / 绝对值 / 镜像 / Y 偏移)烘进采样值、**时间轴偏移保留在样式上**,
名称前置 `*`,插入到源信号**正下方**,并**保留原外观**(颜色/透明度/线宽/绘制样式)。
- 抽出 `Application::InsertSignalBelowSource(...)` 供派生与克隆共用。
### 3. 主题配色
- `ThemeKind{LIGHT, SOLARIZED_LIGHT, DARK, SOLARIZED_DARK}` + `ThemeFor()` / `ThemeName()`;
Light 取 VS Code 2026 默认浅色、Dark 取 Dark+、Solarized Dark 取 VS Code solarized dark。
**默认仍是 Solarised Light**。
- 新增 `CurrentTheme()` / `SetCurrentTheme()`:宿主交出自己的 `m_theme` 地址,
切换主题只改内容;`UiControls` 里没有 `Theme` 形参的原语改用它(原来硬编码
`SolarizedLight()`,换主题不生效)。
- `Application::ApplyTheme()`:改配色 → `PlotView::OnThemeChanged()`(作废场景/宽基底/
装饰/光晕/面板图层)+ `SignalPanel::OnThemeChanged()`(作废行位图)→ 整窗重绘。
- 菜单用 `SUBMENU` + 4 个 `keepOpen` 子项;新增 `PopupMenu::UpdateSubmenuItemLabel()`
回写勾选,并让子菜单分支尊重 `keepOpen`(原为无条件关闭)。
### 4. 单点信号吸附
`MinorCellSparse` 改为只在 `dataMax < dataMin` 时放弃;相等时按"那一个小格"判定
(`need = 5 > finiteCount = 1` ⇒ 稀疏 ⇒ 吸附恢复)。
### 5. 审计
`Makefile` 的 `LAYOUT_BOUNDS` / `JDI_LAYOUT_BOUNDS` **从未被任何 `#if` 引用**
(`LAYOUT_BOUNDS=0` 产物字节数完全相同),属名实不符的死开关 ⇒ 删除并注明原因。
## 怎么验证
新增两支离屏探针(不占鼠标 / 键盘 / 屏幕):
```
.preflight/round15check 28 passed, 0 failed
单点吸附 7 条(含"远处不吸附")、面板层级 5 条、面板文本框跨帧与中键平移稳定 2 条、
克隆 14 条(烘值逐点比对 / 名称前缀 / 偏移与外观保留 / 调整复位)
.preflight/themecheck 17 passed, 0 failed
4 套主题可读性与可区分、默认 = Solarised Light、逐主题换色且无旧底色残留、
切回默认逐像素还原(diff = 0)、子菜单 4 项 / 顺序 / 唯一勾选 / 命令切换 / 菜单保持打开
```
回归全绿:
```
uicheck 69/69 lbcheck 21/21 lbresid 5/5 lbzorder 4/4
lbcovr 7/7 lbpanel 10/10 derivecheck 16/16
rendercheck: 8 个场景哈希与上一轮逐一相同 + FUNC 7/7, exit=0
A FAED59DF284153DB B E6443BFE4E3955F8 C 96A0AA8C402F1729 D E3D5DBD998E43720
E DA0583CB13C26A6F F FFB822159F604D1D G 19F8DE8FE3A8DF18 H DBBDDE4CB2C97C92
```
`make release` / `make debug` 零 error / 零 warning;产物 **791,552 B**
(单一 exe,未拆 DLL,未改 `-Oz`;离 1MB 预警线约 250 KB)。
This commit is contained in:
1 parent
71c6ff91cb
commit
71195c4c48
21 files changed
+907
-151
No files matched your search
@@ -323,6 +323,35 @@ namespace jdi
|
||||
*/
|
||||
bool BuildDerivedSignal(const DerivedSignalRequest& request, bool makeVisible);
|
||||
|
||||
/**
|
||||
* @brief 把一条信号插入到「源信号」的下方,并同步各标签页的引用与可见性。
|
||||
*
|
||||
* 派生(均值滤波 / 弱峰调整)与克隆共用这一段:插入 → 各标签页下标重映射 →
|
||||
* 在当前标签页勾选并把它所在的网格挪到源信号网格下方。**不做** `SyncTabsToDataSet`,
|
||||
* 好让调用方在同步之前再改新信号的外观。
|
||||
*
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @param [IN] sourceIndex 源信号在组内的下标。
|
||||
* @param [IN] signal 要插入的信号(移动语义)。
|
||||
* @param [IN] makeVisible 是否在当前标签页中勾选新信号。
|
||||
* @param [OUT] newIndex 新信号在组内的下标。
|
||||
* @return 插入成功返回 true。
|
||||
*/
|
||||
bool InsertSignalBelowSource(int groupIndex, int sourceIndex, SignalData&& signal,
|
||||
bool makeVisible, int& newIndex);
|
||||
|
||||
/**
|
||||
* @brief 克隆一条信号:把它的「调整」(系数 / 绝对值 / 镜像 / Y 偏移)烘焙进采样值,
|
||||
* 时间轴偏移保留在样式上,名称前置 `*`,插入到原信号下方。
|
||||
*
|
||||
* 烘值规则与「构建为新信号」完全一致,因此克隆曲线与原信号重合;区别只是
|
||||
* 克隆**保留原外观**(颜色 / 透明度 / 线宽 / 绘制样式),便于“冻住当前效果继续调”。
|
||||
*
|
||||
* @param [IN] sourceRef 源信号引用。
|
||||
* @return 克隆成功返回 true。
|
||||
*/
|
||||
bool CloneSignal(const SignalRef& sourceRef);
|
||||
|
||||
/**
|
||||
* @brief 弹出“打开数据文件”对话框并加载所选文件。
|
||||
*/
|
||||
@@ -396,6 +425,16 @@ namespace jdi
|
||||
*/
|
||||
void OpenSettingsMenu();
|
||||
|
||||
/**
|
||||
* @brief 切换主题配色,并作废全部**像素级**缓存后整窗重绘。
|
||||
*
|
||||
* 主题色被烘进了多处位图缓存(场景缓存、每网格宽基底图层、装饰层、浮动面板层、
|
||||
* 信号列表行位图),换主题后必须整体作废,否则界面会停留在旧配色上。
|
||||
*
|
||||
* @param [IN] kind 目标主题。
|
||||
*/
|
||||
void ApplyTheme(ThemeKind kind);
|
||||
|
||||
/**
|
||||
* @brief 绘制全部 UI 控件的布局边界(调试开关开启时在 Render 的最后一步调用)。
|
||||
*
|
||||
@@ -411,6 +450,43 @@ namespace jdi
|
||||
*/
|
||||
void PaintLayoutBounds(gd::Graphics& g, int width, int height);
|
||||
|
||||
/**
|
||||
* @brief 取某个组件的**绘制层号**(越小越靠下)。
|
||||
*
|
||||
* 层号 = 组件在 `m_components` 里的下标 × `kLayersPerComponent`,因此
|
||||
* “文本收集层”与 `PaintLayoutBounds` 里给几何 box 分配的层号**完全同源**。
|
||||
*
|
||||
* ⚠️ **为什么要乘以间隔而不是直接用下标**:组件内部可能还需要再压一层
|
||||
*(`PlotView` 的浮动面板就在自己之上再加一层)。若直接用下标,
|
||||
* 那个子层会与“下一个组件的基层”撞号,遮挡比较就失去意义。
|
||||
*
|
||||
* @param [IN] component 组件实例。
|
||||
* @return 层号;不在组件表里返回 0。
|
||||
*/
|
||||
int ComponentLayer(const UiComponent* component) const;
|
||||
|
||||
/**
|
||||
* @brief 每个组件占用的层号个数。
|
||||
*
|
||||
* * `+0`:组件自己的基层(网格 / 悬浮构件 / 光标读数…);
|
||||
* * `+1`:组件内部的不透明浮层(`PlotView` 的实验性功能浮动面板);
|
||||
* * `+2`:绘制在那些浮层**之上**的叠加层(滚动条 / 回到最上方 / 内联输入框)。
|
||||
*/
|
||||
static constexpr int kLayersPerComponent = 3;
|
||||
|
||||
/**
|
||||
* @brief 取**不属于组件表**的浮层 / 装饰的绘制层号。
|
||||
*
|
||||
* 它们接在组件表之后:`offset = 0` = 面板分隔条,`1` = 弹出菜单,`2` = 浮动拾色器。
|
||||
* @param [IN] offset 相对组件表末尾的偏移。
|
||||
* @return 层号。
|
||||
*/
|
||||
int OverlayLayer(int offset) const
|
||||
{
|
||||
return static_cast<int>(m_components.size()) * kLayersPerComponent +
|
||||
offset * kLayersPerComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理客户区鼠标移动。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
@@ -439,6 +515,13 @@ namespace jdi
|
||||
bool m_gdiplusReady = false; /**< GDI+ 是否初始化成功。 */
|
||||
|
||||
Theme m_theme; /**< 主题配色。 */
|
||||
/**
|
||||
* @brief 当前选中的主题种类(设置菜单 → 主题配色里的勾选项)。
|
||||
*
|
||||
* 默认值是 **Solarised Light**:这是项目历史默认,用户 2026-10-29 明确要求
|
||||
* 新增其它主题时"默认的主题色维持现状"。
|
||||
*/
|
||||
ThemeKind m_themeKind = ThemeKind::SOLARIZED_LIGHT;
|
||||
UiFonts m_fonts; /**< 界面字体。 */
|
||||
PopupMenu m_menu; /**< 与主题一致的自绘弹出菜单。 */
|
||||
ColorPickerPopup m_colorPicker; /**< 与主题一致的浮动拾色器。 */
|
||||
|
||||
@@ -63,6 +63,7 @@ namespace jdi
|
||||
inline const int kMinClientHeight = 540; /**< 窗口最小客户区高度。 */
|
||||
inline const UINT kMessageRemoveGroup = WM_APP + 1; /**< 面板请求移除分组的自定义消息。 */
|
||||
inline const UINT kMessageRemoveSignal = WM_APP + 2; /**< 面板请求移除单个信号的自定义消息。 */
|
||||
inline const UINT kMessageCloneSignal = WM_APP + 5; /**< 面板请求克隆单个信号的自定义消息。 */
|
||||
inline const UINT_PTR kHintTimerId = 1; /**< 提示浮层淡出用的定时器 ID。 */
|
||||
inline const UINT kHintTimerPeriodMs = 33; /**< 提示浮层淡出的重绘间隔(约 30fps)。 */
|
||||
inline const UINT_PTR kRamTimerId = 2; /**< 标题栏内存占用刷新的定时器 ID。 */
|
||||
@@ -179,6 +180,13 @@ namespace jdi
|
||||
|
||||
/** @brief 应用设置菜单(工具栏齿轮)命令 ID。 */
|
||||
inline const int kAppCmdShowLayoutBounds = 1;
|
||||
/**
|
||||
* @brief 「主题配色」二级菜单的命令 ID 基准。
|
||||
*
|
||||
* 第 i 个主题的 ID = `kAppCmdThemeBase + i`,`i` 即 `ThemeKind` 的整型值
|
||||
* (顺序就是菜单里的显示顺序:Light / Solarised Light / Dark / Solarised Dark)。
|
||||
*/
|
||||
inline const int kAppCmdThemeBase = 100;
|
||||
|
||||
/**
|
||||
* @brief 从应用资源中按指定尺寸加载图标。
|
||||
|
||||
@@ -74,7 +74,11 @@ namespace jdi
|
||||
}
|
||||
m_gdiplusReady = true;
|
||||
|
||||
m_theme = SolarizedLight();
|
||||
m_theme = ThemeFor(m_themeKind);
|
||||
// ⚠️ 把**宿主自己那个** `Theme` 实例的地址交给全局指针:这样切换主题只需改
|
||||
// `m_theme` 的内容,所有持有 `const Theme*` 的组件会自动看到新配色,
|
||||
// 而 `UiControls` 里那些没有 `Theme` 形参的原语也能跟随(见 `CurrentTheme`)。
|
||||
SetCurrentTheme(&m_theme);
|
||||
if (!m_fonts.Create(m_dpi))
|
||||
{
|
||||
m_gdiplusReady = false;
|
||||
@@ -535,7 +539,6 @@ namespace jdi
|
||||
if (m_showLayoutBounds)
|
||||
{
|
||||
m_debugBoxes.Clear();
|
||||
m_debugBoxes.SetLayer(2); // 基础内容层(工具栏/面板/绘图区)
|
||||
UiSetTextRectSink(&m_debugBoxes);
|
||||
}
|
||||
else if (!m_debugBoxes.Empty())
|
||||
@@ -559,34 +562,37 @@ namespace jdi
|
||||
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
|
||||
m_theme.windowBg);
|
||||
|
||||
// ⚠️ 文本必须记到**与它所在组件相同的层**:
|
||||
// `UiDebugPaintBoxList` 按 `(layer, depth)` 排序并拿更高的层当遮挡矩形,
|
||||
// 若把所有文本都固定记到某一层,就会出现“胶囊文字框穿透浮动面板”
|
||||
//(文本层比面板层低 ⇒ 被挖洞,或被当成上层直接画在面板上)。
|
||||
// 这里逐组件下发,`ComponentLayer` 与 `PaintLayoutBounds` 里的层号完全同源。
|
||||
UiSetTextLayer(ComponentLayer(&m_tabBar));
|
||||
m_tabBar.Paint(g);
|
||||
UiSetTextLayer(ComponentLayer(&m_toolbar));
|
||||
m_toolbar.Paint(g);
|
||||
UiSetTextLayer(ComponentLayer(&m_panel));
|
||||
m_panel.Paint(g);
|
||||
UiFillRect(g, m_splitterRect, m_theme.border);
|
||||
}
|
||||
else if (panelOnly)
|
||||
{
|
||||
// 只有面板像素变了 ⇒ 只重画面板与分隔条(`SignalPanel::Paint` 会铺满自己的区域)。
|
||||
UiSetTextLayer(ComponentLayer(&m_panel));
|
||||
m_panel.Paint(g);
|
||||
UiFillRect(g, m_splitterRect, m_theme.border);
|
||||
}
|
||||
|
||||
UiSetTextLayer(ComponentLayer(&m_plot));
|
||||
m_plot.Paint(g);
|
||||
|
||||
// 弹出菜单与浮动拾色器始终绘制在最上层,保证与主界面风格一致。
|
||||
// ⚠️ 调试:它们是不透明浮层,文本要记入更高的绘制层,
|
||||
// 否则会被自己登记到遮挡矩形里的"挖洞"逻辑裁掉。
|
||||
if (m_showLayoutBounds)
|
||||
{
|
||||
m_debugBoxes.SetLayer(5);
|
||||
}
|
||||
UiSetTextLayer(OverlayLayer(1));
|
||||
m_menu.Paint(g);
|
||||
if (m_showLayoutBounds)
|
||||
{
|
||||
m_debugBoxes.SetLayer(6);
|
||||
}
|
||||
UiSetTextLayer(OverlayLayer(2));
|
||||
m_colorPicker.Paint(g);
|
||||
|
||||
// 加载遮罩层在一切内容之上(包括菜单与拾色器),形成模态观感。
|
||||
PaintLoadingOverlay(g, width, height);
|
||||
|
||||
@@ -1694,6 +1700,15 @@ namespace jdi
|
||||
RemoveSignal(static_cast<int>(wParam), static_cast<int>(static_cast<LONG_PTR>(lParam)));
|
||||
return 0;
|
||||
}
|
||||
case kMessageCloneSignal:
|
||||
{
|
||||
// wParam = 分组下标,lParam = 组内信号下标。
|
||||
SignalRef ref;
|
||||
ref.groupIndex = static_cast<int>(wParam);
|
||||
ref.signalIndex = static_cast<int>(static_cast<LONG_PTR>(lParam));
|
||||
CloneSignal(ref);
|
||||
return 0;
|
||||
}
|
||||
case WM_TIMER:
|
||||
{
|
||||
// 标题栏内存占用:只改窗口标题,不动客户区,因此无需重绘。
|
||||
|
||||
@@ -740,17 +740,11 @@ namespace jdi
|
||||
|
||||
// 3. 插入到原信号的下方(数据层 + 各标签页引用重映射)。
|
||||
int newIndex = -1;
|
||||
// 插入派生信号会移动该分组的信号存储:先暂停后台计算,避免其读取已移动的数据。
|
||||
m_plot.PauseDerivedAsync();
|
||||
if (!m_dataSet.InsertDerivedSignal(request.groupIndex, request.signalIndex, std::move(derived),
|
||||
newIndex))
|
||||
if (!InsertSignalBelowSource(request.groupIndex, request.signalIndex, std::move(derived),
|
||||
makeVisible, newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (TabConfig& tab : m_tabs)
|
||||
{
|
||||
tab.RemapAfterSignalInsert(request.groupIndex, newIndex);
|
||||
}
|
||||
|
||||
// 3b. 时间轴偏移:**只**写进新信号的样式(不再烘进采样值,见 2c 的说明)。
|
||||
// 绘制路径本来就把采样画在 `t + offsetTime` 处,因此新信号的曲线与预览曲线
|
||||
@@ -763,22 +757,110 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 在当前标签页中勾选新信号,并把它所在的网格移到原信号网格的下方。
|
||||
if (makeVisible && !m_tabs.empty())
|
||||
{
|
||||
SignalRef derivedRef;
|
||||
derivedRef.groupIndex = request.groupIndex;
|
||||
derivedRef.signalIndex = newIndex;
|
||||
ActiveTab().SetVisible(derivedRef, true);
|
||||
// 4. 数据层结构变化:刷新列表、作废折线/派生缓存并重绘。
|
||||
SyncTabsToDataSet();
|
||||
return true;
|
||||
}
|
||||
|
||||
SignalRef sourceRef;
|
||||
sourceRef.groupIndex = request.groupIndex;
|
||||
sourceRef.signalIndex = request.signalIndex;
|
||||
ActiveTab().MoveLaneOfSignalBelow(derivedRef, sourceRef);
|
||||
bool Application::InsertSignalBelowSource(int groupIndex, int sourceIndex, SignalData&& signal,
|
||||
bool makeVisible, int& newIndex)
|
||||
{
|
||||
newIndex = -1;
|
||||
// 插入派生信号会移动该分组的信号存储:先暂停后台计算,避免其读取已移动的数据。
|
||||
m_plot.PauseDerivedAsync();
|
||||
if (!m_dataSet.InsertDerivedSignal(groupIndex, sourceIndex, std::move(signal), newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (TabConfig& tab : m_tabs)
|
||||
{
|
||||
tab.RemapAfterSignalInsert(groupIndex, newIndex);
|
||||
}
|
||||
|
||||
// 在当前标签页中勾选新信号,并把它所在的网格移到原信号网格的下方。
|
||||
if (makeVisible && !m_tabs.empty())
|
||||
{
|
||||
const SignalRef insertedRef{groupIndex, newIndex};
|
||||
ActiveTab().SetVisible(insertedRef, true);
|
||||
const SignalRef sourceRef{groupIndex, sourceIndex};
|
||||
ActiveTab().MoveLaneOfSignalBelow(insertedRef, sourceRef);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::CloneSignal(const SignalRef& sourceRef)
|
||||
{
|
||||
const SignalGroup* group = m_dataSet.GroupAt(sourceRef.groupIndex);
|
||||
if (group == nullptr || sourceRef.signalIndex < 0 ||
|
||||
sourceRef.signalIndex >= static_cast<int>(group->signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(sourceRef.signalIndex)];
|
||||
|
||||
// 源信号的「调整」快照(插入会移动信号存储,之后不能再解引用 `source`)。
|
||||
SignalStyle transform;
|
||||
bool hasStyle = false;
|
||||
if (const SignalStyle* style = m_dataSet.Style(sourceRef))
|
||||
{
|
||||
transform = *style;
|
||||
hasStyle = true;
|
||||
}
|
||||
|
||||
// 克隆 = 把「调整」烘进采样值(**时间轴偏移除外**,它留在样式上)。
|
||||
// 与「构建为新信号」完全同一套烘值规则,因此克隆出来的曲线与原信号**逐像素重合**,
|
||||
// 但它的「调整」面板回到默认值,可以继续在此基础上继续叠加新调整。
|
||||
SignalData clone;
|
||||
clone.name = L"*" + source.name;
|
||||
clone.axisIndex = source.axisIndex;
|
||||
clone.values = source.values;
|
||||
if (hasStyle)
|
||||
{
|
||||
const bool valueTransform = (transform.gain != 1.0f || transform.absolute ||
|
||||
transform.mirrorVertical || transform.offsetY != 0.0f);
|
||||
if (valueTransform)
|
||||
{
|
||||
for (double& value : clone.values)
|
||||
{
|
||||
value = ApplyDrawTransform(transform, value);
|
||||
}
|
||||
}
|
||||
if (transform.mirrorHorizontal)
|
||||
{
|
||||
std::reverse(clone.values.begin(), clone.values.end());
|
||||
}
|
||||
}
|
||||
|
||||
int newIndex = -1;
|
||||
if (!InsertSignalBelowSource(sourceRef.groupIndex, sourceRef.signalIndex, std::move(clone),
|
||||
true, newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 外观:克隆要**看起来与原信号一样**(颜色 / 透明度 / 线宽 / 绘制样式),
|
||||
// 只把被烘掉的「调整」复位为默认值,并保留时间轴偏移。
|
||||
if (SignalStyle* cloned = m_dataSet.Style(SignalRef{sourceRef.groupIndex, newIndex}))
|
||||
{
|
||||
if (hasStyle)
|
||||
{
|
||||
cloned->color = transform.color;
|
||||
cloned->customColor = transform.customColor;
|
||||
cloned->opacity = transform.opacity;
|
||||
cloned->lineWidth = transform.lineWidth;
|
||||
cloned->mode = transform.mode;
|
||||
cloned->offsetTime = transform.offsetTime;
|
||||
}
|
||||
// ⚠️ 单点信号默认会改成「散点 + 最大线宽」(`InsertDerivedSignal` 里的规则),
|
||||
// 克隆应该用**原信号的样式**,因此上面几行把它覆盖回去了。
|
||||
}
|
||||
|
||||
// 5. 数据层结构变化:刷新列表、作废折线/派生缓存并重绘。
|
||||
SyncTabsToDataSet();
|
||||
SetStatusMessage(L"已克隆信号:" + m_dataSet.GroupAt(sourceRef.groupIndex)
|
||||
->signals[static_cast<std::size_t>(newIndex)]
|
||||
.name,
|
||||
false);
|
||||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1080,6 +1162,27 @@ namespace jdi
|
||||
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()
|
||||
{
|
||||
// 锚点取齿轮按钮的**实际矩形**:按钮宽度由控件实测,宿主不应自己推算几何。
|
||||
@@ -1106,6 +1209,25 @@ namespace jdi
|
||||
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),
|
||||
@@ -1117,10 +1239,30 @@ namespace jdi
|
||||
m_menu.Open(MenuScope::APP_SETTINGS, bounds, x, y, items);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 客户区外框:便于确认"窗口边缘 == 最外层边界"。
|
||||
@@ -1128,19 +1270,22 @@ namespace jdi
|
||||
static_cast<float>(height)), 0);
|
||||
|
||||
// 组件区域(底 → 顶;组件自己负责补齐内部结构)。
|
||||
// ⚠️ 两点都很重要:
|
||||
// ⚠️ 三点都很重要:
|
||||
// 1. `SetClip(组件区域)`:组件内部的元素可能落在自己地盘之外(典型:滚出可视区的
|
||||
// 网格 `lane.rect` 的 Y 为负数),不裁剪会把边框画到工具栏/标签栏上。
|
||||
// 2. 每收集完一个组件就 `PushLayer()`:浮层压在本体之上(右键二级菜单压在行上面),
|
||||
// 只有按收集顺序分层,后收集的内容才画在上面。
|
||||
for (const ComponentEntry& entry : m_components)
|
||||
// 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);
|
||||
}
|
||||
m_debugBoxes.PushLayer();
|
||||
}
|
||||
|
||||
// 不属于任何组件的两块:面板分隔条(由宿主直接填充色块)、菜单 / 拾色器(浮层)。
|
||||
@@ -1148,10 +1293,11 @@ namespace jdi
|
||||
// 它们自己会把本体登记为"遮挡矩形"(见 `PopupMenu::BuildBoxes`),
|
||||
// 由绘制阶段把更低层的 box 从浮层区域里挖掉。
|
||||
m_debugBoxes.SetClip(gd::RectF());
|
||||
m_debugBoxes.SetLayer(OverlayLayer(0));
|
||||
m_debugBoxes.Add(m_splitterRect, 0);
|
||||
m_debugBoxes.PushLayer();
|
||||
m_debugBoxes.SetLayer(OverlayLayer(1));
|
||||
m_menu.BuildBoxes(m_debugBoxes);
|
||||
m_debugBoxes.PushLayer();
|
||||
m_debugBoxes.SetLayer(OverlayLayer(2));
|
||||
m_colorPicker.BuildBoxes(m_debugBoxes);
|
||||
|
||||
UiDebugPaintBoxList(g, m_debugBoxes);
|
||||
@@ -1177,6 +1323,19 @@ namespace jdi
|
||||
// 边界比组件矩形外扩 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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user