Files
oak-editor/docs/zh/plans/riir/M2-oakundo.md
T
Mike-Solar c50017127b 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
2026-08-05 18:02:24 +08:00

172 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M2 · oakundo 拆分手册
> 内容:`engine/undo/`UndoCommand、MultiUndoCommand、UndoStack)。
> 依赖:common 2、node/project.h **1 处**undocommand.cpp)。
> 被依赖:node 4、timeline 1、render 1、plugin 2、capi 14。
> 拆分顺序第 2 位(近叶子)。
## 1. 目标形态
```
oakundo/
include/oakundo/{undocommand.h, undostack.h, types.h, export.h}
src/
tests/ # oakundo_gtest
```
## 2. 冻结 C API
### 2.1 `oakundo/undocommand.h`
```c
typedef struct OakUndoCommand OakUndoCommand;
/* 回调式命令(app 与跨模块命令的统一载体) */
typedef void (*oakundo_command_fn)(void *userdata);
OAKUNDO_API OakUndoCommand *oakundo_command_create(
const char *name, oakundo_command_fn redo, oakundo_command_fn undo,
oakundo_command_fn free_fn, void *userdata);
OAKUNDO_API OakUndoCommand *oakundo_command_create_multi(void);
OAKUNDO_API int oakundo_command_multi_add_child(OakUndoCommand *multi,
OakUndoCommand *child);
OAKUNDO_API int oakundo_command_multi_child_count(
const OakUndoCommand *multi);
OAKUNDO_API void oakundo_command_redo_now(OakUndoCommand *cmd);
OAKUNDO_API void oakundo_command_undo_now(OakUndoCommand *cmd);
OAKUNDO_API void oakundo_command_free(OakUndoCommand *cmd); /* NULL no-op */
```
### 2.2 `oakundo/undostack.h`
```c
typedef struct OakUndoStack OakUndoStack;
typedef struct OakUndoObjectParent OakUndoObjectParent; /* borrowed QObject 挂载点 */
OAKUNDO_API OakUndoStack *oakundo_undostack_init(
const OakUndoObjectParent *parent);
/* OakUndoObjectParentQObject 父子树挂载点的 borrowed 不透明句柄
* 2026-08 修订:按 01 §0.1 由 void* 改为有类型句柄;
* 提供侧内部即 QObject*,消费侧不可解引用;可传 NULL 表无父) */
OAKUNDO_API void oakundo_undostack_free(OakUndoStack *self);
OAKUNDO_API void oakundo_undostack_push(OakUndoStack *self,
OakUndoCommand *cmd, const char *name);
/* push_pre_executed:子命令已 redo 过,入栈不重复 redo(undo 分组用) */
OAKUNDO_API void oakundo_undostack_push_pre_executed(OakUndoStack *self,
OakUndoCommand *cmd, const char *name);
OAKUNDO_API int oakundo_undostack_can_undo(const OakUndoStack *self);
OAKUNDO_API int oakundo_undostack_can_redo(const OakUndoStack *self);
OAKUNDO_API void oakundo_undostack_undo(OakUndoStack *self);
OAKUNDO_API void oakundo_undostack_redo(OakUndoStack *self);
OAKUNDO_API void oakundo_undostack_jump(OakUndoStack *self, int64_t index);
OAKUNDO_API void oakundo_undostack_clear(OakUndoStack *self);
OAKUNDO_API int64_t oakundo_undostack_count(const OakUndoStack *self);
OAKUNDO_API int64_t oakundo_undostack_index(const OakUndoStack *self);
OAKUNDO_API int oakundo_undostack_command_text(OakUndoStack *self,
int64_t row, char *buf, int buf_size);
OAKUNDO_API int oakundo_undostack_command_is_done(OakUndoStack *self,
int64_t row);
OAKUNDO_API void oakundo_undostack_update_actions(OakUndoStack *self);
/* QAction* 句柄(GUI 菜单绑定用,borrowed */
OAKUNDO_API void *oakundo_undostack_undo_action(OakUndoStack *self);
OAKUNDO_API void *oakundo_undostack_redo_action(OakUndoStack *self);
```
**无事件接口(2026-08 修订)**:上层对下层只有命令。push/undo/redo/jump
的调用方知道栈索引的变化(`oakundo_undostack_index` 调用后即可读),
`index_changed` 通知改由**调用方所在层**(facade 的 undo 适配层)在
每次变更命令后自行发出——oakundo 不持有任何上层回调。
## 3. 切割点(1 处)
`undocommand.cpp` include `node/project.h``get_relevant_project()`
虚函数的 Project 类型)。
处理:`UndoCommand::get_relevant_project()` 的返回类型在 oakundo
内部改为不透明 `void *`oakundo 不解释它);node 侧(M3)在自己的
适配层把 `void *``olive::Project *` 互转。**不改语义**——
Project* 本来就只是作为不透明身份被使用(修改标记归属)。
## 4. 消费侧适配(按 01 §2)
- `node/`4 处)、`timeline/`1)、`render/`1)、`plugin/`2):
各自放 `adapter/undocommand.h`,转发用到的方法子集。
- `src/capi/undo.cpp`14 处):facade 的 undo 族实现改为转发
oakundo C API(或保持 facade 直链 oakundo——facade 是装配层,
裁决见 M9 §4,默认直链不绕圈)。
## 5. 测试(映射 03 §2/§3
- 每条 API 正常+错误路径(NULL self、空 push 不入栈——
空 MultiUndoCommand 被删除的既有行为必须有 TEST 钉死)。
- push/undo/redo/jump/clear 全序列;command_text/is_done 边界行;
每步后 `oakundo_undostack_index` 读数与预期一致(替代原事件断言——
调用方知道影响,直接读状态)。
- 往返测试: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`)。