Files
Just-DataInspector/Components/Toolbar/Inc/Toolbar.hpp
T
DeepSeek V4.1 Flash ab19a6e72e feat(UI): 显示布局边界(Android 风格)+ 工具栏设置菜单(齿轮)+ 三处可见收益优化
## 1. 新功能:齿轮按钮 -> 应用设置菜单 -> 显示布局边界

- ToolbarAction::SETTINGS 位于「导入文件」左侧;工具栏布局改为
  「左侧固定簇(kLeftClusterCount = 2:齿轮 + 导入文件)+ 右侧按钮组」。
- 齿轮是矢量化绘制(8 齿 + 外环 + 轴孔,全部按 half 的比例推导),
  不用字体字符「⚙」——该字符在不同回退字体里大小与基线不一致,会忽大忽小、垂直不居中。
  UiButtonWidget 新增 UiButtonIcon(NONE / GEAR)与 SetIcon。
- MenuScope::APP_SETTINGS(新增)+ 设置菜单:HEADER「调试」+ 勾选切换项「显示布局边界」
  (keepOpen:点击后菜单保持打开,勾选态由 UpdateItemLabel 回写)。
  锚点取 Toolbar::ButtonRect(SETTINGS) 的控件实测矩形,宿主不再自己推算按钮几何。

## 2. 布局边界:基类给协议,组件自己上报

- UiComponent 新增 virtual PaintDebugBounds(g, scale),默认只画 Area()。
- Toolbar / TabBar 覆盖:跳过与区域重合的根控件,遍历 m_root->Children()(层级 1);
  TabBar 另上报每个 ✕(层级 2)。
- SignalPanel 覆盖:表头 / 筛选框 / 列表 / 样式区(1)+ 滚动条、样式行、当前可见列表行(2)。
- PlotView 覆盖:每个 LaneVisual::rect(1);不上报随数据变化的瞬态几何(曲线/胶囊/提示)。
- UiWidget 新增 PaintDebugBounds(g, scale, depth) 递归整棵控件树 —— 已上树的控件零成本获得边界。
- 宿主 PaintLayoutBounds 补客户区外框、分隔条、打开的菜单/拾色器。

视觉与实现约束(Android「Show layout bounds」风):
- 只描边、不填充(几十个嵌套矩形叠加填充会把内容糊住);
- 层级配色 青蓝(2px) / 品红 / 琥珀 / 绿(1px);
- 线宽按设备像素固定(不乘 scale),临时 PixelOffsetModeNone + 关抗锯齿 + 整像素对齐,
  保证 1px 就是 1px;
- ⚠️ 只在 Render 最后一步叠加,绝不进入任何位图缓存(PlotView 4 层缓存 / SignalPanel 行位图缓存),
  否则关掉开关后线条会残留;
- ⚠️ fullFrame 并入 m_showLayoutBounds:开启时必须整帧回送,否则「只回送绘图区」的快路径
  会让装饰区边界停在旧帧上。

## 3. 可见收益优化(行为不变)

- Toolbar::Layout:控件树改为按需重建(m_treeDirty),只在规格/字体/主题/缩放/动态文本
  变化时重建。拖动面板分隔条时 Relayout 每帧调用一次 -> 不再每帧 10 次堆分配 + 8 次测量缓存失效。
- Toolbar::SetActionLabel:只写 m_labelOverrides(唯一权威)+ 置树脏,消除「覆盖表与控件文本
  双写」可能不同步的问题。
- UiWidget::DispatchRedraw(event):新增;收敛 Toolbar / TabBar 里重复的
  Dispatch(); return UiRectHasArea(TakeDirty());(TakeDirty 会清零,漏一次就把下一次真实变化
  误判成「无变化」)。

## 4. 验证

- make release / make debug:零 error 零 warning;TargetCheck.exe 897,024 B(< 900 KB)。
- 新增离屏探针 .preflight/lbcheck:21 / 21 PASS
  (齿轮在导入文件左侧/同高/更窄;开启后青蓝 24,930 px、品红 19,734 px;关闭后逐像素还原;
   菜单 scope、锚定贴合按钮、宽度未被撑大、条目初始未勾选、点击翻转开关、菜单保持打开)。
- Tools/rendercheck:8 个场景的「工具栏带以下区域」哈希与改造前**逐一相同**
  (证明改动像素被完全限制在工具栏带内);整帧哈希因新增按钮整体变化,属预期。
  FUNC 断言 7 / 7 PASS。
- Tools/uicheck:69 passed / 0 failed。

## 5. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第六阶段;
Documents/agent_mem.md 同步变更日志与「2026-10-28 第八轮」硬约束与验证坑。
2026-10-28 15:46:45 +08:00

213 lines
7.4 KiB
C++

#ifndef __TOOLBAR_HPP__
#define __TOOLBAR_HPP__
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "PaintUtil.hpp"
#include "Theme.hpp"
#include "UiButtonWidget.hpp"
#include "UiComponent.hpp"
#include "UiWidget.hpp"
namespace jdi
{
/**
* @brief 工具栏按钮触发的动作。
*/
enum class ToolbarAction : int
{
NONE = 0, /**< 未触发任何动作。 */
SETTINGS, /**< **展开应用设置菜单**(齿轮图标,位于「导入文件」左侧)。 */
OPEN_FILE, /**< 打开数据文件。 */
FIT_ALL, /**< 视图自适应到可见信号范围(**不**退出选区放大模式)。 */
AUTO_FIT_Y, /**< 切换「选区 Y 轴自适应」全局开关:时间轴选区放大后自动匹配各网格 Y 轴。 */
ZOOM_SELECT, /**< 切换「选区放大」模式(横向拖拽放大时间轴,纵向拖拽放大该网格幅值)。 */
CURSOR, /**< 三态循环切换时间光标:无 → 单光标 → 双光标。 */
MERGE_LANES, /**< 合并为单一网格。 */
SPLIT_LANES, /**< 按信号拆分为多网格。 */
CLEAR_ALL /**< 清空当前标签页的全部网格与信号勾选。 */
};
/**
* @brief 顶部工具栏:文件操作与视图控制的入口。
*/
class Toolbar : public UiComponent
{
public:
Toolbar();
~Toolbar();
/**
* @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) override;
/**
* @brief 设置是否已加载数据(未加载时大部分按钮被禁用)。
* @param [IN] hasDocument 是否已加载数据。
*/
void SetDocumentLoaded(bool hasDocument);
/**
* @brief 设置可切换按钮的选中状态。
* @param [IN] action 目标按钮动作。
* @param [IN] checked 是否处于选中(生效)状态。
*/
void SetToggleState(ToolbarAction action, bool checked);
/**
* @brief 覆盖某个按钮的显示文本(用于状态相关的动态标签,例如「光标」)。
*
* 调用后需要由宿主重新排布(`Layout`)才能让按钮宽度与新文本匹配。
*
* @param [IN] action 目标按钮动作。
* @param [IN] label 新文本。
* @return 文本发生变化返回 true(此时宿主应重新排布)。
*/
bool SetActionLabel(ToolbarAction action, const std::wstring& label);
/**
* @brief 计算工具栏在指定缩放下的期望高度。
* @return 期望高度(像素)。
*/
float DesiredHeight() const;
/**
* @brief 查询某个按钮当前的屏幕矩形(用于把弹出菜单锚定到按钮下方)。
*
* 几何的唯一权威来源是控件本身(按钮宽度由文本实测得出),因此这里读控件而不是重算。
*
* @param [IN] action 目标按钮动作。
* @param [OUT] rect 按钮矩形(客户区坐标)。
* @return 找到且已摆放返回 true。
*/
bool ButtonRect(ToolbarAction action, gd::RectF& rect) const;
/**
* @brief 按给定区域重新排布按钮。
* @param [IN] area 工具栏可用区域。
*/
void Layout(const gd::RectF& area) override;
/** @brief 工具栏占用的整条区域(与 `Layout` 同源)。 */
gd::RectF Area() const override { return m_area; }
/**
* @brief 绘制工具栏。
* @param [IN,OUT] g 绘图上下文。
*/
void Paint(gd::Graphics& g) override;
/**
* @brief 处理鼠标移动,更新悬停状态。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 悬停状态发生变化返回 true。
*/
bool OnMouseMove(int x, int y);
/**
* @brief 处理鼠标左键按下。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @param [OUT] action 命中的按钮动作。
* @return 命中按钮返回 true。
*/
bool OnLButtonDown(int x, int y, ToolbarAction& action);
/**
* @brief 处理鼠标左键抬起。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @param [OUT] action 抬起时命中的按钮动作(仅在按钮上抬起时有效)。
* @return 视觉状态发生变化返回 true。
*/
bool OnLButtonUp(int x, int y, ToolbarAction& action);
/**
* @brief 处理鼠标离开工具栏。
* @return 视觉状态发生变化返回 true。
*/
bool OnMouseLeave() override;
/**
* @brief 判断坐标是否落在工具栏区域内。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 落在区域内返回 true。
*/
bool HitTest(int x, int y) const override;
/**
* @brief 绘制工具栏区域与**每个按钮**的布局边界(调试用)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] scale DPI 缩放系数。
*/
void PaintDebugBounds(gd::Graphics& g, float scale) override;
private:
/**
* @brief 单个工具栏按钮的**静态配置**(矩形与外观状态由控件自己维护)。
*/
struct Button
{
ToolbarAction action = ToolbarAction::NONE; /**< 按钮动作。 */
UiButtonWidget* widget = nullptr; /**< 对应控件(由 `m_root` 拥有)。 */
UiButtonIcon icon = UiButtonIcon::NONE; /**< 图标(非 `NONE` 时按钮只画图标)。 */
bool primary = false; /**< 是否为主按钮(强调色填充)。 */
bool needsDocument = true; /**< 是否依赖已加载数据。 */
bool toggle = false; /**< 是否为可切换按钮。 */
bool checked = false; /**< 可切换按钮的选中状态。 */
float width = 0.0f; /**< 按钮宽度(由控件实测得出)。 */
};
/**
* @brief 判断按钮当前是否可用。
* @param [IN] button 目标按钮。
* @return 可用返回 true。
*/
bool IsEnabled(const Button& button) const;
/**
* @brief 按固定规格表重建控件树。
*
* ⚠️ 只在 `m_treeDirty` 为真时由 `Layout` 调用:规格表、字体、主题、缩放、动态文本
* 任何一项变化都会置脏,而**纯粹的尺寸变化不会**(按钮几何由 `Arrange` 决定)。
* 旧实现每次 `Layout` 都无条件重建(拖动面板分隔条时每帧一次)⇒ 每帧 9 次堆分配
* 加 8 次测量缓存失效。
*/
void RebuildWidgets();
/** @brief 把可用性同步到控件(未加载数据时禁用依赖数据的按钮)。 */
void SyncEnabledState();
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
const Theme* m_theme = nullptr; /**< 主题资源。 */
float m_scale = 1.0f; /**< DPI 缩放比例。 */
bool m_documentLoaded = false; /**< 是否已加载数据。 */
std::vector<Button> m_buttons; /**< 按钮配置集合。 */
std::vector<std::pair<ToolbarAction, std::wstring>> m_labelOverrides; /**< 动态文本覆盖表。 */
gd::RectF m_area; /**< 工具栏整体区域。 */
std::unique_ptr<UiWidget> m_root; /**< 控件树根(负责底色与底部分隔线)。 */
ToolbarAction m_pendingAction = ToolbarAction::NONE; /**< 最近一次成功点击的动作。 */
bool m_treeDirty = true; /**< 控件树是否需要重建(见 `RebuildWidgets`)。 */
};
} // namespace jdi
#endif // __TOOLBAR_HPP__