- 「调整」行三个按钮改为「系数」「镜像」「绝对值」(顺序即结算顺序:系数 → 绝对值 → 镜像)。 「系数」「镜像」点击后在按钮下沿展开二级面板(按下只登记、抬起命中同一按钮才展开, 与自定义颜色入口一致),「绝对值」仍是直接切换的按钮。 打开面板时主动清除样式区悬停态 —— 面板会接管鼠标事件,否则该按钮会一直保持"鼠标按住"般的高亮。 - 数据层:`SignalStyle::inverted` 拆成 `mirrorVertical`(Y=0 上下翻转)与 `mirrorHorizontal`(水平镜像 = 数据反序);新增 `Mirrored()`、 `SourceIndexFor`(水平镜像的遍历下标 → 源下标映射)、`TransformValueRange`(区间映射)。 `ApplyDrawTransform` 顺序固定为 系数 → 绝对值 → 垂直镜像;水平镜像在采样遍历层处理。 - 绘制链路:`BuildPolyline`(逐点描边与像素列包络两条路径、跨列边界插值、粗粒度分组层)、 派生预览曲线、光标取值读数(含 Δt)全部按同一变换取值;水平镜像时按源下标区间反序读取, 包络层仍复用粗粒度分组;折线缓存键、`SceneContentHash`、样式快照都带上两个镜像位。 - 系数二级面板:标题「系数调整」,标题栏最右端是一个可点击的「还原」自绘图标按钮 (悬停有高亮反馈,点击后系数重置为 1,面板保持打开);下面是一整行输入框 (不再显示「绘制系数」说明文字),数值至少显示两位小数、精度更高时按实际精度显示 (1 → 1.00、2.5 → 2.50、1.2345678 原样)。 - 镜像二级面板:两行带复选框的切换项「水平镜像」「垂直镜像」,点击后**面板保持打开** (`MenuItem::keepOpen`),勾选状态就地回写;任一项勾选时「镜像」按钮显示为激活态。 - Y 轴自适应修复:整段自适应(`TabConfig::ComputeLaneRange`)按区间映射(`TransformValueRange`) 计算,不再直接用原始 min/max;可见区间自适应(`AutoRangeLaneToView`)逐点套用绘制变换 (含水平镜像的源下标映射)。用户报告的"调整系数/镜像后自适应区域错误、仍按原始数据"由此修复。 - `PopupMenu`:新增 `MenuItemKind::HEADER_ACTION`(标题 + 右侧可点击图标,图标有悬停高亮)、 `MenuItem::keepOpen`(切换项点击后不关闭菜单)、`HeaderIconRect()`, 以及无说明文字的整行输入框(占满内容区)。 验证(离屏 harness,SW_HIDE、不动鼠标键盘屏幕): - [AL] 重写/扩充:变换顺序与区间映射(含水平镜像的源下标反序)、调整行三按钮的顺序/等宽/标签不裁切、 「系数」按住不弹出 / 移开取消 / 单击展开(标题 HEADER_ACTION、无标题输入框占满整行、初始 1.00、 两位小数与更高精度回显、提交后面板保持打开、折线缓存键携带系数)、 「还原」图标位置/悬停像素变化/点击后系数回到 1.00 且面板保持打开、 「镜像」面板两行复选框(初始未勾选、keepOpen、勾选后按钮激活、取消其一保留另一)、 打开面板后按钮不再停留高亮、系数/镜像按钮的激活填充与其它启用态切换按钮逐像素同色; `BuildPolyline` 顶点范围与变换后数据范围逐项对齐(默认/系数 2/绝对值/垂直镜像/三者叠加), 水平镜像用窄视窗逐顶点核对"取的是对面端点的采样"(8/8)+ 读数跟随; 以及 Y 轴自适应:整段/可见区间自适应在系数、绝对值、垂直镜像下均跟随变换后的曲线。 - 合计 594/594 通过;make clean && make all 零警告。
400 lines
13 KiB
C++
400 lines
13 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
|
|
{
|
|
|
|
/**
|
|
* @brief 弹出菜单的打开来源,用于把选择结果派发回正确的组件。
|
|
*/
|
|
enum class MenuScope : int
|
|
{
|
|
NONE = 0, /**< 无来源。 */
|
|
SIGNAL_PANEL = 1, /**< 信号面板。 */
|
|
TAB_BAR = 2, /**< 标签栏。 */
|
|
PLOT_VIEW = 3 /**< 波形绘图区。 */
|
|
};
|
|
|
|
/**
|
|
* @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 项的当前数值文本(非编辑态显示)。 */
|
|
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
|
|
std::vector<MenuItem> submenu; /**< 二级子菜单项(kind == SUBMENU 时有效)。 */
|
|
/**
|
|
* 普通项被点击后是否**保持菜单打开**。
|
|
*
|
|
* 用于「带复选框的切换项」(如镜像的水平/垂直):可以连续勾选多项,
|
|
* 勾选状态通过 `UpdateItemLabel` 回写。默认 false(点击即关):与既有行为一致。
|
|
*/
|
|
bool keepOpen = false;
|
|
};
|
|
|
|
/**
|
|
* @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 菜单项集合。
|
|
*/
|
|
void Open(MenuScope scope, const gd::RectF& bounds, int x, int y, const std::vector<MenuItem>& items);
|
|
|
|
/**
|
|
* @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 设置剪贴板所有者窗口句柄(供内联输入框复制/粘贴)。
|
|
* @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 新的值文本。
|
|
* @return 找到对应项并更新返回 true。
|
|
*/
|
|
bool UpdateItemValue(int id, const std::wstring& value);
|
|
|
|
/**
|
|
* @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);
|
|
|
|
private:
|
|
/**
|
|
* @brief 计算单个菜单项的纵向高度。
|
|
* @param [IN] item 菜单项。
|
|
* @return 高度(像素)。
|
|
*/
|
|
float ItemHeight(const MenuItem& item) const;
|
|
|
|
/**
|
|
* @brief 依据当前项目集合计算整体矩形与各项矩形。
|
|
* @param [IN] bounds 宿主客户区矩形。
|
|
* @param [IN] x 锚点横坐标。
|
|
* @param [IN] y 锚点纵坐标。
|
|
*/
|
|
void BuildLayout(const gd::RectF& bounds, int x, int y);
|
|
|
|
/**
|
|
* @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 计算 `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);
|
|
|
|
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; /**< 各项矩形。 */
|
|
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_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__
|