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:
@@ -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/` — gtest,target `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 完全不认识 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`)。
|
||||
|
||||
@@ -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 wrapper(free 只释放
|
||||
wrapper)。
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_EDITOR_UNDO_ERROR_H
|
||||
#define OAK_EDITOR_UNDO_ERROR_H
|
||||
|
||||
/**
|
||||
* @brief Status and error codes shared by all oakundo C API families.
|
||||
*
|
||||
* Return-code convention (mirrors engine/include/oakengine/init.h):
|
||||
* 0 (OAKUNDO_OK) on success, a negative OAKUNDO_E_* error code on
|
||||
* failure. String getters return the required buffer size in bytes
|
||||
* (including the terminating NUL) as a non-negative value instead.
|
||||
*/
|
||||
#define OAKUNDO_OK 0 /**< Success. */
|
||||
#define OAKUNDO_E_INVALID (-1) /**< NULL handle or invalid argument. */
|
||||
#define OAKUNDO_E_STATE (-2) /**< Call not valid in the current state. */
|
||||
#define OAKUNDO_E_FAILED (-3) /**< The underlying operation failed. */
|
||||
#define OAKUNDO_E_NOT_FOUND (-4) /**< Index out of range / entry not found. */
|
||||
#define OAKUNDO_E_NOMEM (-5) /**< Allocation failed. */
|
||||
|
||||
#endif //OAK_EDITOR_UNDO_ERROR_H
|
||||
@@ -0,0 +1,137 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_EDITOR_UNDO_UNDOCOMMAND_H
|
||||
#define OAK_EDITOR_UNDO_UNDOCOMMAND_H
|
||||
|
||||
#include "undo/error.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Opaque handle to an undo command (olive::UndoCommand).
|
||||
*
|
||||
* Handles created by oakundo_command_init() /
|
||||
* oakundo_command_init_multi() own the underlying command and must be
|
||||
* released with oakundo_command_free() UNLESS they are pushed onto an
|
||||
* OakUndoStack, which takes ownership. Handles returned by
|
||||
* oakundo_command_multi_child() are borrowed wrappers: free the wrapper
|
||||
* with oakundo_command_free(), the underlying command stays owned by the
|
||||
* multi command.
|
||||
*/
|
||||
typedef struct OakUndoCommand OakUndoCommand;
|
||||
|
||||
/**
|
||||
* @brief Callback table backing a caller-defined undo command.
|
||||
*
|
||||
* Any callback may be NULL; a NULL redo/undo makes that direction a
|
||||
* no-op. free_fn is invoked when the command is destroyed (whether pushed
|
||||
* onto a stack or freed directly) and releases userdata.
|
||||
*/
|
||||
typedef struct OakUndoCommandVtable {
|
||||
void (*redo)(void *userdata);
|
||||
void (*undo)(void *userdata);
|
||||
void (*free_fn)(void *userdata);
|
||||
} OakUndoCommandVtable;
|
||||
|
||||
/**
|
||||
* @brief Create an undo command backed by C callbacks.
|
||||
*
|
||||
* The command takes ownership of `userdata`; `vtable` is copied.
|
||||
*
|
||||
* @return Command handle, or NULL on invalid argument or allocation
|
||||
* failure.
|
||||
*/
|
||||
OakUndoCommand *oakundo_command_init(const OakUndoCommandVtable *vtable,
|
||||
void *userdata);
|
||||
|
||||
/**
|
||||
* @brief Create an empty multi command (olive::MultiUndoCommand).
|
||||
*
|
||||
* @return Command handle, or NULL on allocation failure.
|
||||
*/
|
||||
OakUndoCommand *oakundo_command_init_multi(void);
|
||||
|
||||
/**
|
||||
* @brief Add `child` to the multi command `multi`.
|
||||
*
|
||||
* On success the multi command takes ownership of the underlying child
|
||||
* command; the child handle wrapper is consumed and must not be used or
|
||||
* freed afterwards.
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_command_multi_add_child(OakUndoCommand *multi,
|
||||
OakUndoCommand *child);
|
||||
|
||||
/**
|
||||
* @brief Query the number of children in a multi command.
|
||||
*
|
||||
* @param out_count Receives the result. Must not be NULL.
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_command_multi_child_count(OakUndoCommand *multi, int *out_count);
|
||||
|
||||
/**
|
||||
* @brief Borrow a handle to the child at `index` of a multi command.
|
||||
*
|
||||
* The returned handle is a wrapper owned by the caller (free with
|
||||
* oakundo_command_free()); the underlying command is owned by `multi`.
|
||||
*
|
||||
* @return OAKUNDO_OK, OAKUNDO_E_NOT_FOUND for an out-of-range index, or
|
||||
* another negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_command_multi_child(OakUndoCommand *multi, int index,
|
||||
OakUndoCommand **out_child);
|
||||
|
||||
/**
|
||||
* @brief Execute the command's redo without a stack
|
||||
* (olive::UndoCommand::redo_now semantics; a no-op if already done).
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_command_redo_now(OakUndoCommand *command);
|
||||
|
||||
/**
|
||||
* @brief Execute the command's undo without a stack
|
||||
* (olive::UndoCommand::undo_now semantics; a no-op if not done).
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_command_undo_now(OakUndoCommand *command);
|
||||
|
||||
/**
|
||||
* @brief Destroy a command handle.
|
||||
*
|
||||
* Owned handles destroy the underlying command; borrowed handles (from
|
||||
* oakundo_command_multi_child()) only destroy the wrapper. Commands
|
||||
* pushed onto an OakUndoStack are owned by the stack and must not be
|
||||
* freed by the caller. NULL is a no-op.
|
||||
*/
|
||||
void oakundo_command_free(OakUndoCommand *command);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //OAK_EDITOR_UNDO_UNDOCOMMAND_H
|
||||
@@ -0,0 +1,164 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_EDITOR_UNDO_UNDOSTACK_H
|
||||
#define OAK_EDITOR_UNDO_UNDOSTACK_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "undo/error.h"
|
||||
#include "undo/undocommand.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Opaque handle to an undo stack (olive::UndoStack).
|
||||
*/
|
||||
typedef struct OakUndoStack OakUndoStack;
|
||||
|
||||
/**
|
||||
* @brief Create an undo stack.
|
||||
*
|
||||
* A fresh stack contains a single "New/Open Project" empty command,
|
||||
* matching olive::UndoStack::clear().
|
||||
*
|
||||
* @return Stack handle, or NULL on allocation failure.
|
||||
*/
|
||||
OakUndoStack *oakundo_undostack_init(void);
|
||||
|
||||
/**
|
||||
* @brief Destroy an undo stack and all commands it owns.
|
||||
*
|
||||
* NULL is a no-op.
|
||||
*/
|
||||
void oakundo_undostack_free(OakUndoStack *stack);
|
||||
|
||||
/**
|
||||
* @brief Push `command` onto the stack and execute its redo.
|
||||
*
|
||||
* On success the stack takes ownership of the underlying command; the
|
||||
* handle wrapper is consumed and must not be used or freed afterwards.
|
||||
* An empty multi command is deleted immediately (not pushed), matching
|
||||
* olive::UndoStack::push. `name` is the user-visible label (NULL behaves
|
||||
* like an empty label).
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_push(OakUndoStack *stack, OakUndoCommand *command,
|
||||
const char *name);
|
||||
|
||||
/**
|
||||
* @brief Push a command that has already been executed (redo skipped).
|
||||
*
|
||||
* Ownership rules match oakundo_undostack_push().
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_push_pre_executed(OakUndoStack *stack,
|
||||
OakUndoCommand *command,
|
||||
const char *name);
|
||||
|
||||
/**
|
||||
* @brief Undo the most recently done command, if any.
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_undo(OakUndoStack *stack);
|
||||
|
||||
/**
|
||||
* @brief Redo the most recently undone command, if any.
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_redo(OakUndoStack *stack);
|
||||
|
||||
/**
|
||||
* @brief Undo/redo until the done-command count equals `index`
|
||||
* (olive::UndoStack::jump semantics). Negative values are clamped to 0.
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_jump(OakUndoStack *stack, int64_t index);
|
||||
|
||||
/**
|
||||
* @brief Delete all commands and push the fresh "New/Open Project" empty
|
||||
* command (olive::UndoStack::clear).
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_clear(OakUndoStack *stack);
|
||||
|
||||
/**
|
||||
* @brief Query whether undo (redo) is currently possible.
|
||||
*
|
||||
* @param out_value Receives 1/0. Must not be NULL.
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_can_undo(OakUndoStack *stack, int *out_value);
|
||||
int oakundo_undostack_can_redo(OakUndoStack *stack, int *out_value);
|
||||
|
||||
/**
|
||||
* @brief Total number of history rows (done + undone commands).
|
||||
*
|
||||
* @param out_count Receives the result. Must not be NULL.
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_count(OakUndoStack *stack, int64_t *out_count);
|
||||
|
||||
/**
|
||||
* @brief Current position in the history: the number of done commands
|
||||
* (rows at or above this index are undone).
|
||||
*
|
||||
* @param out_index Receives the result. Must not be NULL.
|
||||
*
|
||||
* @return OAKUNDO_OK or a negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_index(OakUndoStack *stack, int64_t *out_index);
|
||||
|
||||
/**
|
||||
* @brief Label of the history row at `row` (0-based, two-stage getter).
|
||||
*
|
||||
* @return Required buffer size in bytes including the terminating NUL
|
||||
* (non-negative), OAKUNDO_E_NOT_FOUND for an invalid row, or
|
||||
* another negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_command_text(OakUndoStack *stack, int64_t row,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Query whether the row at `row` is currently done (not undone).
|
||||
*
|
||||
* @param out_value Receives 1 (done) / 0 (undone). Must not be NULL.
|
||||
*
|
||||
* @return OAKUNDO_OK, OAKUNDO_E_NOT_FOUND for an invalid row, or another
|
||||
* negative OAKUNDO_E_* error code.
|
||||
*/
|
||||
int oakundo_undostack_command_is_done(OakUndoStack *stack, int64_t row,
|
||||
int *out_value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //OAK_EDITOR_UNDO_UNDOSTACK_H
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(undo)
|
||||
@@ -1,6 +1,6 @@
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(c_api)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
if(BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(c_api)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -0,0 +1,4 @@
|
||||
target_sources(oakundo PRIVATE
|
||||
undocommand.cpp
|
||||
undostack.cpp
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_UNDO_COMMANDHANDLE_H
|
||||
#define OAK_UNDO_COMMANDHANDLE_H
|
||||
|
||||
#include "undo/undocommand.h"
|
||||
|
||||
#include "../src/undocommand.h"
|
||||
|
||||
/**
|
||||
* @brief Internal layout of the OakUndoCommand handle, shared between the
|
||||
* c_api translation units.
|
||||
*
|
||||
* `owned` is true for handles created by oakundo_command_init() /
|
||||
* oakundo_command_init_multi() and false for borrowed wrappers handed out
|
||||
* by oakundo_command_multi_child().
|
||||
*/
|
||||
struct OakUndoCommand {
|
||||
olive::UndoCommand *command;
|
||||
bool owned;
|
||||
};
|
||||
|
||||
#endif // OAK_UNDO_COMMANDHANDLE_H
|
||||
@@ -0,0 +1,217 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "undo/undocommand.h"
|
||||
|
||||
#include <new>
|
||||
|
||||
#include "commandhandle.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief olive::UndoCommand subclass forwarding to a C callback table.
|
||||
*/
|
||||
class CallbackUndoCommand : public olive::UndoCommand {
|
||||
public:
|
||||
CallbackUndoCommand(const OakUndoCommandVtable &vtable, void *userdata)
|
||||
: vtable_(vtable), userdata_(userdata)
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~CallbackUndoCommand() override
|
||||
{
|
||||
if (vtable_.free_fn) {
|
||||
vtable_.free_fn(userdata_);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override
|
||||
{
|
||||
if (vtable_.redo) {
|
||||
vtable_.redo(userdata_);
|
||||
}
|
||||
}
|
||||
|
||||
virtual void undo() override
|
||||
{
|
||||
if (vtable_.undo) {
|
||||
vtable_.undo(userdata_);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
OakUndoCommandVtable vtable_;
|
||||
void *userdata_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
OakUndoCommand *oakundo_command_init(const OakUndoCommandVtable *vtable,
|
||||
void *userdata)
|
||||
{
|
||||
if (!vtable) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
try {
|
||||
OakUndoCommand *handle = new (std::nothrow) OakUndoCommand();
|
||||
if (!handle) {
|
||||
return NULL;
|
||||
}
|
||||
handle->command = new CallbackUndoCommand(*vtable, userdata);
|
||||
handle->owned = true;
|
||||
return handle;
|
||||
} catch (...) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
OakUndoCommand *oakundo_command_init_multi(void)
|
||||
{
|
||||
try {
|
||||
OakUndoCommand *handle = new (std::nothrow) OakUndoCommand();
|
||||
if (!handle) {
|
||||
return NULL;
|
||||
}
|
||||
handle->command = new olive::MultiUndoCommand();
|
||||
handle->owned = true;
|
||||
return handle;
|
||||
} catch (...) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_command_multi_add_child(OakUndoCommand *multi,
|
||||
OakUndoCommand *child)
|
||||
{
|
||||
if (!multi || !multi->command || !child || !child->command) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
olive::MultiUndoCommand *mcu =
|
||||
dynamic_cast<olive::MultiUndoCommand *>(multi->command);
|
||||
if (!mcu) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
mcu->add_child(child->command);
|
||||
// Ownership of the underlying command moved to the multi command;
|
||||
// consume the wrapper.
|
||||
delete child;
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_command_multi_child_count(OakUndoCommand *multi, int *out_count)
|
||||
{
|
||||
if (!multi || !multi->command || !out_count) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
olive::MultiUndoCommand *mcu =
|
||||
dynamic_cast<olive::MultiUndoCommand *>(multi->command);
|
||||
if (!mcu) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
*out_count = mcu->child_count();
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_command_multi_child(OakUndoCommand *multi, int index,
|
||||
OakUndoCommand **out_child)
|
||||
{
|
||||
if (!multi || !multi->command || !out_child) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
olive::MultiUndoCommand *mcu =
|
||||
dynamic_cast<olive::MultiUndoCommand *>(multi->command);
|
||||
if (!mcu) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
if (index < 0 || index >= mcu->child_count()) {
|
||||
return OAKUNDO_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
OakUndoCommand *handle = new (std::nothrow) OakUndoCommand();
|
||||
if (!handle) {
|
||||
return OAKUNDO_E_NOMEM;
|
||||
}
|
||||
handle->command = mcu->child(index);
|
||||
handle->owned = false;
|
||||
*out_child = handle;
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_command_redo_now(OakUndoCommand *command)
|
||||
{
|
||||
if (!command || !command->command) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
command->command->redo_now();
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_command_undo_now(OakUndoCommand *command)
|
||||
{
|
||||
if (!command || !command->command) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
command->command->undo_now();
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
void oakundo_command_free(OakUndoCommand *command)
|
||||
{
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (command->owned) {
|
||||
delete command->command;
|
||||
}
|
||||
delete command;
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "undo/undostack.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#include "../src/undostack.h"
|
||||
#include "commandhandle.h"
|
||||
|
||||
struct OakUndoStack {
|
||||
olive::UndoStack impl;
|
||||
};
|
||||
|
||||
OakUndoStack *oakundo_undostack_init(void)
|
||||
{
|
||||
try {
|
||||
return new (std::nothrow) OakUndoStack();
|
||||
} catch (...) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void oakundo_undostack_free(OakUndoStack *stack)
|
||||
{
|
||||
delete stack;
|
||||
}
|
||||
|
||||
int oakundo_undostack_push(OakUndoStack *stack, OakUndoCommand *command,
|
||||
const char *name)
|
||||
{
|
||||
if (!stack || !command || !command->command) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
olive::UndoCommand *impl = command->command;
|
||||
stack->impl.push(impl, name ? name : "");
|
||||
// Ownership of the underlying command moved to the stack (or was
|
||||
// deleted as an empty multi command); consume the wrapper.
|
||||
delete command;
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_push_pre_executed(OakUndoStack *stack,
|
||||
OakUndoCommand *command,
|
||||
const char *name)
|
||||
{
|
||||
if (!stack || !command || !command->command) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
olive::UndoCommand *impl = command->command;
|
||||
stack->impl.push_pre_executed(impl, name ? name : "");
|
||||
delete command;
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_undo(OakUndoStack *stack)
|
||||
{
|
||||
if (!stack) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
stack->impl.undo();
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_redo(OakUndoStack *stack)
|
||||
{
|
||||
if (!stack) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
stack->impl.redo();
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_jump(OakUndoStack *stack, int64_t index)
|
||||
{
|
||||
if (!stack) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
stack->impl.jump(index < 0 ? 0 : static_cast<size_t>(index));
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_clear(OakUndoStack *stack)
|
||||
{
|
||||
if (!stack) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
stack->impl.clear();
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_can_undo(OakUndoStack *stack, int *out_value)
|
||||
{
|
||||
if (!stack || !out_value) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
*out_value = stack->impl.can_undo() ? 1 : 0;
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_can_redo(OakUndoStack *stack, int *out_value)
|
||||
{
|
||||
if (!stack || !out_value) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
*out_value = stack->impl.can_redo() ? 1 : 0;
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_count(OakUndoStack *stack, int64_t *out_count)
|
||||
{
|
||||
if (!stack || !out_count) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
*out_count = stack->impl.command_count();
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_index(OakUndoStack *stack, int64_t *out_index)
|
||||
{
|
||||
if (!stack || !out_index) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
*out_index = stack->impl.done_count();
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_command_text(OakUndoStack *stack, int64_t row,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!stack) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
if (row < 0 || row >= stack->impl.command_count()) {
|
||||
return OAKUNDO_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
std::string name = stack->impl.command_name(int(row));
|
||||
|
||||
int required = static_cast<int>(name.size()) + 1;
|
||||
if (buf && buf_size > 0) {
|
||||
size_t copy_len = name.size();
|
||||
if (copy_len > static_cast<size_t>(buf_size) - 1) {
|
||||
copy_len = static_cast<size_t>(buf_size) - 1;
|
||||
}
|
||||
memcpy(buf, name.data(), copy_len);
|
||||
buf[copy_len] = '\0';
|
||||
}
|
||||
return required;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakundo_undostack_command_is_done(OakUndoStack *stack, int64_t row,
|
||||
int *out_value)
|
||||
{
|
||||
if (!stack || !out_value) {
|
||||
return OAKUNDO_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
if (row < 0 || row >= stack->impl.command_count()) {
|
||||
return OAKUNDO_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
*out_value = stack->impl.command_is_done(int(row)) ? 1 : 0;
|
||||
return OAKUNDO_OK;
|
||||
} catch (...) {
|
||||
return OAKUNDO_E_FAILED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# Olive - Non-Linear Video Editor
|
||||
# Copyright (C) 2022 Olive Team
|
||||
# Modifications Copyright (C) 2025 mikesolar
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
add_library(oakundo SHARED
|
||||
undocommand.cpp
|
||||
undocommand.h
|
||||
undostack.cpp
|
||||
undostack.h
|
||||
)
|
||||
|
||||
# In a full-tree build the repo root is CMAKE_SOURCE_DIR; a standalone
|
||||
# build (see src/undo/standalone) sets OAK_REPO_ROOT explicitly.
|
||||
if(NOT DEFINED OAK_REPO_ROOT)
|
||||
set(OAK_REPO_ROOT ${CMAKE_SOURCE_DIR})
|
||||
endif()
|
||||
|
||||
# oakundo depends only on oakcommon's header-only define.h macros
|
||||
# (DISABLE_COPY_MOVE), so an include path is enough; no library is linked.
|
||||
target_include_directories(oakundo PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${OAK_REPO_ROOT}/src/common/src
|
||||
${OAK_REPO_ROOT}/include
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
Modifications Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "undocommand.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
MultiUndoCommand::~MultiUndoCommand()
|
||||
{
|
||||
for (auto it = children_.begin(); it != children_.end(); it++) {
|
||||
delete *it;
|
||||
}
|
||||
}
|
||||
|
||||
void MultiUndoCommand::redo()
|
||||
{
|
||||
for (auto it = children_.cbegin(); it != children_.cend(); it++) {
|
||||
(*it)->redo_and_set_modified();
|
||||
}
|
||||
}
|
||||
|
||||
void MultiUndoCommand::undo()
|
||||
{
|
||||
for (auto it = children_.crbegin(); it != children_.crend(); it++) {
|
||||
(*it)->undo_and_set_modified();
|
||||
}
|
||||
}
|
||||
|
||||
UndoCommand::UndoCommand()
|
||||
{
|
||||
modified_ = false;
|
||||
prepared_ = false;
|
||||
done_ = false;
|
||||
}
|
||||
|
||||
void UndoCommand::set_modified_callbacks(
|
||||
std::function<bool()> is_modified, std::function<void(bool)> set_modified)
|
||||
{
|
||||
is_modified_ = std::move(is_modified);
|
||||
set_modified_ = std::move(set_modified);
|
||||
}
|
||||
|
||||
void UndoCommand::redo_and_set_modified()
|
||||
{
|
||||
redo_now();
|
||||
|
||||
if (is_modified_ && set_modified_) {
|
||||
modified_ = is_modified_();
|
||||
set_modified_(true);
|
||||
}
|
||||
}
|
||||
|
||||
void UndoCommand::undo_and_set_modified()
|
||||
{
|
||||
undo_now();
|
||||
|
||||
if (set_modified_) {
|
||||
set_modified_(modified_);
|
||||
}
|
||||
}
|
||||
|
||||
void UndoCommand::redo_now()
|
||||
{
|
||||
if (!done_) {
|
||||
if (!prepared_) {
|
||||
prepare();
|
||||
prepared_ = true;
|
||||
}
|
||||
|
||||
redo();
|
||||
done_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UndoCommand::undo_now()
|
||||
{
|
||||
if (done_) {
|
||||
undo();
|
||||
done_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
Modifications Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_UNDOCOMMAND_H
|
||||
#define OAK_UNDOCOMMAND_H
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include "define.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Base class for an undoable operation.
|
||||
*
|
||||
* Subclasses implement redo() and undo(). The Qt version coupled this
|
||||
* class to olive::Project through get_relevant_project(); oakundo instead
|
||||
* lets the caller attach a modified-state callback pair with
|
||||
* set_modified_callbacks(). When callbacks are attached,
|
||||
* redo_and_set_modified() records the current modified state and forces
|
||||
* "modified", and undo_and_set_modified() restores the recorded state.
|
||||
* Without callbacks those functions simply redo/undo.
|
||||
*/
|
||||
class UndoCommand {
|
||||
public:
|
||||
UndoCommand();
|
||||
|
||||
virtual ~UndoCommand()
|
||||
{
|
||||
}
|
||||
|
||||
DISABLE_COPY_MOVE(UndoCommand)
|
||||
|
||||
bool has_prepared() const
|
||||
{
|
||||
return prepared_;
|
||||
}
|
||||
void set_prepared(bool e)
|
||||
{
|
||||
prepared_ = true;
|
||||
}
|
||||
|
||||
void redo_now();
|
||||
void undo_now();
|
||||
|
||||
/**
|
||||
* @brief Mark the command as already executed (done) without running
|
||||
* redo. Used by UndoStack::push_pre_executed() so the command remains
|
||||
* undoable.
|
||||
*/
|
||||
void set_done(bool e)
|
||||
{
|
||||
done_ = e;
|
||||
}
|
||||
|
||||
void redo_and_set_modified();
|
||||
void undo_and_set_modified();
|
||||
|
||||
/**
|
||||
* @brief Attach project modified-state accessors.
|
||||
*
|
||||
* @param is_modified Getter for the current modified flag.
|
||||
* @param set_modified Setter for the modified flag.
|
||||
*
|
||||
* Either may be empty; both directions are guarded. Passing two empty
|
||||
* functions detaches the callbacks.
|
||||
*/
|
||||
void set_modified_callbacks(std::function<bool()> is_modified,
|
||||
std::function<void(bool)> set_modified);
|
||||
|
||||
protected:
|
||||
virtual void prepare()
|
||||
{
|
||||
}
|
||||
virtual void redo() = 0;
|
||||
virtual void undo() = 0;
|
||||
|
||||
private:
|
||||
bool modified_;
|
||||
|
||||
bool prepared_;
|
||||
|
||||
bool done_;
|
||||
|
||||
std::function<bool()> is_modified_;
|
||||
|
||||
std::function<void(bool)> set_modified_;
|
||||
};
|
||||
|
||||
class MultiUndoCommand : public UndoCommand {
|
||||
public:
|
||||
MultiUndoCommand() = default;
|
||||
|
||||
/**
|
||||
* @brief Destructor. Deletes all owned children (added with
|
||||
* add_child() and not yet pushed elsewhere).
|
||||
*/
|
||||
virtual ~MultiUndoCommand() override;
|
||||
|
||||
void add_child(UndoCommand *command)
|
||||
{
|
||||
children_.push_back(command);
|
||||
}
|
||||
|
||||
int child_count() const
|
||||
{
|
||||
return children_.size();
|
||||
}
|
||||
|
||||
UndoCommand *child(int i) const
|
||||
{
|
||||
return children_[i];
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override;
|
||||
virtual void undo() override;
|
||||
|
||||
private:
|
||||
std::vector<UndoCommand *> children_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_UNDOCOMMAND_H
|
||||
@@ -0,0 +1,194 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
Modifications Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "undostack.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
const int UndoStack::k_max_undo_commands = 200;
|
||||
|
||||
class EmptyCommand : public UndoCommand {
|
||||
public:
|
||||
EmptyCommand()
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override
|
||||
{
|
||||
}
|
||||
virtual void undo() override
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
UndoStack::UndoStack()
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
UndoStack::~UndoStack()
|
||||
{
|
||||
for (auto it = commands_.cbegin(); it != commands_.cend(); it++) {
|
||||
delete it->command;
|
||||
}
|
||||
for (auto it = undone_commands_.cbegin(); it != undone_commands_.cend();
|
||||
it++) {
|
||||
delete it->command;
|
||||
}
|
||||
}
|
||||
|
||||
void UndoStack::push(UndoCommand *command, const std::string &name)
|
||||
{
|
||||
MultiUndoCommand *mcu = dynamic_cast<MultiUndoCommand *>(command);
|
||||
if (mcu && mcu->child_count() == 0) {
|
||||
delete command;
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any redoable commands
|
||||
if (can_redo()) {
|
||||
for (auto it = undone_commands_.cbegin(); it != undone_commands_.cend();
|
||||
it++) {
|
||||
delete it->command;
|
||||
}
|
||||
undone_commands_.clear();
|
||||
}
|
||||
|
||||
// Do command and push
|
||||
command->redo_and_set_modified();
|
||||
commands_.push_back({ command, name });
|
||||
|
||||
// Delete oldest
|
||||
if (commands_.size() > k_max_undo_commands) {
|
||||
delete commands_.front().command;
|
||||
commands_.pop_front();
|
||||
}
|
||||
|
||||
emit_index_changed();
|
||||
}
|
||||
|
||||
void UndoStack::push_pre_executed(UndoCommand *command, const std::string &name)
|
||||
{
|
||||
MultiUndoCommand *mcu = dynamic_cast<MultiUndoCommand *>(command);
|
||||
if (mcu && mcu->child_count() == 0) {
|
||||
delete command;
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any redoable commands
|
||||
if (can_redo()) {
|
||||
for (auto it = undone_commands_.cbegin(); it != undone_commands_.cend();
|
||||
it++) {
|
||||
delete it->command;
|
||||
}
|
||||
undone_commands_.clear();
|
||||
}
|
||||
|
||||
// Push without redoing: the caller already executed the children.
|
||||
// Mark the command done so it stays undoable.
|
||||
command->set_done(true);
|
||||
commands_.push_back({ command, name });
|
||||
|
||||
// Delete oldest
|
||||
if (commands_.size() > k_max_undo_commands) {
|
||||
delete commands_.front().command;
|
||||
commands_.pop_front();
|
||||
}
|
||||
|
||||
emit_index_changed();
|
||||
}
|
||||
|
||||
void UndoStack::jump(size_t index)
|
||||
{
|
||||
// Guard with can_undo/can_redo: the bottom EmptyCommand is not
|
||||
// undoable, so jumping to 0 must stop at it instead of spinning.
|
||||
while (commands_.size() > index && can_undo()) {
|
||||
undo();
|
||||
}
|
||||
while (commands_.size() < index && can_redo()) {
|
||||
redo();
|
||||
}
|
||||
}
|
||||
|
||||
void UndoStack::undo()
|
||||
{
|
||||
if (can_undo()) {
|
||||
// Undo most recently done command
|
||||
commands_.back().command->undo_and_set_modified();
|
||||
|
||||
// Place at the front of the "undone commands" list
|
||||
undone_commands_.push_front(commands_.back());
|
||||
|
||||
// Remove undone command from the commands list
|
||||
commands_.pop_back();
|
||||
|
||||
emit_index_changed();
|
||||
}
|
||||
}
|
||||
|
||||
void UndoStack::redo()
|
||||
{
|
||||
if (can_redo()) {
|
||||
// Redo most recently undone command
|
||||
undone_commands_.front().command->redo_and_set_modified();
|
||||
|
||||
// Place at the back of the done commands list
|
||||
commands_.push_back(undone_commands_.front());
|
||||
|
||||
// Remove done command from undone list
|
||||
undone_commands_.pop_front();
|
||||
|
||||
emit_index_changed();
|
||||
}
|
||||
}
|
||||
|
||||
void UndoStack::clear()
|
||||
{
|
||||
for (auto it = commands_.cbegin(); it != commands_.cend(); it++) {
|
||||
delete it->command;
|
||||
}
|
||||
commands_.clear();
|
||||
for (auto it = undone_commands_.cbegin(); it != undone_commands_.cend();
|
||||
it++) {
|
||||
delete it->command;
|
||||
}
|
||||
undone_commands_.clear();
|
||||
|
||||
push(new EmptyCommand(), "New/Open Project");
|
||||
}
|
||||
|
||||
bool UndoStack::can_undo() const
|
||||
{
|
||||
return !commands_.empty() &&
|
||||
!dynamic_cast<EmptyCommand *>(commands_.back().command);
|
||||
}
|
||||
|
||||
void UndoStack::emit_index_changed()
|
||||
{
|
||||
if (index_changed_callback_) {
|
||||
index_changed_callback_(int(commands_.size()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
Modifications Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_UNDOSTACK_H
|
||||
#define OAK_UNDOSTACK_H
|
||||
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <string>
|
||||
|
||||
#include "define.h"
|
||||
#include "undocommand.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief History stack of undoable commands.
|
||||
*
|
||||
* The Qt version inherited QAbstractItemModel and owned undo/redo
|
||||
* QActions; both are UI concerns and were removed in oakundo (they belong
|
||||
* to the app layer). The index_changed signal is replaced by an optional
|
||||
* std::function callback (set_index_changed_callback()).
|
||||
*/
|
||||
class UndoStack {
|
||||
public:
|
||||
UndoStack();
|
||||
|
||||
virtual ~UndoStack();
|
||||
|
||||
DISABLE_COPY_MOVE(UndoStack)
|
||||
|
||||
void push(UndoCommand *command, const std::string &name);
|
||||
|
||||
/**
|
||||
* @brief Push a command that has already been executed (redo skipped).
|
||||
*
|
||||
* Used by the facade undo-group: child commands are added to the group
|
||||
* and executed eagerly, then the whole group is pushed with this method
|
||||
* so it is not redone again. Empty commands are discarded.
|
||||
*/
|
||||
void push_pre_executed(UndoCommand *command, const std::string &name);
|
||||
|
||||
void jump(size_t index);
|
||||
|
||||
void clear();
|
||||
|
||||
bool can_undo() const;
|
||||
|
||||
bool can_redo() const
|
||||
{
|
||||
return !undone_commands_.empty();
|
||||
}
|
||||
|
||||
void undo();
|
||||
|
||||
void redo();
|
||||
|
||||
/**
|
||||
* @brief Set the callback fired after every stack mutation with the
|
||||
* current done-command count (replaces the Qt index_changed signal).
|
||||
*/
|
||||
void set_index_changed_callback(std::function<void(int)> callback)
|
||||
{
|
||||
index_changed_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
// Facade accessors (oakengine/undo.h C ABI): row-based history queries.
|
||||
// Rows 0..done_count()-1 are done commands (commands_ in order), rows
|
||||
// done_count()..command_count()-1 are undone commands (undone_commands_
|
||||
// in order, most recently undone first).
|
||||
int command_count() const
|
||||
{
|
||||
return int(commands_.size() + undone_commands_.size());
|
||||
}
|
||||
|
||||
int done_count() const
|
||||
{
|
||||
return int(commands_.size());
|
||||
}
|
||||
|
||||
bool command_is_done(int row) const
|
||||
{
|
||||
return row >= 0 && row < done_count();
|
||||
}
|
||||
|
||||
std::string command_name(int row) const
|
||||
{
|
||||
if (row < 0 || row >= command_count()) {
|
||||
return std::string();
|
||||
}
|
||||
if (row < done_count()) {
|
||||
auto it = commands_.begin();
|
||||
std::advance(it, row);
|
||||
return it->name;
|
||||
}
|
||||
auto it = undone_commands_.begin();
|
||||
std::advance(it, row - done_count());
|
||||
return it->name;
|
||||
}
|
||||
|
||||
private:
|
||||
static const int k_max_undo_commands;
|
||||
|
||||
struct CommandEntry {
|
||||
UndoCommand *command;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
void emit_index_changed();
|
||||
|
||||
std::list<CommandEntry> commands_;
|
||||
|
||||
std::list<CommandEntry> undone_commands_;
|
||||
|
||||
std::function<void(int)> index_changed_callback_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_UNDOSTACK_H
|
||||
@@ -0,0 +1,40 @@
|
||||
# Oak Video Editor - Non-Linear Video Editor
|
||||
# Copyright (C) 2026 Oak Team
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# Standalone build driver for the oakundo module. oakundo depends only on
|
||||
# oakcommon's header-only define.h macros (pulled in by include path, no
|
||||
# linking), so this builds just oakundo and its tests.
|
||||
#
|
||||
# Usage (macOS/Homebrew):
|
||||
# cmake -S src/undo/standalone -B build-oakundo
|
||||
# cmake --build build-oakundo -j
|
||||
# ctest --test-dir build-oakundo
|
||||
|
||||
cmake_minimum_required(VERSION 3.16 FATAL_ERROR)
|
||||
|
||||
project(oakundo-standalone LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
get_filename_component(OAK_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${OAK_REPO_ROOT}/cmake")
|
||||
if(EXISTS "/opt/homebrew")
|
||||
list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew")
|
||||
endif()
|
||||
|
||||
set(BUILD_TESTS ON)
|
||||
enable_testing()
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo)
|
||||
@@ -0,0 +1,28 @@
|
||||
# Oak Video Editor - Non-Linear Video Editor
|
||||
# Copyright (C) 2026 Oak Team
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
find_package(GTest REQUIRED)
|
||||
include(GoogleTest)
|
||||
|
||||
add_executable(oakundo-gtest
|
||||
undocommand_test.cpp
|
||||
undostack_test.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(oakundo-gtest PRIVATE
|
||||
oakundo
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
gtest_discover_tests(oakundo-gtest)
|
||||
@@ -0,0 +1,202 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "undo/undocommand.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct CommandLog {
|
||||
int redo_count = 0;
|
||||
int undo_count = 0;
|
||||
int free_count = 0;
|
||||
};
|
||||
|
||||
OakUndoCommandVtable make_vtable()
|
||||
{
|
||||
OakUndoCommandVtable vtable;
|
||||
vtable.redo = [](void *userdata) {
|
||||
static_cast<CommandLog *>(userdata)->redo_count++;
|
||||
};
|
||||
vtable.undo = [](void *userdata) {
|
||||
static_cast<CommandLog *>(userdata)->undo_count++;
|
||||
};
|
||||
vtable.free_fn = [](void *userdata) {
|
||||
static_cast<CommandLog *>(userdata)->free_count++;
|
||||
};
|
||||
return vtable;
|
||||
}
|
||||
|
||||
OakUndoCommand *make_command(CommandLog &log)
|
||||
{
|
||||
OakUndoCommandVtable vtable = make_vtable();
|
||||
OakUndoCommand *command = oakundo_command_init(&vtable, &log);
|
||||
EXPECT_NE(command, nullptr);
|
||||
return command;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST(OakUndoCommand, InitFree)
|
||||
{
|
||||
CommandLog log;
|
||||
OakUndoCommand *command = make_command(log);
|
||||
oakundo_command_free(command);
|
||||
EXPECT_EQ(log.free_count, 1);
|
||||
}
|
||||
|
||||
TEST(OakUndoCommand, InitNullVtableFails)
|
||||
{
|
||||
EXPECT_EQ(oakundo_command_init(nullptr, nullptr), nullptr);
|
||||
}
|
||||
|
||||
TEST(OakUndoCommand, FreeNullIsNoOp)
|
||||
{
|
||||
oakundo_command_free(nullptr);
|
||||
}
|
||||
|
||||
TEST(OakUndoCommand, RedoUndoNowRoundtrip)
|
||||
{
|
||||
CommandLog log;
|
||||
OakUndoCommand *command = make_command(log);
|
||||
|
||||
EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK);
|
||||
EXPECT_EQ(log.redo_count, 1);
|
||||
// redo_now on a done command is a no-op
|
||||
EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK);
|
||||
EXPECT_EQ(log.redo_count, 1);
|
||||
|
||||
EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK);
|
||||
EXPECT_EQ(log.undo_count, 1);
|
||||
// undo_now on an undone command is a no-op
|
||||
EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK);
|
||||
EXPECT_EQ(log.undo_count, 1);
|
||||
|
||||
oakundo_command_free(command);
|
||||
}
|
||||
|
||||
TEST(OakUndoCommand, RedoNowNullHandleFails)
|
||||
{
|
||||
EXPECT_EQ(oakundo_command_redo_now(nullptr), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_command_undo_now(nullptr), OAKUNDO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakUndoCommand, NullCallbacksAreNoOp)
|
||||
{
|
||||
OakUndoCommandVtable vtable;
|
||||
vtable.redo = nullptr;
|
||||
vtable.undo = nullptr;
|
||||
vtable.free_fn = nullptr;
|
||||
|
||||
OakUndoCommand *command = oakundo_command_init(&vtable, nullptr);
|
||||
ASSERT_NE(command, nullptr);
|
||||
EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK);
|
||||
EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK);
|
||||
oakundo_command_free(command);
|
||||
}
|
||||
|
||||
TEST(OakUndoCommand, MultiAddChildAndCount)
|
||||
{
|
||||
CommandLog log_a;
|
||||
CommandLog log_b;
|
||||
OakUndoCommand *multi = oakundo_command_init_multi();
|
||||
ASSERT_NE(multi, nullptr);
|
||||
|
||||
int count = -1;
|
||||
EXPECT_EQ(oakundo_command_multi_child_count(multi, &count), OAKUNDO_OK);
|
||||
EXPECT_EQ(count, 0);
|
||||
|
||||
EXPECT_EQ(oakundo_command_multi_add_child(multi, make_command(log_a)),
|
||||
OAKUNDO_OK);
|
||||
EXPECT_EQ(oakundo_command_multi_add_child(multi, make_command(log_b)),
|
||||
OAKUNDO_OK);
|
||||
|
||||
EXPECT_EQ(oakundo_command_multi_child_count(multi, &count), OAKUNDO_OK);
|
||||
EXPECT_EQ(count, 2);
|
||||
|
||||
// Multi redo/undo forward to children.
|
||||
EXPECT_EQ(oakundo_command_redo_now(multi), OAKUNDO_OK);
|
||||
EXPECT_EQ(log_a.redo_count, 1);
|
||||
EXPECT_EQ(log_b.redo_count, 1);
|
||||
EXPECT_EQ(oakundo_command_undo_now(multi), OAKUNDO_OK);
|
||||
EXPECT_EQ(log_a.undo_count, 1);
|
||||
EXPECT_EQ(log_b.undo_count, 1);
|
||||
|
||||
// Freeing the multi frees the children it owns.
|
||||
oakundo_command_free(multi);
|
||||
EXPECT_EQ(log_a.free_count, 1);
|
||||
EXPECT_EQ(log_b.free_count, 1);
|
||||
}
|
||||
|
||||
TEST(OakUndoCommand, MultiChildBorrowedHandle)
|
||||
{
|
||||
CommandLog log;
|
||||
OakUndoCommand *multi = oakundo_command_init_multi();
|
||||
ASSERT_NE(multi, nullptr);
|
||||
ASSERT_EQ(oakundo_command_multi_add_child(multi, make_command(log)),
|
||||
OAKUNDO_OK);
|
||||
|
||||
OakUndoCommand *child = nullptr;
|
||||
EXPECT_EQ(oakundo_command_multi_child(multi, 0, &child), OAKUNDO_OK);
|
||||
ASSERT_NE(child, nullptr);
|
||||
EXPECT_EQ(oakundo_command_redo_now(child), OAKUNDO_OK);
|
||||
EXPECT_EQ(log.redo_count, 1);
|
||||
|
||||
// Freeing the borrowed wrapper must not free the underlying command.
|
||||
oakundo_command_free(child);
|
||||
EXPECT_EQ(log.free_count, 0);
|
||||
|
||||
oakundo_command_free(multi);
|
||||
EXPECT_EQ(log.free_count, 1);
|
||||
}
|
||||
|
||||
TEST(OakUndoCommand, MultiErrorPaths)
|
||||
{
|
||||
CommandLog log;
|
||||
OakUndoCommand *multi = oakundo_command_init_multi();
|
||||
ASSERT_NE(multi, nullptr);
|
||||
OakUndoCommand *plain = make_command(log);
|
||||
|
||||
int count = 0;
|
||||
OakUndoCommand *child = nullptr;
|
||||
|
||||
EXPECT_EQ(oakundo_command_multi_add_child(nullptr, plain),
|
||||
OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_command_multi_add_child(multi, nullptr),
|
||||
OAKUNDO_E_INVALID);
|
||||
// plain is not a multi command
|
||||
EXPECT_EQ(oakundo_command_multi_add_child(plain, make_command(log)),
|
||||
OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_command_multi_child_count(nullptr, &count),
|
||||
OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_command_multi_child_count(multi, nullptr),
|
||||
OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_command_multi_child(multi, 0, nullptr),
|
||||
OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_command_multi_child(multi, 5, &child),
|
||||
OAKUNDO_E_NOT_FOUND);
|
||||
EXPECT_EQ(oakundo_command_multi_child(multi, -1, &child),
|
||||
OAKUNDO_E_NOT_FOUND);
|
||||
|
||||
oakundo_command_free(multi);
|
||||
oakundo_command_free(plain);
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "undo/undostack.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "../src/undostack.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct CommandLog {
|
||||
int redo_count = 0;
|
||||
int undo_count = 0;
|
||||
int free_count = 0;
|
||||
};
|
||||
|
||||
OakUndoCommandVtable make_vtable()
|
||||
{
|
||||
OakUndoCommandVtable vtable;
|
||||
vtable.redo = [](void *userdata) {
|
||||
static_cast<CommandLog *>(userdata)->redo_count++;
|
||||
};
|
||||
vtable.undo = [](void *userdata) {
|
||||
static_cast<CommandLog *>(userdata)->undo_count++;
|
||||
};
|
||||
vtable.free_fn = [](void *userdata) {
|
||||
static_cast<CommandLog *>(userdata)->free_count++;
|
||||
};
|
||||
return vtable;
|
||||
}
|
||||
|
||||
OakUndoCommand *make_command(CommandLog &log)
|
||||
{
|
||||
OakUndoCommandVtable vtable = make_vtable();
|
||||
OakUndoCommand *command = oakundo_command_init(&vtable, &log);
|
||||
EXPECT_NE(command, nullptr);
|
||||
return command;
|
||||
}
|
||||
|
||||
int64_t stack_index(OakUndoStack *stack)
|
||||
{
|
||||
int64_t index = -1;
|
||||
EXPECT_EQ(oakundo_undostack_index(stack, &index), OAKUNDO_OK);
|
||||
return index;
|
||||
}
|
||||
|
||||
int64_t stack_count(OakUndoStack *stack)
|
||||
{
|
||||
int64_t count = -1;
|
||||
EXPECT_EQ(oakundo_undostack_count(stack, &count), OAKUNDO_OK);
|
||||
return count;
|
||||
}
|
||||
|
||||
int stack_can_undo(OakUndoStack *stack)
|
||||
{
|
||||
int value = -1;
|
||||
EXPECT_EQ(oakundo_undostack_can_undo(stack, &value), OAKUNDO_OK);
|
||||
return value;
|
||||
}
|
||||
|
||||
int stack_can_redo(OakUndoStack *stack)
|
||||
{
|
||||
int value = -1;
|
||||
EXPECT_EQ(oakundo_undostack_can_redo(stack, &value), OAKUNDO_OK);
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, InitFree)
|
||||
{
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
|
||||
// A fresh stack holds the "New/Open Project" empty command.
|
||||
EXPECT_EQ(stack_count(stack), 1);
|
||||
EXPECT_EQ(stack_index(stack), 1);
|
||||
EXPECT_EQ(stack_can_undo(stack), 0);
|
||||
EXPECT_EQ(stack_can_redo(stack), 0);
|
||||
|
||||
oakundo_undostack_free(stack);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, FreeNullIsNoOp)
|
||||
{
|
||||
oakundo_undostack_free(nullptr);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, NullHandleFails)
|
||||
{
|
||||
CommandLog log;
|
||||
int64_t value64 = 0;
|
||||
int value = 0;
|
||||
char buf[8];
|
||||
|
||||
EXPECT_EQ(oakundo_undostack_push(nullptr, make_command(log), "x"),
|
||||
OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_push_pre_executed(nullptr, make_command(log),
|
||||
"x"),
|
||||
OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_undo(nullptr), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_redo(nullptr), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_jump(nullptr, 0), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_clear(nullptr), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_can_undo(nullptr, &value), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_can_redo(nullptr, &value), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_count(nullptr, &value64), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_index(nullptr, &value64), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_command_text(nullptr, 0, buf, sizeof(buf)),
|
||||
OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_command_is_done(nullptr, 0, &value),
|
||||
OAKUNDO_E_INVALID);
|
||||
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
EXPECT_EQ(oakundo_undostack_push(stack, nullptr, "x"), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_push_pre_executed(stack, nullptr, "x"),
|
||||
OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_can_undo(stack, nullptr), OAKUNDO_E_INVALID);
|
||||
EXPECT_EQ(oakundo_undostack_count(stack, nullptr), OAKUNDO_E_INVALID);
|
||||
oakundo_undostack_free(stack);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, PushUndoRedoRoundtrip)
|
||||
{
|
||||
CommandLog log;
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
|
||||
ASSERT_EQ(oakundo_undostack_push(stack, make_command(log), "edit"),
|
||||
OAKUNDO_OK);
|
||||
EXPECT_EQ(log.redo_count, 1);
|
||||
EXPECT_EQ(stack_count(stack), 2);
|
||||
EXPECT_EQ(stack_index(stack), 2);
|
||||
EXPECT_EQ(stack_can_undo(stack), 1);
|
||||
EXPECT_EQ(stack_can_redo(stack), 0);
|
||||
|
||||
ASSERT_EQ(oakundo_undostack_undo(stack), OAKUNDO_OK);
|
||||
EXPECT_EQ(log.undo_count, 1);
|
||||
EXPECT_EQ(stack_index(stack), 1);
|
||||
EXPECT_EQ(stack_can_undo(stack), 0);
|
||||
EXPECT_EQ(stack_can_redo(stack), 1);
|
||||
|
||||
ASSERT_EQ(oakundo_undostack_redo(stack), OAKUNDO_OK);
|
||||
EXPECT_EQ(log.redo_count, 2);
|
||||
EXPECT_EQ(stack_index(stack), 2);
|
||||
EXPECT_EQ(stack_can_undo(stack), 1);
|
||||
EXPECT_EQ(stack_can_redo(stack), 0);
|
||||
|
||||
oakundo_undostack_free(stack);
|
||||
EXPECT_EQ(log.free_count, 1);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, PushClearsRedoable)
|
||||
{
|
||||
CommandLog log_a;
|
||||
CommandLog log_b;
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
|
||||
ASSERT_EQ(oakundo_undostack_push(stack, make_command(log_a), "a"),
|
||||
OAKUNDO_OK);
|
||||
ASSERT_EQ(oakundo_undostack_undo(stack), OAKUNDO_OK);
|
||||
ASSERT_EQ(stack_can_redo(stack), 1);
|
||||
|
||||
// Pushing a new command deletes the redoable one.
|
||||
ASSERT_EQ(oakundo_undostack_push(stack, make_command(log_b), "b"),
|
||||
OAKUNDO_OK);
|
||||
EXPECT_EQ(log_a.free_count, 1);
|
||||
EXPECT_EQ(stack_can_redo(stack), 0);
|
||||
EXPECT_EQ(stack_count(stack), 2);
|
||||
|
||||
oakundo_undostack_free(stack);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, PushPreExecutedSkipsRedo)
|
||||
{
|
||||
CommandLog log;
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
|
||||
ASSERT_EQ(oakundo_undostack_push_pre_executed(stack, make_command(log),
|
||||
"done elsewhere"),
|
||||
OAKUNDO_OK);
|
||||
EXPECT_EQ(log.redo_count, 0);
|
||||
EXPECT_EQ(stack_index(stack), 2);
|
||||
|
||||
// Undo and redo still work afterwards.
|
||||
ASSERT_EQ(oakundo_undostack_undo(stack), OAKUNDO_OK);
|
||||
EXPECT_EQ(log.undo_count, 1);
|
||||
ASSERT_EQ(oakundo_undostack_redo(stack), OAKUNDO_OK);
|
||||
EXPECT_EQ(log.redo_count, 1);
|
||||
|
||||
oakundo_undostack_free(stack);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, EmptyMultiIsDiscarded)
|
||||
{
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
|
||||
OakUndoCommand *multi = oakundo_command_init_multi();
|
||||
ASSERT_NE(multi, nullptr);
|
||||
ASSERT_EQ(oakundo_undostack_push(stack, multi, "empty"), OAKUNDO_OK);
|
||||
EXPECT_EQ(stack_count(stack), 1);
|
||||
|
||||
multi = oakundo_command_init_multi();
|
||||
ASSERT_NE(multi, nullptr);
|
||||
ASSERT_EQ(oakundo_undostack_push_pre_executed(stack, multi, "empty"),
|
||||
OAKUNDO_OK);
|
||||
EXPECT_EQ(stack_count(stack), 1);
|
||||
|
||||
oakundo_undostack_free(stack);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, Jump)
|
||||
{
|
||||
CommandLog logs[3];
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
char name[8];
|
||||
snprintf(name, sizeof(name), "cmd%d", i);
|
||||
ASSERT_EQ(oakundo_undostack_push(stack, make_command(logs[i]), name),
|
||||
OAKUNDO_OK);
|
||||
}
|
||||
ASSERT_EQ(stack_index(stack), 4);
|
||||
|
||||
// Jump back to 1: undo all three commands.
|
||||
ASSERT_EQ(oakundo_undostack_jump(stack, 1), OAKUNDO_OK);
|
||||
EXPECT_EQ(stack_index(stack), 1);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
EXPECT_EQ(logs[i].undo_count, 1);
|
||||
}
|
||||
EXPECT_EQ(stack_can_redo(stack), 1);
|
||||
|
||||
// Jump forward to 3: redo the first two undone commands.
|
||||
ASSERT_EQ(oakundo_undostack_jump(stack, 3), OAKUNDO_OK);
|
||||
EXPECT_EQ(stack_index(stack), 3);
|
||||
EXPECT_EQ(logs[0].redo_count, 2);
|
||||
EXPECT_EQ(logs[1].redo_count, 2);
|
||||
EXPECT_EQ(logs[2].redo_count, 1);
|
||||
|
||||
// Negative index clamps to 0; the bottom "New/Open Project" empty
|
||||
// command is not undoable, so the jump stops at index 1.
|
||||
ASSERT_EQ(oakundo_undostack_jump(stack, -5), OAKUNDO_OK);
|
||||
EXPECT_EQ(stack_index(stack), 1);
|
||||
|
||||
oakundo_undostack_free(stack);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, Clear)
|
||||
{
|
||||
CommandLog log;
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
|
||||
ASSERT_EQ(oakundo_undostack_push(stack, make_command(log), "edit"),
|
||||
OAKUNDO_OK);
|
||||
ASSERT_EQ(oakundo_undostack_clear(stack), OAKUNDO_OK);
|
||||
|
||||
EXPECT_EQ(log.free_count, 1);
|
||||
EXPECT_EQ(stack_count(stack), 1);
|
||||
EXPECT_EQ(stack_can_undo(stack), 0);
|
||||
EXPECT_EQ(stack_can_redo(stack), 0);
|
||||
|
||||
char buf[32];
|
||||
ASSERT_EQ(oakundo_undostack_command_text(stack, 0, buf, sizeof(buf)),
|
||||
int(strlen("New/Open Project")) + 1);
|
||||
EXPECT_STREQ(buf, "New/Open Project");
|
||||
|
||||
oakundo_undostack_free(stack);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, CommandTextTwoStage)
|
||||
{
|
||||
CommandLog log;
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
|
||||
ASSERT_EQ(oakundo_undostack_push(stack, make_command(log), "hello"),
|
||||
OAKUNDO_OK);
|
||||
|
||||
// Stage one: query the required size.
|
||||
int required = oakundo_undostack_command_text(stack, 1, nullptr, 0);
|
||||
EXPECT_EQ(required, int(strlen("hello")) + 1);
|
||||
|
||||
// Stage two: copy into an exact buffer.
|
||||
std::vector<char> buf(required);
|
||||
EXPECT_EQ(oakundo_undostack_command_text(stack, 1, buf.data(), required),
|
||||
required);
|
||||
EXPECT_STREQ(buf.data(), "hello");
|
||||
|
||||
// Truncating buffer still reports the full required size.
|
||||
char small[3];
|
||||
EXPECT_EQ(oakundo_undostack_command_text(stack, 1, small, sizeof(small)),
|
||||
required);
|
||||
EXPECT_EQ(small[sizeof(small) - 1], '\0');
|
||||
|
||||
// Invalid rows.
|
||||
EXPECT_EQ(oakundo_undostack_command_text(stack, 2, nullptr, 0),
|
||||
OAKUNDO_E_NOT_FOUND);
|
||||
EXPECT_EQ(oakundo_undostack_command_text(stack, -1, nullptr, 0),
|
||||
OAKUNDO_E_NOT_FOUND);
|
||||
|
||||
// NULL name behaves like an empty label.
|
||||
ASSERT_EQ(oakundo_undostack_push(stack, make_command(log), nullptr),
|
||||
OAKUNDO_OK);
|
||||
EXPECT_EQ(oakundo_undostack_command_text(stack, 2, nullptr, 0), 1);
|
||||
|
||||
oakundo_undostack_free(stack);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, CommandIsDone)
|
||||
{
|
||||
CommandLog log;
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
|
||||
ASSERT_EQ(oakundo_undostack_push(stack, make_command(log), "edit"),
|
||||
OAKUNDO_OK);
|
||||
ASSERT_EQ(oakundo_undostack_undo(stack), OAKUNDO_OK);
|
||||
|
||||
int value = -1;
|
||||
EXPECT_EQ(oakundo_undostack_command_is_done(stack, 0, &value), OAKUNDO_OK);
|
||||
EXPECT_EQ(value, 1);
|
||||
EXPECT_EQ(oakundo_undostack_command_is_done(stack, 1, &value), OAKUNDO_OK);
|
||||
EXPECT_EQ(value, 0);
|
||||
|
||||
EXPECT_EQ(oakundo_undostack_command_is_done(stack, 2, &value),
|
||||
OAKUNDO_E_NOT_FOUND);
|
||||
EXPECT_EQ(oakundo_undostack_command_is_done(stack, -1, &value),
|
||||
OAKUNDO_E_NOT_FOUND);
|
||||
EXPECT_EQ(oakundo_undostack_command_is_done(stack, 0, nullptr),
|
||||
OAKUNDO_E_INVALID);
|
||||
|
||||
oakundo_undostack_free(stack);
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, MaxUndoCommands)
|
||||
{
|
||||
OakUndoStack *stack = oakundo_undostack_init();
|
||||
ASSERT_NE(stack, nullptr);
|
||||
|
||||
// The fresh stack holds 1 empty command; push well past the 200 cap.
|
||||
std::vector<std::unique_ptr<CommandLog>> logs;
|
||||
for (int i = 0; i < 250; i++) {
|
||||
logs.emplace_back(new CommandLog());
|
||||
ASSERT_EQ(oakundo_undostack_push(stack, make_command(*logs.back()),
|
||||
"edit"),
|
||||
OAKUNDO_OK);
|
||||
}
|
||||
|
||||
EXPECT_EQ(stack_count(stack), 200);
|
||||
EXPECT_EQ(stack_index(stack), 200);
|
||||
// The empty "New/Open Project" command was evicted, so everything left
|
||||
// is undoable.
|
||||
EXPECT_EQ(stack_can_undo(stack), 1);
|
||||
|
||||
// The 51 oldest commands (empty + first 50 pushed) were freed.
|
||||
int freed = 0;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
freed += logs[i]->free_count;
|
||||
}
|
||||
EXPECT_EQ(freed, 50);
|
||||
|
||||
// Undo all 200 entries: the bottom one is not an EmptyCommand.
|
||||
for (int i = 0; i < 200; i++) {
|
||||
EXPECT_EQ(stack_can_undo(stack), 1);
|
||||
ASSERT_EQ(oakundo_undostack_undo(stack), OAKUNDO_OK);
|
||||
}
|
||||
EXPECT_EQ(stack_can_undo(stack), 0);
|
||||
EXPECT_EQ(stack_count(stack), 200);
|
||||
|
||||
oakundo_undostack_free(stack);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class FakeCommand : public olive::UndoCommand {
|
||||
public:
|
||||
FakeCommand(int *redo_count, int *undo_count)
|
||||
: redo_count_(redo_count), undo_count_(undo_count)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override
|
||||
{
|
||||
(*redo_count_)++;
|
||||
}
|
||||
virtual void undo() override
|
||||
{
|
||||
(*undo_count_)++;
|
||||
}
|
||||
|
||||
private:
|
||||
int *redo_count_;
|
||||
int *undo_count_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
TEST(OakUndoStack, CppIndexChangedCallback)
|
||||
{
|
||||
olive::UndoStack stack;
|
||||
|
||||
std::vector<int> notifications;
|
||||
stack.set_index_changed_callback(
|
||||
[¬ifications](int index) { notifications.push_back(index); });
|
||||
|
||||
// The callback only fires on mutations after it is attached.
|
||||
int redo_count = 0;
|
||||
int undo_count = 0;
|
||||
stack.push(new FakeCommand(&redo_count, &undo_count), "fake");
|
||||
ASSERT_EQ(notifications.size(), 1u);
|
||||
EXPECT_EQ(notifications.back(), 2);
|
||||
|
||||
stack.undo();
|
||||
ASSERT_EQ(notifications.size(), 2u);
|
||||
EXPECT_EQ(notifications.back(), 1);
|
||||
|
||||
stack.redo();
|
||||
EXPECT_EQ(notifications.back(), 2);
|
||||
|
||||
// The bottom EmptyCommand is not undoable: jump(0) stops at index 1.
|
||||
stack.jump(0);
|
||||
EXPECT_EQ(notifications.back(), 1);
|
||||
}
|
||||
Reference in New Issue
Block a user