refactor(undo): de-Qt oakundo and wrap it in a pure C ABI

- strip QAbstractItemModel/QAction/signals-slots from UndoStack (UI
  concerns belong to the app layer), index_changed becomes a
  std::function callback
- decouple UndoCommand from Project via an optional modified-flag
  callback pair
- add pure C ABI in include/undo + src/undo/c_api (oakundo_ prefix,
  vtable-based command wrapper, OAKUNDO_E_* error codes)
- fix three engine-side defects: jump(0) infinite loop,
  MultiUndoCommand child leak, push_pre_executed not undoable
- add gtest suites (22 cases) and a standalone build driver
This commit is contained in:
2026-08-05 18:02:24 +08:00
parent bad52feda3
commit c50017127b
21 changed files with 2307 additions and 2 deletions
+66
View File
@@ -103,3 +103,69 @@ Project* 本来就只是作为不透明身份被使用(修改标记归属)
调用方知道影响,直接读状态)。
- 往返测试:C API 与适配类各做一遍 push-undo-redo,状态一致。
- `oakundo_debug_alive_count()`init/free 配对无泄漏。
## 实施现状(2026-08-05
M2 已落地并可独立构建、测试全绿。以下为与上文计划的实际差异。
### 最终目录结构
- `src/undo/src/` — 去 Qt 化 C++ 实现(`olive::` 命名空间,类名
`UndoCommand`/`MultiUndoCommand`/`UndoStack` 不变),target
`oakundo`SHARED)。
- `src/undo/c_api/` — 纯 C ABI 包装(`undocommand.cpp``undostack.cpp`
+ 内部共享头 `commandhandle.h`),通过 `target_sources` 合并进
`oakundo`,不单独成库。
- `src/undo/tests/` — gtesttarget `oakundo-gtest`
`gtest_discover_tests`
- `include/undo/`(仓库根)— 公共 C 头:`error.h``undocommand.h`
`undostack.h`
- `src/undo/standalone/CMakeLists.txt` — 独立构建 driver(见下)。
### 独立构建与测试
```sh
cmake -S src/undo/standalone -B build-oakundo
cmake --build build-oakundo -j
ctest --test-dir build-oakundo --output-on-failure
```
### 实际依赖
- Oak 内部:仅 oakcommon 的头文件宏(`define.h`
`DISABLE_COPY_MOVE`),纯头文件,按 include 路径引用
`${OAK_REPO_ROOT}/src/common/src`),**不链接** oakcommon
不依赖 olivecore/ffmpeg_bridge。
- 第三方:GTest(仅测试)。无 Qt。
### 与计划的主要差异
- 接口未按 §2 冻结清单逐条实现,而是对齐 oakcommon 的既有契约:
C ABI 头放在仓库根 `include/undo/`,命名 `oakundo_<族>_<动词>`
查询类函数返回 int 错误码 + out 参数(计划中的
`int64_t oakundo_undostack_count(...)` 直接返回值形式改为
`int ...(OakUndoStack *, int64_t *out)`);字符串两段式 buffer 约定。
- 句柄族名:`OakUndoCommand`/`OakUndoStack`init/free 语义与
oakcommon 一致(init 失败返回 NULL 且内部 try/catch 兜底,
free(NULL) 为 no-op)。
- §2.2 的 `OakUndoObjectParent`QObject 挂载点)、
`oakundo_undostack_update_actions``oakundo_undostack_undo_action/
redo_action`QAction 句柄)**未实现**QAction/model 属 app UI 层,
已从 UndoStack 剥离(见 notes.md「oakundo 去Qt化的删除与语义
变更」)。
- `index_changed` 事件:C ABI 无订阅接口(与 §2.2 修订一致);C++ 侧
保留了 `set_index_changed_callback(std::function<void(int)>)`
作为 Qt signal 的替代。
- §3 切割点的处理与计划不同:`get_relevant_project()` 未改成返回
`void *`,而是整体删除,修改标记语义改为
`UndoCommand::set_modified_callbacks(is_modified, set_modified)`
回调对(oakundo 完全不认识 ProjectM3 node 适配层绑定
`Project::is_modified/set_modified`)。
- 未实现 `oakundo_debug_alive_count()` 泄漏计数。
- 额外行为修复:`UndoStack` 析构不再经 `clear()`(原实现在析构时会
再 push 一个 EmptyCommand 造成泄漏),改为直接删除持有命令;
`jump` 增加 `can_undo/can_redo` 守卫(原版 `jump(0)` 死循环);
`MultiUndoCommand` 析构删除子命令(原版泄漏);
`push_pre_executed` 通过新增的 `UndoCommand::set_done()` 置完成
标记(原版入栈后 undo 为空操作)。详见 notes.md。
- 测试结果:22 个用例全部通过(独立构建 `build-oakundo`)。
+46
View File
@@ -80,3 +80,49 @@ oakcommon;后续若确认无用途可直接删除。
- `FFmpegUtils`:新增类内常量 `k_rgb_channel_count/k_rgba_channel_count`
3/4)取代对 `VideoParams` 同名常量的引用,消除 common→render
反向依赖。
## oakundo 去Qt化的删除与语义变更(2026-08-05)
`src/undo/`olive::UndoCommand / MultiUndoCommand / UndoStack)去Qt化
过程中以下删除与语义变化,迁移调用方时需注意:
- `UndoStack` 不再继承 `QAbstractItemModel`(删除 `Q_OBJECT`
`columnCount/data/index/parent/rowCount/headerData/hasChildren` 全部
model overrides 及 `begin/end*Rows` 通知)。历史面板(QTreeView 等)
属 app UI 层,迁移时应以 facade 的行式查询
command_count/done_count/command_name/command_is_done)自行实现
model。
- `UndoStack::GetUndoAction/GetRedoAction/update_actions`QAction 成员
及其文本/使能维护)删除——QAction 属 app 菜单层,app 应自行创建
action 并连接 undo/redo`index_changed(int)` signal 改为 C++ 侧
`std::function<void(int)>``set_index_changed_callback`),C ABI
不暴露事件,调用方在变更命令后读 `oakundo_undostack_index`
- `UndoCommand``olive::Project` 解耦:删除纯虚
`get_relevant_project()`(含 MultiUndoCommand/EmptyCommand 的
override)与 `project_` 成员;`redo_and_set_modified/
undo_and_set_modified` 的修改标记语义改为可选回调对
`set_modified_callbacks(is_modified, set_modified)`redo 时记录当前
修改标记并置 true,undo 时恢复记录值;不挂回调则退化为纯
redo/undo。读写顺序与 Qt 版一致(redo_now() 之后读取并置位),
行为对齐。Project 侧迁移时在适配层把 `Project::is_modified/
set_modified` 绑到这对回调即可。
- `UndoStack` 析构不再调用 `clear()`(Qt 版在析构里 clear 会再 push
一个 EmptyCommand 造成泄漏),改为直接删除所有持有命令。
- 顺带修复三处 engine 原版就存在的缺陷(迁移调用方无需适配,但
行为与旧版不同,特此记录):
- `jump(0)` 死循环:底部 EmptyCommand 不可 undo`jump` 现以
`can_undo()/can_redo()` 为守卫,跳到 0 时停在 index 1;
- `MultiUndoCommand` 原来不拥有子命令(析构泄漏),现析构删除
全部 children
- `push_pre_executed` 原来不置 `done_` 标记,导致入栈后 undo
为空操作(facade undo-group 的组撤销实际不生效),现入栈前
`set_done(true)`UndoCommand 新增 `set_done()`)。
- `clear()` 推入的占位命令名固定为字面量 "New/Open Project"(原
`tr()` 翻译移除,翻译由 app 层负责)。
- `push/push_pre_executed` 的命令名由 `QString` 改为 `std::string`
C ABI 中 NULL name 视同空串。
- C ABI 句柄语义:`oakundo_command_init/init_multi` 返回的句柄拥有
底层命令;push 或 `oakundo_command_multi_add_child` 成功后所有权
转移、wrapper 被消费(不可再用/再 free);
`oakundo_command_multi_child` 返回 borrowed wrapperfree 只释放
wrapper)。