## 为什么
用户 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)。
576 lines
21 KiB
C++
576 lines
21 KiB
C++
#ifndef __POPUP_MENU_HPP__
|
||
#define __POPUP_MENU_HPP__
|
||
|
||
#include <windows.h>
|
||
#include <objidl.h>
|
||
#include <gdiplus.h>
|
||
|
||
#include <cstdint>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include "PaintUtil.hpp"
|
||
#include "TextEdit.hpp"
|
||
#include "Theme.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
class UiBoxTable;
|
||
|
||
/**
|
||
* @brief 弹出菜单的打开来源,用于把选择结果派发回正确的组件。
|
||
*/
|
||
enum class MenuScope : int
|
||
{
|
||
NONE = 0, /**< 无来源。 */
|
||
SIGNAL_PANEL = 1, /**< 信号面板。 */
|
||
TAB_BAR = 2, /**< 标签栏。 */
|
||
PLOT_VIEW = 3, /**< 波形绘图区。 */
|
||
APP_SETTINGS = 4 /**< 应用设置菜单(工具栏齿轮按钮):由宿主自己处理,不下发到组件。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 弹出菜单项的显示类型。
|
||
*/
|
||
enum class MenuItemKind : int
|
||
{
|
||
NORMAL = 0, /**< 普通可点击项。 */
|
||
SEPARATOR = 1, /**< 分隔线。 */
|
||
HEADER = 2, /**< 分组标题(不可点击)。 */
|
||
SWATCHES = 3, /**< 颜色色块行。 */
|
||
EDIT = 4, /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
|
||
SUBMENU = 5, /**< 带二级子菜单的项:鼠标悬停时向右展开。 */
|
||
HEADER_ACTION = 6 /**< 分组标题 + **右侧图标按钮**(如「还原」),图标可点击。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 单个弹出菜单项。
|
||
*/
|
||
struct MenuItem
|
||
{
|
||
MenuItemKind kind = MenuItemKind::NORMAL; /**< 项目类型。 */
|
||
std::wstring label; /**< 显示文本;EDIT 项为输入框左侧的说明文字。 */
|
||
int id = 0; /**< 命令标识。 */
|
||
bool enabled = true; /**< 是否可用。 */
|
||
bool checked = false; /**< 是否处于选中(单选)状态。 */
|
||
int swatchCount = 0; /**< 色块数量(仅色块行有效)。 */
|
||
std::uint32_t swatches[16] = {}; /**< 色块颜色。 */
|
||
int checkedSwatch = -1; /**< 被选中的色块下标。 */
|
||
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
|
||
/**
|
||
* EDIT 项**进入编辑时的初始文本**;为空则使用 `value`。
|
||
*
|
||
* 用于「非编辑态的显示文本」与「编辑态的数值」不一致的场合,例如时间轴偏移:
|
||
* 平时显示 `Δt:5s 60ms`,进入编辑后应显示以秒为单位的数值 `5.06`。
|
||
*/
|
||
std::wstring editValue;
|
||
/**
|
||
* EDIT 项**编辑态**在输入框最右侧提示的单位文本(如 `s`);为空则不提示。
|
||
*
|
||
* 非编辑态不显示(那时显示的是 `value` 的完整文案)。
|
||
*/
|
||
std::wstring editSuffix;
|
||
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
|
||
std::vector<MenuItem> submenu; /**< 二级子菜单项(kind == SUBMENU 时有效)。 */
|
||
/**
|
||
* 普通项被点击后是否**保持菜单打开**。
|
||
*
|
||
* 用于「带复选框的切换项」(如镜像的水平/垂直):可以连续勾选多项,
|
||
* 勾选状态通过 `UpdateItemLabel` 回写。默认 false(点击即关):与既有行为一致。
|
||
*/
|
||
bool keepOpen = false;
|
||
};
|
||
|
||
/**
|
||
* @brief 打开菜单时的附加布局约束。
|
||
*/
|
||
struct MenuLayoutConstraint
|
||
{
|
||
/**
|
||
* @brief 菜单允许的最大高度(**客户区像素**,0 = 不限制)。
|
||
*
|
||
* 超过该高度时菜单不再变高:**前导标题钉在顶部、最后一个分隔线及其之后的条目固定在底部**,
|
||
* 中间的内容成为可滚动的条目区(鼠标滚轮 / 键盘方向键 / 滚动条)。
|
||
*
|
||
* 用于「条目数不定」的菜单:例如加载含上百个信号的 MF4 并把每个信号都放进独立网格时,
|
||
* 「所属网格」菜单会有上百个条目(既超出屏幕、绘制开销也很大)。
|
||
*/
|
||
float maxHeight = 0.0f;
|
||
|
||
/**
|
||
* @brief 可滚动条目区**最多显示的标准行数**(0 = 不限制)。
|
||
*
|
||
* 比 `maxHeight` 更直观(用户要求“保留 10 条网格的上限”):无论条目多高,
|
||
* 滚动视口高度都不会超过该行数×标准行高。与 `maxHeight` 同时给出时**取更严格的那个**。
|
||
*/
|
||
int maxScrollRows = 0;
|
||
};
|
||
|
||
/**
|
||
* @brief 菜单选择结果。
|
||
*/
|
||
struct MenuResult
|
||
{
|
||
bool valid = false; /**< 是否产生了有效选择。 */
|
||
MenuScope scope = MenuScope::NONE; /**< 来源组件。 */
|
||
int id = 0; /**< 被选中项的命令标识。 */
|
||
int swatchIndex = -1; /**< 被选中的色块下标(非色块行为 -1)。 */
|
||
gd::RectF itemRect; /**< 被点击条目的矩形(客户区坐标),供就近弹出输入框。 */
|
||
bool editCommit = false; /**< 该结果来自菜单内联输入框的提交(菜单保持打开)。 */
|
||
std::wstring editText; /**< 提交的输入文本(editCommit 为 true 时有效)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 与界面主题一致的自绘弹出菜单。
|
||
*
|
||
* 菜单不创建独立窗口,而是作为宿主窗口绘制流程的最上层内容直接绘制到后备缓冲区,
|
||
* 因此外观与主界面完全统一,不会出现系统默认菜单样式。
|
||
*/
|
||
class PopupMenu
|
||
{
|
||
public:
|
||
PopupMenu();
|
||
|
||
/**
|
||
* @brief 绑定字体与主题资源。
|
||
* @param [IN] fonts 字体集合,需在菜单生命周期内保持有效。
|
||
* @param [IN] theme 主题配色,需在菜单生命周期内保持有效。
|
||
*/
|
||
void Setup(const UiFonts* fonts, const Theme* theme);
|
||
|
||
/**
|
||
* @brief 设置 DPI 缩放比例。
|
||
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
|
||
*/
|
||
void SetScale(float scale);
|
||
|
||
/**
|
||
* @brief 在指定位置打开菜单。
|
||
* @param [IN] scope 来源组件标识。
|
||
* @param [IN] bounds 宿主客户区矩形,用于把菜单限制在窗口内。
|
||
* @param [IN] x 锚点横坐标(客户区坐标)。
|
||
* @param [IN] y 锚点纵坐标(客户区坐标)。
|
||
* @param [IN] items 菜单项集合。
|
||
* @param [IN] expandUp 为 true 时菜单**向上展开**,且 `(x, y)` 是菜单的**左下角**
|
||
* (用于“二级面板贴在按钮左上方”这类锚定);为 false(默认)时向右下展开,
|
||
* `(x, y)` 是菜单的左上角。
|
||
* @param [IN] constraint 附加布局约束(最大高度 / 超长时可滚动),默认不限制。
|
||
*/
|
||
void Open(MenuScope scope, const gd::RectF& bounds, int x, int y,
|
||
const std::vector<MenuItem>& items, bool expandUp = false,
|
||
const MenuLayoutConstraint& constraint = MenuLayoutConstraint());
|
||
|
||
/**
|
||
* @brief 关闭菜单。
|
||
*/
|
||
void Close();
|
||
|
||
/**
|
||
* @brief 判断菜单是否处于打开状态。
|
||
* @return 打开返回 true。
|
||
*/
|
||
bool IsOpen() const { return m_open; }
|
||
|
||
/**
|
||
* @brief 获取当前菜单的来源组件标识。
|
||
* @return 来源标识。
|
||
*/
|
||
MenuScope Scope() const { return m_scope; }
|
||
|
||
/**
|
||
* @brief 绘制菜单(应在宿主绘制的最后一步调用)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void Paint(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 处理鼠标移动。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnMouseMove(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理鼠标左键按下。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @param [OUT] result 在菜单外按下时返回被取消的选择结果。
|
||
* @return 事件已被菜单消费返回 true。
|
||
*/
|
||
bool OnLButtonDown(int x, int y, MenuResult& result);
|
||
|
||
/**
|
||
* @brief 处理鼠标左键抬起。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @param [OUT] result 选择结果。
|
||
* @return 事件已被菜单消费返回 true。
|
||
*/
|
||
bool OnLButtonUp(int x, int y, MenuResult& result);
|
||
|
||
/**
|
||
* @brief 处理键盘按键。
|
||
* @param [IN] key 虚拟键码。
|
||
* @param [OUT] result 选择结果。
|
||
* @return 事件已被菜单消费返回 true。
|
||
*/
|
||
bool OnKeyDown(WPARAM key, MenuResult& result);
|
||
|
||
/**
|
||
* @brief 判断坐标是否落在菜单内部。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 落在菜单内返回 true。
|
||
*/
|
||
bool ContainsPoint(int x, int y) const;
|
||
|
||
/**
|
||
* @brief 菜单整体矩形(客户区坐标;未打开时为空矩形)。
|
||
*
|
||
* 只读用途:宿主的「显示布局边界」需要把浮层区域也上报出来。
|
||
*/
|
||
const gd::RectF& Rect() const { return m_rect; }
|
||
|
||
/**
|
||
* @brief 收集菜单与其内部结构的布局边界(调试用)。
|
||
*
|
||
* 菜单未打开时什么都不登记。条目、滚动区、滚动条与二级子菜单各自成为独立的 box。
|
||
* @param [IN,OUT] boxes 收集器。
|
||
*/
|
||
void BuildBoxes(UiBoxTable& boxes) const;
|
||
|
||
/**
|
||
* @brief 菜单是否因内容过长而启用了滚动(条目区可滚动、前导标题与尾部条目固定)。
|
||
* @return 启用滚动返回 true。
|
||
*/
|
||
bool IsScrollable() const { return m_open && m_scrollEnabled; }
|
||
|
||
/**
|
||
* @brief 滚动条**轨道**矩形(仅可滚动菜单有效,其余返回空矩形)。
|
||
*
|
||
* 同时也是“可滚动条目区”的依据:条目区内的条目只在该矩形内可点。
|
||
*/
|
||
const gd::RectF& ScrollBarRect() const { return m_scrollBarRect; }
|
||
|
||
/**
|
||
* @brief 滚动条**滑块**矩形(仅可滚动菜单有效)。
|
||
* @return 滑块矩形;无需滚动时返回空矩形。
|
||
*/
|
||
gd::RectF ScrollBarThumbRect() const;
|
||
|
||
/**
|
||
* @brief 由「滑块的顶部 Y」反推对应的滚动偏移(拖拽滚动条 / 点击轨道时用)。
|
||
* @param [IN] thumbTop 期望的滑块顶部 Y(客户区坐标)。
|
||
* @return 滚动偏移(已夹到合法范围)。
|
||
*/
|
||
float ScrollOffsetForThumbTop(float thumbTop) const;
|
||
|
||
/**
|
||
* @brief 条目区当前的滚动偏移(仅可滚动菜单有效)。
|
||
* @return 偏移量(像素,0 = 顶部)。
|
||
*/
|
||
float ScrollOffset() const { return m_scrollOffset; }
|
||
|
||
/**
|
||
* @brief 可滚动条目区在菜单条目中的下标范围(半开区间 `[first, last)`)。
|
||
*
|
||
* 前导标题(`HEADER` / `HEADER_ACTION`)与末尾分隔线及其之后的条目**不参与滚动**。
|
||
* @param [OUT] first 条目区首个条目下标。
|
||
* @param [OUT] last 条目区末尾(不含)下标。
|
||
* @return 菜单启用滚动返回 true;否则输出全区间。
|
||
*/
|
||
bool ScrollRegion(int& first, int& last) const;
|
||
|
||
/**
|
||
* @brief 处理鼠标滚轮:滚动可滚动菜单的条目区。
|
||
* @param [IN] delta 滚轮增量(`WM_MOUSEWHEEL` 的 `GET_WHEEL_DELTA_WPARAM`)。
|
||
* @return 滚动位置发生变化返回 true。
|
||
*/
|
||
bool OnMouseWheel(int delta);
|
||
|
||
/**
|
||
* @brief 设置剪贴板所有者窗口句柄(供内联输入框复制/粘贴)。
|
||
* @param [IN] host 宿主窗口句柄,允许为空。
|
||
*/
|
||
void SetHostWindow(HWND host) { m_host = host; }
|
||
|
||
/**
|
||
* @brief 判断是否有菜单项正处于内联编辑状态。
|
||
* @return 处于编辑状态返回 true。
|
||
*/
|
||
bool IsEditing() const { return m_editIndex >= 0; }
|
||
|
||
/**
|
||
* @brief 判断坐标是否落在当前正在编辑的输入框内。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 落在输入框内返回 true。
|
||
*/
|
||
bool IsOnEditField(int x, int y) const;
|
||
|
||
/**
|
||
* @brief 编辑态下按下左键:定位插入符并开始拖拽框选。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 命中输入框返回 true。
|
||
*/
|
||
bool OnEditMouseDown(int x, int y);
|
||
|
||
/**
|
||
* @brief 编辑态下拖拽鼠标:扩展选区。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @return 选区发生变化返回 true。
|
||
*/
|
||
bool OnEditMouseMove(int x);
|
||
|
||
/**
|
||
* @brief 编辑态下松开左键:结束拖拽框选。
|
||
* @return 之前处于拖拽中返回 true。
|
||
*/
|
||
bool OnEditMouseUp();
|
||
|
||
/**
|
||
* @brief 编辑态下处理字符输入。
|
||
* @param [IN] character 字符。
|
||
* @param [OUT] result 提交时返回 editCommit 结果。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnChar(wchar_t character, MenuResult& result);
|
||
|
||
/**
|
||
* @brief 更新指定命令项显示的值文本(菜单保持打开,立即重新排布)。
|
||
* @param [IN] id 命令标识。
|
||
* @param [IN] value 新的值文本(非编辑态显示)。
|
||
* @param [IN] editValue 新的**编辑初值**(为空时保持原值)。
|
||
* ⚠️ 非编辑态文本与编辑态文本不一致的行(如时间轴偏移:`Δt:1h 1min 2s` ↔ `3662`)
|
||
* 必须一并更新,否则下次进入编辑会拿到陈旧的初值。
|
||
* @return 找到对应项并更新返回 true。
|
||
*/
|
||
bool UpdateItemValue(int id, const std::wstring& value,
|
||
const std::wstring& editValue = std::wstring());
|
||
|
||
/**
|
||
* @brief 更新指定普通项的文字与可用/选中状态(用于实时反映状态)。
|
||
* @param [IN] id 命令标识。
|
||
* @param [IN] label 新的文字。
|
||
* @param [IN] checked 新的选中状态。
|
||
* @param [IN] enabled 新的可用状态。
|
||
* @return 找到对应项并更新返回 true。
|
||
*/
|
||
bool UpdateItemLabel(int id, const std::wstring& label, bool checked = false, bool enabled = true);
|
||
|
||
/**
|
||
* @brief 更新**二级子菜单**里某个普通项的文字与勾选状态。
|
||
*
|
||
* 子菜单内容是 `MenuItem::submenu` 的快照,改完必须重建子菜单布局才会生效
|
||
*(这正是「主题配色」能边点边看到 √ 挪位的原因)。
|
||
*
|
||
* @param [IN] id 命令标识。
|
||
* @param [IN] label 新的文字。
|
||
* @param [IN] checked 新的选中状态。
|
||
* @param [IN] enabled 新的可用状态。
|
||
* @return 找到对应项并更新返回 true。
|
||
*/
|
||
bool UpdateSubmenuItemLabel(int id, const std::wstring& label, bool checked = false,
|
||
bool enabled = true);
|
||
|
||
private:
|
||
/**
|
||
* @brief 计算单个菜单项的纵向高度。
|
||
* @param [IN] item 菜单项。
|
||
* @return 高度(像素)。
|
||
*/
|
||
float ItemHeight(const MenuItem& item) const;
|
||
|
||
/**
|
||
* @brief 依据当前项目集合计算整体矩形与各项矩形。
|
||
* @param [IN] bounds 宿主客户区矩形。
|
||
* @param [IN] x 锚点横坐标。
|
||
* @param [IN] y 锚点纵坐标。
|
||
* @param [IN] expandUp 为 true 时以 `(x, y)` 为**左下角**向上展开。
|
||
*/
|
||
void BuildLayout(const gd::RectF& bounds, int x, int y, bool expandUp);
|
||
|
||
/**
|
||
* @brief 查询坐标命中的菜单项下标。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @param [OUT] swatchIndex 命中的色块下标(非色块行为 -1)。
|
||
* @return 项目下标,未命中返回 -1。
|
||
*/
|
||
int ItemAtPoint(int x, int y, int& swatchIndex) const;
|
||
|
||
/**
|
||
* @brief 将悬停项移动到下一个可用项。
|
||
* @param [IN] step 步进(+1 向下,-1 向上)。
|
||
*/
|
||
void MoveHover(int step);
|
||
|
||
/**
|
||
* @brief 确认当前悬停项并生成结果。
|
||
* @param [OUT] result 选择结果。
|
||
* @return 生成了有效选择返回 true。
|
||
*/
|
||
bool CommitHover(MenuResult& result);
|
||
|
||
/**
|
||
* @brief 计算指定下标项的内联输入框矩形(EDIT 项)。
|
||
* @param [IN] index 项目下标。
|
||
* @return 输入框矩形,非法下标返回空矩形。
|
||
*/
|
||
gd::RectF EditFieldRect(int index) const;
|
||
|
||
/**
|
||
* @brief 计算 EDIT 项**编辑态单位后缀**所占的宽度(0 表示无后缀)。
|
||
*
|
||
* 排版、命中测试与绘制共用:输入框正文区会向右让出这么多宽度,
|
||
* 后缀文本绘制在它右侧(靠右对齐)。
|
||
*
|
||
* @param [IN] item 菜单项。
|
||
* @return 后缀宽度(像素)。
|
||
*/
|
||
float EditSuffixWidth(const MenuItem& item) const;
|
||
|
||
/**
|
||
* @brief 计算 `HEADER_ACTION` 项右侧**图标按钮**(如「还原」)的矩形。
|
||
*
|
||
* 图标紧贴该行右端(右边距与其它行一致、行内垂直居中),大小按缩放比例推导。
|
||
* 鼠标悬停时该图标会高亮,点击即下发该项的命令。
|
||
*
|
||
* @param [IN] index 项目下标。
|
||
* @return 图标矩形;非 `HEADER_ACTION` 项或非法下标返回空矩形。
|
||
*/
|
||
gd::RectF HeaderIconRect(int index) const;
|
||
|
||
/**
|
||
* @brief 计算 EDIT 项输入框的“自然宽度”(排版与绘制共用同一公式)。
|
||
* @param [IN] item 菜单项。
|
||
* @return 宽度(像素)。
|
||
*/
|
||
float NaturalEditFieldWidth(const MenuItem& item) const;
|
||
|
||
/**
|
||
* @brief 进入指定项的内联编辑状态。
|
||
* @param [IN] index 项目下标。
|
||
* @return 成功进入返回 true。
|
||
*/
|
||
bool BeginItemEdit(int index);
|
||
|
||
/**
|
||
* @brief 结束内联编辑。
|
||
* @param [IN] commit 是否提交(false 为放弃修改)。
|
||
* @param [OUT] result 提交时输出 editCommit 结果。
|
||
* @return 之前处于编辑状态返回 true。
|
||
*/
|
||
bool EndItemEdit(bool commit, MenuResult& result);
|
||
|
||
/**
|
||
* @brief 依据当前项集合重新排布(用于值文本变化后)。
|
||
*/
|
||
void RebuildLayout();
|
||
|
||
/**
|
||
* @brief 退出内联编辑状态(不产生任何结果)。
|
||
*/
|
||
void CloseEditState();
|
||
|
||
/**
|
||
* @brief 展开指定项的二级子菜单(项不存在或非 SUBMENU 时等价于关闭)。
|
||
* @param [IN] parentIndex 父项下标。
|
||
*/
|
||
void BuildSubLayout(int parentIndex);
|
||
|
||
/**
|
||
* @brief 关闭二级子菜单并清空其状态。
|
||
*/
|
||
void CloseSubmenu();
|
||
|
||
/**
|
||
* @brief 查询坐标命中的子菜单项下标。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 子菜单项下标,未命中返回 -1。
|
||
*/
|
||
int SubItemAtPoint(int x, int y) const;
|
||
|
||
/**
|
||
* @brief 绘制二级子菜单(含投影与全部条目)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintSubmenu(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 键盘移动悬停项后,把该项滚动到可滚动区的可视范围内。
|
||
* @param [IN] index 目标条目下标。
|
||
* @return 滚动偏移发生变化返回 true。
|
||
*/
|
||
bool EnsureItemVisible(int index);
|
||
|
||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||
|
||
bool m_open = false; /**< 是否处于打开状态。 */
|
||
MenuScope m_scope = MenuScope::NONE; /**< 来源组件标识。 */
|
||
std::vector<MenuItem> m_items; /**< 菜单项集合。 */
|
||
|
||
gd::RectF m_rect; /**< 菜单整体矩形。 */
|
||
std::vector<gd::RectF> m_itemRects; /**< 各项矩形。 */
|
||
/**
|
||
* 同一次排布中,**带说明文字的 EDIT 项**共用的输入框宽度(0 表示没有这类项)。
|
||
*
|
||
* 宽度取这些项里最宽的自然宽度,并在排布时按“不能挤掉说明文字 / 不超过内容区”夹好。
|
||
* 同一面板内的输入框因此一样宽、右边界对齐(用户要求「Y轴」与「时间轴」等宽对齐,
|
||
* 而不是各自按自己的文本长度自适应)。
|
||
*/
|
||
float m_editFieldWidth = 0.0f;
|
||
std::vector<std::vector<gd::RectF>> m_swatchRects; /**< 各项内的色块矩形。 */
|
||
|
||
int m_hoverIndex = -1; /**< 当前悬停项。 */
|
||
int m_hoverIconIndex = -1; /**< 当前悬停的 `HEADER_ACTION` 图标所在项下标。 */
|
||
int m_hoverSwatch = -1; /**< 当前悬停色块。 */
|
||
int m_pressedIndex = -1; /**< 按下时的项目下标。 */
|
||
int m_pressedSwatch = -1; /**< 按下时的色块下标。 */
|
||
|
||
HWND m_host = nullptr; /**< 剪贴板所有者窗口句柄。 */
|
||
TextEdit m_edit; /**< 内联输入框编辑模型。 */
|
||
int m_editIndex = -1; /**< 正在编辑的项目下标,-1 表示未编辑。 */
|
||
|
||
gd::RectF m_buildBounds; /**< 最近一次排布使用的宿主矩形。 */
|
||
int m_buildX = 0; /**< 最近一次排布使用的锚点横坐标。 */
|
||
int m_buildY = 0; /**< 最近一次排布使用的锚点纵坐标。 */
|
||
bool m_buildExpandUp = false; /**< 最近一次排布是否为“向上展开(锚点为左下角)”。 */
|
||
|
||
/** 最近一次使用的布局约束(`RebuildLayout` 复用)。 */
|
||
MenuLayoutConstraint m_constraint;
|
||
/**
|
||
* @brief 可滚动条目区的下标范围(半开区间 `[m_scrollFirst, m_pinnedIndex)`)。
|
||
*
|
||
* `m_scrollFirst` = 前导固定条目数(**「所属网格」这类标题必须始终留在最上方、不参与滚动**);
|
||
* `m_pinnedIndex` = 末尾固定区起点(最后一个分隔线,典型「新建网格」钉在底部)。
|
||
*/
|
||
int m_scrollFirst = 0;
|
||
int m_pinnedIndex = -1;
|
||
bool m_scrollEnabled = false; /**< 条目区是否可滚动(内容超过约束上限时启用)。 */
|
||
float m_scrollOffset = 0.0f; /**< 滚动区当前的滚动偏移(像素,0 = 顶部)。 */
|
||
float m_scrollRegionHeight = 0.0f; /**< 滚动区的内容总高(像素)。 */
|
||
gd::RectF m_scrollViewport; /**< 滚动区的可视矩形(也是滚动区条目的裁剪矩形)。 */
|
||
gd::RectF m_scrollBarRect; /**< 滚动条轨道矩形(仅可滚动菜单有效)。 */
|
||
bool m_scrollBarDragging = false; /**< 是否正在拖拽滚动条滑块。 */
|
||
float m_scrollBarGrabOffset = 0.0f; /**< 拖拽时鼠标相对滑块顶部的偏移(像素)。 */
|
||
|
||
bool m_subOpen = false; /**< 二级子菜单是否展开。 */
|
||
int m_subParentIndex = -1; /**< 展开子菜单的父项下标。 */
|
||
gd::RectF m_subRect; /**< 子菜单整体矩形。 */
|
||
std::vector<MenuItem> m_subItems; /**< 子菜单项集合。 */
|
||
std::vector<gd::RectF> m_subItemRects; /**< 子菜单各项矩形。 */
|
||
int m_subHoverIndex = -1; /**< 子菜单当前悬停项。 */
|
||
int m_subPressedIndex = -1; /**< 子菜单按下时的项下标。 */
|
||
};
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __POPUP_MENU_HPP__
|