Add undo2 library

This commit is contained in:
David Capello 2015-01-04 20:23:05 -03:00
parent e40d8e8cfe
commit a9fa9f5fdc
10 changed files with 526 additions and 0 deletions

View File

@ -31,6 +31,7 @@ set(aseprite_libraries
render-lib
scripting-lib
undo-lib
undo2-lib
filters-lib
ui-lib
she
@ -220,6 +221,7 @@ add_subdirectory(scripting)
add_subdirectory(she)
add_subdirectory(ui)
add_subdirectory(undo)
add_subdirectory(undo2)
add_subdirectory(app)
@ -328,6 +330,7 @@ function(find_tests dir dependencies)
endfunction()
find_tests(base base-lib ${sys_libs})
find_tests(undo2 undo2-lib ${sys_libs})
find_tests(gfx gfx-lib base-lib ${libs3rdparty} ${sys_libs})
find_tests(doc doc-lib gfx-lib base-lib ${libs3rdparty} ${sys_libs})
find_tests(render render-lib doc-lib gfx-lib base-lib ${libs3rdparty} ${sys_libs})

View File

@ -19,6 +19,7 @@ because they don't depend on any other component.
* [gfx](gfx/): Abstract graphics structures like point, size, rectangle, region, color, etc.
* [scripting](scripting/): JavaScript engine ([V8](https://code.google.com/p/v8/)).
* [undo](undo/): Generic library to manage undo history of undoable actions.
* [undo2](undo2/): New library to replace the old undo system.
## Level 1

5
src/undo2/CMakeLists.txt Normal file
View File

@ -0,0 +1,5 @@
# Aseprite Undo2 Library
# Copyright (C) 2015 David Capello
add_library(undo2-lib
undo_history.cpp)

20
src/undo2/LICENSE.txt Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2015 David Capello
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

4
src/undo2/README.md Normal file
View File

@ -0,0 +1,4 @@
# Aseprite Undo2 Library
*Copyright (C) 2015 David Capello*
> Distributed under [MIT license](LICENSE.txt)

22
src/undo2/undo_command.h Normal file
View File

@ -0,0 +1,22 @@
// Aseprite Undo2 Library
// Copyright (C) 2015 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef UNDO2_UNDO_COMMAND_H_INCLUDED
#define UNDO2_UNDO_COMMAND_H_INCLUDED
#pragma once
namespace undo2 {
class UndoCommand {
public:
virtual ~UndoCommand() { }
virtual void undo() = 0;
virtual void redo() = 0;
};
} // namespace undo2
#endif // UNDO2_UNDO_COMMAND_H_INCLUDED

162
src/undo2/undo_history.cpp Normal file
View File

@ -0,0 +1,162 @@
// Aseprite Undo2 Library
// Copyright (C) 2015 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "undo2/undo_history.h"
#include "undo2/undo_command.h"
#include "undo2/undo_state.h"
#include <cassert>
#include <stack>
namespace undo2 {
UndoHistory::UndoHistory()
: m_first(nullptr)
, m_last(nullptr)
, m_cur(nullptr)
, m_createBranches(true)
{
}
UndoHistory::~UndoHistory()
{
m_cur = nullptr;
clearRedo();
}
bool UndoHistory::canUndo() const
{
return m_cur != nullptr;
}
bool UndoHistory::canRedo() const
{
return m_cur != m_last;
}
void UndoHistory::undo()
{
assert(m_cur);
if (!m_cur)
return;
assert(
(m_cur != m_first && m_cur->m_prev) ||
(m_cur == m_first && !m_cur->m_prev));
moveTo(m_cur->m_prev);
}
void UndoHistory::redo()
{
if (!m_cur)
moveTo(m_first);
else
moveTo(m_cur->m_next);
}
void UndoHistory::setCreateBranches(bool state)
{
m_createBranches = false;
}
void UndoHistory::clearRedo()
{
for (UndoState* state = m_last, *prev;
state && state != m_cur;
state = prev) {
prev = state->m_prev;
delete state;
}
if (m_cur) {
m_cur->m_next = nullptr;
m_last = m_cur;
}
else {
m_first = m_last = nullptr;
}
}
void UndoHistory::add(UndoCommand* cmd)
{
if (!m_createBranches)
clearRedo();
UndoState* state = new UndoState;
state->m_prev = m_last;
state->m_next = nullptr;
state->m_parent = m_cur;
state->m_cmd = cmd;
if (!m_first)
m_first = state;
m_cur = m_last = state;
if (state->m_prev) {
assert(!state->m_prev->m_next);
state->m_prev->m_next = state;
}
}
UndoState* UndoHistory::findCommonParent(UndoState* a, UndoState* b)
{
UndoState* pA = a;
UndoState* pB = b;
if (pA == nullptr || pB == nullptr)
return nullptr;
while (pA != pB) {
pA = pA->m_parent;
if (!pA) {
pA = a;
pB = pB->m_parent;
if (!pB)
return nullptr;
}
}
return pA;
}
void UndoHistory::moveTo(UndoState* new_state)
{
UndoState* common = findCommonParent(m_cur, new_state);
if (m_cur) {
while (m_cur != common) {
m_cur->m_cmd->undo();
m_cur = m_cur->m_parent;
}
}
if (new_state) {
std::stack<UndoState*> redo_parents;
UndoState* p = new_state;
while (p != common) {
redo_parents.push(p);
p = p->m_parent;
}
while (!redo_parents.empty()) {
p = redo_parents.top();
redo_parents.pop();
p->m_cmd->redo();
}
}
m_cur = new_state;
}
} // namespace undo

50
src/undo2/undo_history.h Normal file
View File

@ -0,0 +1,50 @@
// Aseprite Undo2 Library
// Copyright (C) 2015 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef UNDO2_UNDO_HISTORY_H_INCLUDED
#define UNDO2_UNDO_HISTORY_H_INCLUDED
#pragma once
namespace undo2 {
class UndoCommand;
class UndoState;
class UndoHistory {
public:
UndoHistory();
virtual ~UndoHistory();
const UndoState* firstState() const { return m_first; }
const UndoState* lastState() const { return m_last; }
const UndoState* currentState() const { return m_cur; }
void add(UndoCommand* cmd);
bool canUndo() const;
bool canRedo() const;
void undo();
void redo();
// By default, UndoHistory creates branches if we undo a command
// and call add(). With this method we can disable this behavior
// and call clearRedo() automatically when a new command is added
// in the history. (Like a regular undo history works.)
void setCreateBranches(bool state);
void clearRedo();
private:
UndoState* findCommonParent(UndoState* a, UndoState* b);
void moveTo(UndoState* new_state);
UndoState* m_first;
UndoState* m_last;
UndoState* m_cur; // Current action that can be undone
bool m_createBranches;
};
} // namespace undo2
#endif // UNDO2_UNDO_HISTORY_H_INCLUDED

33
src/undo2/undo_state.h Normal file
View File

@ -0,0 +1,33 @@
// Aseprite Undo2 Library
// Copyright (C) 2015 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef UNDO2_UNDO_STATE_H_INCLUDED
#define UNDO2_UNDO_STATE_H_INCLUDED
#pragma once
namespace undo2 {
class UndoCommand;
class UndoHistory;
// Represents a state that can be undone. If we are in this state,
// is because the command was already executed.
class UndoState {
friend class UndoHistory;
public:
UndoState* prev() { return m_prev; }
UndoState* next() { return m_next; }
UndoCommand* cmd() { return m_cmd; }
private:
UndoState* m_prev;
UndoState* m_next;
UndoState* m_parent; // Parent state, after we undo
UndoCommand* m_cmd;
};
} // namespace undo2
#endif // UNDO2_UNDO_STATE_H_INCLUDED

226
src/undo2/undo_tests.cpp Normal file
View File

@ -0,0 +1,226 @@
// Aseprite Undo2 Library
// Copyright (C) 2015 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <functional>
#include <gtest/gtest.h>
#include "undo2/undo_command.h"
#include "undo2/undo_history.h"
using namespace undo2;
class Cmd : public UndoCommand {
public:
template<typename UndoT, typename RedoT>
Cmd(RedoT redoFunc, UndoT undoFunc)
: m_redo(redoFunc), m_undo(undoFunc) {
}
void redo() override { m_redo(); }
void undo() override { m_undo(); }
private:
std::function<void()> m_redo;
std::function<void()> m_undo;
};
TEST(Undo, Basics)
{
UndoHistory history;
int model = 0;
EXPECT_EQ(0, model);
Cmd cmd1(
[&]{ model = 1; }, // redo
[&]{ model = 0; }); // undo
Cmd cmd2(
[&]{ model = 2; }, // redo
[&]{ model = 1; }); // undo
EXPECT_EQ(0, model);
cmd1.redo();
EXPECT_EQ(1, model);
cmd2.redo();
EXPECT_EQ(2, model);
EXPECT_FALSE(history.canUndo());
EXPECT_FALSE(history.canRedo());
history.add(&cmd1);
EXPECT_TRUE(history.canUndo());
EXPECT_FALSE(history.canRedo());
history.add(&cmd2);
EXPECT_TRUE(history.canUndo());
EXPECT_FALSE(history.canRedo());
history.undo();
EXPECT_EQ(1, model);
EXPECT_TRUE(history.canUndo());
EXPECT_TRUE(history.canRedo());
history.undo();
EXPECT_EQ(0, model);
EXPECT_FALSE(history.canUndo());
EXPECT_TRUE(history.canRedo());
history.redo();
EXPECT_EQ(1, model);
EXPECT_TRUE(history.canUndo());
EXPECT_TRUE(history.canRedo());
history.redo();
EXPECT_EQ(2, model);
EXPECT_TRUE(history.canUndo());
EXPECT_FALSE(history.canRedo());
}
TEST(Undo, Linear)
{
UndoHistory history;
history.setCreateBranches(false);
int model = 0;
EXPECT_EQ(0, model);
// 1 --- 3 --- 4
Cmd cmd1([&]{ model = 1; }, [&]{ model = 0; });
Cmd cmd2([&]{ model = 2; }, [&]{ model = 1; });
Cmd cmd3([&]{ model = 3; }, [&]{ model = 1; });
Cmd cmd4([&]{ model = 4; }, [&]{ model = 3; });
cmd1.redo(); history.add(&cmd1);
cmd2.redo(); history.add(&cmd2);
history.undo();
cmd3.redo(); history.add(&cmd3); // Removes state 2
cmd4.redo(); history.add(&cmd4);
EXPECT_EQ(4, model);
history.undo();
EXPECT_EQ(3, model);
history.undo();
EXPECT_EQ(1, model);
history.undo();
EXPECT_EQ(0, model);
EXPECT_FALSE(history.canUndo());
history.redo();
EXPECT_EQ(1, model);
history.redo();
EXPECT_EQ(3, model);
history.redo();
EXPECT_EQ(4, model);
EXPECT_FALSE(history.canRedo());
}
TEST(Undo, Tree)
{
UndoHistory history;
int model = 0;
// 1 --- 2
// \
// ------ 3 --- 4
Cmd cmd1([&]{ model = 1; }, [&]{ model = 0; });
Cmd cmd2([&]{ model = 2; }, [&]{ model = 1; });
Cmd cmd3([&]{ model = 3; }, [&]{ model = 1; });
Cmd cmd4([&]{ model = 4; }, [&]{ model = 3; });
cmd1.redo(); history.add(&cmd1);
cmd2.redo(); history.add(&cmd2);
history.undo();
cmd3.redo(); history.add(&cmd3); // Creates a branch in the history
cmd4.redo(); history.add(&cmd4);
EXPECT_EQ(4, model);
history.undo();
EXPECT_EQ(3, model);
history.undo();
EXPECT_EQ(2, model);
history.undo();
EXPECT_EQ(1, model);
history.undo();
EXPECT_EQ(0, model);
EXPECT_FALSE(history.canUndo());
history.redo();
EXPECT_EQ(1, model);
history.redo();
EXPECT_EQ(2, model);
history.redo();
EXPECT_EQ(3, model);
history.redo();
EXPECT_EQ(4, model);
EXPECT_FALSE(history.canRedo());
}
TEST(Undo, ComplexTree)
{
UndoHistory history;
int model = 0;
// 1 --- 2 --- 3 --- 4 ------ 7 --- 8
// \ /
// ------------- 5 --- 6
Cmd cmd1([&]{ model = 1; }, [&]{ model = 0; });
Cmd cmd2([&]{ model = 2; }, [&]{ model = 1; });
Cmd cmd3([&]{ model = 3; }, [&]{ model = 2; });
Cmd cmd4([&]{ model = 4; }, [&]{ model = 3; });
Cmd cmd5([&]{ model = 5; }, [&]{ model = 2; });
Cmd cmd6([&]{ model = 6; }, [&]{ model = 5; });
Cmd cmd7([&]{ model = 7; }, [&]{ model = 5; });
Cmd cmd8([&]{ model = 8; }, [&]{ model = 7; });
cmd1.redo(); history.add(&cmd1);
cmd2.redo(); history.add(&cmd2);
cmd3.redo(); history.add(&cmd3);
cmd4.redo(); history.add(&cmd4);
history.undo();
cmd5.redo(); history.add(&cmd5);
cmd6.redo(); history.add(&cmd6);
history.undo();
cmd7.redo(); history.add(&cmd7);
cmd8.redo(); history.add(&cmd8);
EXPECT_EQ(8, model);
history.undo();
EXPECT_EQ(7, model);
history.undo();
EXPECT_EQ(6, model);
history.undo();
EXPECT_EQ(5, model);
history.undo();
EXPECT_EQ(4, model);
history.undo();
EXPECT_EQ(3, model);
history.undo();
EXPECT_EQ(2, model);
history.undo();
EXPECT_EQ(1, model);
history.undo();
EXPECT_EQ(0, model);
EXPECT_FALSE(history.canUndo());
history.redo();
EXPECT_EQ(1, model);
history.redo();
EXPECT_EQ(2, model);
history.redo();
EXPECT_EQ(3, model);
history.redo();
EXPECT_EQ(4, model);
history.redo();
EXPECT_EQ(5, model);
history.redo();
EXPECT_EQ(6, model);
history.redo();
EXPECT_EQ(7, model);
history.redo();
EXPECT_EQ(8, model);
EXPECT_FALSE(history.canRedo());
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}