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:
+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