mirror of
https://github.com/aseprite/aseprite.git
synced 2025-04-16 05:42:32 +00:00
Add undo2 library
This commit is contained in:
parent
e40d8e8cfe
commit
a9fa9f5fdc
@ -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})
|
||||
|
@ -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
5
src/undo2/CMakeLists.txt
Normal 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
20
src/undo2/LICENSE.txt
Normal 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
4
src/undo2/README.md
Normal 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
22
src/undo2/undo_command.h
Normal 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
162
src/undo2/undo_history.cpp
Normal 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
50
src/undo2/undo_history.h
Normal 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
33
src/undo2/undo_state.h
Normal 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
226
src/undo2/undo_tests.cpp
Normal 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();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user