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 | |
30 | namespace app { |
31 | |
32 | DocUndo::DocUndo() |
33 | : m_undoHistory(this) |
34 | , m_ctx(nullptr) |
35 | , m_totalUndoSize(0) |
36 | , m_savedCounter(0) |
37 | , m_savedStateIsLost(false) |
38 | { |
39 | } |
40 | |
41 | void DocUndo::setContext(Context* ctx) |
42 | { |
43 | m_ctx = ctx; |
44 | } |
45 | |
46 | void 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 | |
91 | bool DocUndo::canUndo() const |
92 | { |
93 | return m_undoHistory.canUndo(); |
94 | } |
95 | |
96 | bool DocUndo::canRedo() const |
97 | { |
98 | return m_undoHistory.canRedo(); |
99 | } |
100 | |
101 | void 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 | |
121 | void 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 | |
137 | void 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 | |
147 | bool DocUndo::isSavedState() const |
148 | { |
149 | return (!m_savedStateIsLost && m_savedCounter == 0); |
150 | } |
151 | |
152 | void DocUndo::markSavedState() |
153 | { |
154 | m_savedCounter = 0; |
155 | m_savedStateIsLost = false; |
156 | } |
157 | |
158 | void DocUndo::impossibleToBackToSavedState() |
159 | { |
160 | m_savedStateIsLost = true; |
161 | } |
162 | |
163 | std::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 | |
172 | std::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 | |
181 | SpritePosition 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 | |
190 | SpritePosition 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 | |
199 | std::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 | |
208 | std::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 | |
217 | Cmd* 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 | |
226 | void 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 | |
247 | const undo::UndoState* DocUndo::nextUndo() const |
248 | { |
249 | return m_undoHistory.currentState(); |
250 | } |
251 | |
252 | const 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 | |
261 | void 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 | |