From a9fa9f5fdcfa21b17e91db4fb1d4598dd12ff199 Mon Sep 17 00:00:00 2001 From: David Capello Date: Sun, 4 Jan 2015 20:23:05 -0300 Subject: [PATCH] Add undo2 library --- src/CMakeLists.txt | 3 + src/README.md | 1 + src/undo2/CMakeLists.txt | 5 + src/undo2/LICENSE.txt | 20 ++++ src/undo2/README.md | 4 + src/undo2/undo_command.h | 22 ++++ src/undo2/undo_history.cpp | 162 ++++++++++++++++++++++++++ src/undo2/undo_history.h | 50 ++++++++ src/undo2/undo_state.h | 33 ++++++ src/undo2/undo_tests.cpp | 226 +++++++++++++++++++++++++++++++++++++ 10 files changed, 526 insertions(+) create mode 100644 src/undo2/CMakeLists.txt create mode 100644 src/undo2/LICENSE.txt create mode 100644 src/undo2/README.md create mode 100644 src/undo2/undo_command.h create mode 100644 src/undo2/undo_history.cpp create mode 100644 src/undo2/undo_history.h create mode 100644 src/undo2/undo_state.h create mode 100644 src/undo2/undo_tests.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 522497e71..9873cc56b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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}) diff --git a/src/README.md b/src/README.md index e5a2b2246..da469ad79 100644 --- a/src/README.md +++ b/src/README.md @@ -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 diff --git a/src/undo2/CMakeLists.txt b/src/undo2/CMakeLists.txt new file mode 100644 index 000000000..425f433d1 --- /dev/null +++ b/src/undo2/CMakeLists.txt @@ -0,0 +1,5 @@ +# Aseprite Undo2 Library +# Copyright (C) 2015 David Capello + +add_library(undo2-lib + undo_history.cpp) diff --git a/src/undo2/LICENSE.txt b/src/undo2/LICENSE.txt new file mode 100644 index 000000000..11f1e5e40 --- /dev/null +++ b/src/undo2/LICENSE.txt @@ -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. diff --git a/src/undo2/README.md b/src/undo2/README.md new file mode 100644 index 000000000..f95a9a3a2 --- /dev/null +++ b/src/undo2/README.md @@ -0,0 +1,4 @@ +# Aseprite Undo2 Library +*Copyright (C) 2015 David Capello* + +> Distributed under [MIT license](LICENSE.txt) diff --git a/src/undo2/undo_command.h b/src/undo2/undo_command.h new file mode 100644 index 000000000..1d58b1801 --- /dev/null +++ b/src/undo2/undo_command.h @@ -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 diff --git a/src/undo2/undo_history.cpp b/src/undo2/undo_history.cpp new file mode 100644 index 000000000..565b3107e --- /dev/null +++ b/src/undo2/undo_history.cpp @@ -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 +#include + +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 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 diff --git a/src/undo2/undo_history.h b/src/undo2/undo_history.h new file mode 100644 index 000000000..71cdf0927 --- /dev/null +++ b/src/undo2/undo_history.h @@ -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 diff --git a/src/undo2/undo_state.h b/src/undo2/undo_state.h new file mode 100644 index 000000000..eaecdb66c --- /dev/null +++ b/src/undo2/undo_state.h @@ -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 diff --git a/src/undo2/undo_tests.cpp b/src/undo2/undo_tests.cpp new file mode 100644 index 000000000..b6985db98 --- /dev/null +++ b/src/undo2/undo_tests.cpp @@ -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 +#include + +#include "undo2/undo_command.h" +#include "undo2/undo_history.h" + +using namespace undo2; + +class Cmd : public UndoCommand { +public: + template + Cmd(RedoT redoFunc, UndoT undoFunc) + : m_redo(redoFunc), m_undo(undoFunc) { + } + void redo() override { m_redo(); } + void undo() override { m_undo(); } +private: + std::function m_redo; + std::function 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(); +}