- 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
7.9 KiB
7.9 KiB
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
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
typedef struct OakUndoStack OakUndoStack;
typedef struct OakUndoObjectParent OakUndoObjectParent; /* borrowed QObject 挂载点 */
OAKUNDO_API OakUndoStack *oakundo_undostack_init(
const OakUndoObjectParent *parent);
/* OakUndoObjectParent:QObject 父子树挂载点的 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不变),targetoakundo(SHARED)。src/undo/c_api/— 纯 C ABI 包装(undocommand.cpp、undostack.cpp- 内部共享头
commandhandle.h),通过target_sources合并进oakundo,不单独成库。
- 内部共享头
src/undo/tests/— gtest,targetoakundo-gtest,gtest_discover_tests。include/undo/(仓库根)— 公共 C 头:error.h、undocommand.h、undostack.h。src/undo/standalone/CMakeLists.txt— 独立构建 driver(见下)。
独立构建与测试
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 完全不认识 Project;M3 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)。