Files
Just-DataInspector/Utilities/Inc/PopupMenu.hpp
T
DeepSeek V4.1 Flash dd48cdd2e1 feat(ui): 工具栏"选区"改名、菜单对勾右置与边距修正、选区自适应勾选、悬停/选区拖拽性能
1) 工具栏文案:"时间轴缩放"/"Y 轴缩放" -> "时间轴选区"/"Y 轴选区"
- Toolbar.cpp 按钮标签、Toolbar.hpp 枚举注释、Application 状态提示语同步更新

2) 网格右键菜单:对勾从左侧移到右侧,并修正菜单内各元素边距
- 文本列统一为 kPadH + kItemPad(5 + 8),三类条目(HEADER/NORMAL/EDIT)共用;
  原先左侧预留 22px 标记列,导致"网格 N"、"Y 轴最大值"、"选区 Y 轴自适应"等
  所有文本的左边距被整体推右(用户报告的"文本左边距过大")
- 对勾改画在条目右侧的 kMarkerColumn(18) 列内,文本宽度相应预留,不会与文字重叠
- EDIT 行输入框改为右对齐到条目内容区右边界,宽度由 NaturalEditFieldWidth() 统一计算,
  空间不足时为左侧说明文字保留自然宽度(避免被菜单宽度上限挤成省略号)
- 分隔线、色块行同样改用 kItemPad 内缩;kRightPad 已删除

3) "选区 Y 轴自适应"增加与"Y 轴自适应"一致的勾选标记
- Lane 新增 viewAutoRange / viewAutoMin / viewAutoMax,记录应用该操作时的视图时间区间
- 新增 PlotView::LaneRangeIsAutoToView():只有未被任何手动幅值改动且时间选区未变化时才为 true
- 所有手动改动路径(ApplyValueSelection/ScaleLaneRange/ShiftLaneRange/ApplyLaneValueText/
  EndEdit/ResetLaneRange/MoveSignalToLane)都会清除该状态;
  时间选区变化后 viewAutoMin/Max 与当前视图不符,勾选自动消失
- RefreshPlotMenuValues() 同步刷新该项状态

4) 修复大文件(VERY_BIG_DATA 1.77GB / 5 信号)下悬停与选区缩放的严重卡顿
- 根因:任何悬停态变化(左侧信号列表、网格内命中曲线提示)或选区拖拽都会触发整幅重绘,
  其中波形绘制约 80ms/帧,因此交互帧率只有 ~10fps
- 静态场景缓存从"仅拖拽信号/光标"推广为"只要场景键一致就复用",缓存区域由整个客户区
  收敛为仅绘图区:工具栏/标签栏/面板各自带悬停态必须每帧绘制(约 10.5ms),
  而真正昂贵的波形改为直接贴图(约 1ms)
- 场景键新增 contentHash(FNV-1a 折叠网格几何/幅值、每条信号的引用/可见性/
  颜色/透明度/线宽/样式/显示名与所属文件名):改样式、重命名、增删信号都会自动失效
- 随鼠标变化的内容全部移出缓存:新增 PaintHoverHighlight() 在贴图之后绘制
  悬停网格底色(半透明叠加)与悬停信号光晕(PaintSignalHalo());
  PaintLane 内只绘制拖拽中的信号光晕(key 中记录 haloFile/haloSignal)
- EnsureSceneCache 改为要求位图尺寸与当前区域严格相等(原 >= 复用会在窗口缩小后
  把旧像素画到绘图区之外)
- Application::Render 移除 reuseScene 分支,统一每帧绘制界面框架

实测(VERY_BIG_DATA 5 信号,Release):
- 整幅重绘 ~80ms -> 命中缓存贴图 ~1ms
- 悬停左侧信号列表:~95ms -> ~14ms/帧
- 悬停网格内信号(含光晕高亮):~95ms -> ~28~32ms/帧
- 时间轴 / Y 轴选区拖拽:~95~105ms -> ~19~22ms/帧
- 非拖拽静止帧 ~118ms 维持不变;空闲 CPU 0.00%

构建: make clean; make all 无警告无错误
2026-10-07 11:26:43 +08:00

351 lines
11 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 /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
};
/**
* @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 项的输入过滤规则。 */
};
/**
* @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 获取当前正在编辑的菜单项命令标识。
* @return 命令标识,未编辑时返回 0。
*/
int EditingItemId() const;
/**
* @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 计算 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();
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_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; /**< 最近一次排布使用的锚点纵坐标。 */
};
} // namespace jdi
#endif // __POPUP_MENU_HPP__