1// Aseprite
2// Copyright (C) 2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/doc_undo.h"
13
14#include "app/app.h"
15#include "app/cmd.h"
16#include "app/cmd_transaction.h"
17#include "app/context.h"
18#include "app/doc_undo_observer.h"
19#include "app/pref/preferences.h"
20#include "base/mem_utils.h"
21#include "undo/undo_history.h"
22#include "undo/undo_state.h"
23
24#include <cassert>
25#include <stdexcept>
26
27#define UNDO_TRACE(...)
28#define STATE_CMD(state) (static_cast<CmdTransaction*>(state->cmd()))
29
30namespace app {
31
32DocUndo::DocUndo()
33 : m_undoHistory(this)
34 , m_ctx(nullptr)
35 , m_totalUndoSize(0)
36 , m_savedCounter(0)
37 , m_savedStateIsLost(false)
38{
39}
40
41void DocUndo::setContext(Context* ctx)
42{
43 m_ctx = ctx;
44}
45
46void DocUndo::add(CmdTransaction* cmd)
47{
48 ASSERT(cmd);
49 UNDO_TRACE("UNDO: Add state <%s> of %s to %s\n",
50 cmd->label().c_str(),
51 base::get_pretty_memory_size(cmd->memSize()).c_str(),
52 base::get_pretty_memory_size(m_totalUndoSize).c_str());
53
54 // A linear undo history is the default behavior
55 if (!App::instance() ||
56 !App::instance()->preferences().undo.allowNonlinearHistory()) {
57 clearRedo();
58 }
59
60 m_undoHistory.add(cmd);
61 m_totalUndoSize += cmd->memSize();
62
63 notify_observers(&DocUndoObserver::onAddUndoState, this);
64 notify_observers(&DocUndoObserver::onTotalUndoSizeChange, this);
65
66 if (App::instance()) {
67 const size_t undoLimitSize =
68 int(App::instance()->preferences().undo.sizeLimit())
69 * 1024 * 1024;
70
71 // If undo limit is 0, it means "no limit", so we ignore the
72 // complete logic to discard undo states.
73 if (undoLimitSize > 0 &&
74 m_totalUndoSize > undoLimitSize) {
75 UNDO_TRACE("UNDO: Reducing undo history from %s to %s\n",
76 base::get_pretty_memory_size(m_totalUndoSize).c_str(),
77 base::get_pretty_memory_size(undoLimitSize).c_str());
78
79 while (m_undoHistory.firstState() &&
80 m_totalUndoSize > undoLimitSize) {
81 if (!m_undoHistory.deleteFirstState())
82 break;
83 }
84 }
85 }
86
87 UNDO_TRACE("UNDO: New undo size %s\n",
88 base::get_pretty_memory_size(m_totalUndoSize).c_str());
89}
90
91bool DocUndo::canUndo() const
92{
93 return m_undoHistory.canUndo();
94}
95
96bool DocUndo::canRedo() const
97{
98 return m_undoHistory.canRedo();
99}
100
101void DocUndo::undo()
102{
103 const size_t oldSize = m_totalUndoSize;
104 {
105 const undo::UndoState* state = nextUndo();
106 ASSERT(state);
107 const Cmd* cmd = STATE_CMD(state);
108 m_totalUndoSize -= cmd->memSize();
109 m_undoHistory.undo();
110 m_totalUndoSize += cmd->memSize();
111 }
112 // This notification could execute a script that modifies the sprite
113 // again (e.g. a script that is listening the "change" event, check
114 // the SpriteEvents class). If the sprite is modified, the "cmd" is
115 // not valid anymore.
116 notify_observers(&DocUndoObserver::onCurrentUndoStateChange, this);
117 if (m_totalUndoSize != oldSize)
118 notify_observers(&DocUndoObserver::onTotalUndoSizeChange, this);
119}
120
121void DocUndo::redo()
122{
123 const size_t oldSize = m_totalUndoSize;
124 {
125 const undo::UndoState* state = nextRedo();
126 ASSERT(state);
127 const Cmd* cmd = STATE_CMD(state);
128 m_totalUndoSize -= cmd->memSize();
129 m_undoHistory.redo();
130 m_totalUndoSize += cmd->memSize();
131 }
132 notify_observers(&DocUndoObserver::onCurrentUndoStateChange, this);
133 if (m_totalUndoSize != oldSize)
134 notify_observers(&DocUndoObserver::onTotalUndoSizeChange, this);
135}
136
137void DocUndo::clearRedo()
138{
139 // Do nothing
140 if (currentState() == lastState())
141 return;
142
143 m_undoHistory.clearRedo();
144 notify_observers(&DocUndoObserver::onClearRedo, this);
145}
146
147bool DocUndo::isSavedState() const
148{
149 return (!m_savedStateIsLost && m_savedCounter == 0);
150}
151
152void DocUndo::markSavedState()
153{
154 m_savedCounter = 0;
155 m_savedStateIsLost = false;
156}
157
158void DocUndo::impossibleToBackToSavedState()
159{
160 m_savedStateIsLost = true;
161}
162
163std::string DocUndo::nextUndoLabel() const
164{
165 const undo::UndoState* state = nextUndo();
166 if (state)
167 return STATE_CMD(state)->label();
168 else
169 return "";
170}
171
172std::string DocUndo::nextRedoLabel() const
173{
174 const undo::UndoState* state = nextRedo();
175 if (state)
176 return STATE_CMD(state)->label();
177 else
178 return "";
179}
180
181SpritePosition DocUndo::nextUndoSpritePosition() const
182{
183 const undo::UndoState* state = nextUndo();
184 if (state)
185 return STATE_CMD(state)->spritePositionBeforeExecute();
186 else
187 return SpritePosition();
188}
189
190SpritePosition DocUndo::nextRedoSpritePosition() const
191{
192 const undo::UndoState* state = nextRedo();
193 if (state)
194 return STATE_CMD(state)->spritePositionAfterExecute();
195 else
196 return SpritePosition();
197}
198
199std::istream* DocUndo::nextUndoDocRange() const
200{
201 const undo::UndoState* state = nextUndo();
202 if (state)
203 return STATE_CMD(state)->documentRangeBeforeExecute();
204 else
205 return nullptr;
206}
207
208std::istream* DocUndo::nextRedoDocRange() const
209{
210 const undo::UndoState* state = nextRedo();
211 if (state)
212 return STATE_CMD(state)->documentRangeAfterExecute();
213 else
214 return nullptr;
215}
216
217Cmd* DocUndo::lastExecutedCmd() const
218{
219 const undo::UndoState* state = m_undoHistory.currentState();
220 if (state)
221 return STATE_CMD(state);
222 else
223 return NULL;
224}
225
226void DocUndo::moveToState(const undo::UndoState* state)
227{
228 m_undoHistory.moveTo(state);
229
230 // After onCurrentUndoStateChange don't use the "state" argument, it
231 // might be deleted because some script might have modified the
232 // sprite on its "change" event.
233 notify_observers(&DocUndoObserver::onCurrentUndoStateChange, this);
234
235 // Recalculate the total undo size
236 size_t oldSize = m_totalUndoSize;
237 m_totalUndoSize = 0;
238 const undo::UndoState* s = m_undoHistory.firstState();
239 while (s) {
240 m_totalUndoSize += STATE_CMD(s)->memSize();
241 s = s->next();
242 }
243 if (m_totalUndoSize != oldSize)
244 notify_observers(&DocUndoObserver::onTotalUndoSizeChange, this);
245}
246
247const undo::UndoState* DocUndo::nextUndo() const
248{
249 return m_undoHistory.currentState();
250}
251
252const undo::UndoState* DocUndo::nextRedo() const
253{
254 const undo::UndoState* state = m_undoHistory.currentState();
255 if (state)
256 return state->next();
257 else
258 return m_undoHistory.firstState();
259}
260
261void DocUndo::onDeleteUndoState(undo::UndoState* state)
262{
263 ASSERT(state);
264 Cmd* cmd = STATE_CMD(state);
265
266 UNDO_TRACE("UNDO: Deleting undo state <%s> of %s from %s\n",
267 cmd->label().c_str(),
268 base::get_pretty_memory_size(cmd->memSize()).c_str(),
269 base::get_pretty_memory_size(m_totalUndoSize).c_str());
270
271 m_totalUndoSize -= cmd->memSize();
272 notify_observers(&DocUndoObserver::onDeleteUndoState, this, state);
273}
274
275} // namespace app
276