1 /** \file
2 * Undo/Redo stack implementation
3 *
4 * Authors:
5 * Lauris Kaplinski <lauris@kaplinski.com>
6 * MenTaLguY <mental@rydia.net>
7 * Abhishek Sharma
8 *
9 * Copyright (C) 2007 MenTaLguY <mental@rydia.net>
10 * Copyright (C) 1999-2003 authors
11 * Copyright (C) 2001-2002 Ximian, Inc.
12 *
13 * Released under GNU GPL, read the file 'COPYING' for more information
14 *
15 * Using the split document model gives sodipodi a very simple and clean
16 * undo implementation. Whenever mutation occurs in the XML tree,
17 * SPObject invokes one of the five corresponding handlers of its
18 * container document. This writes down a generic description of the
19 * given action, and appends it to the recent action list, kept by the
20 * document. There will be as many action records as there are mutation
21 * events, which are all kept and processed together in the undo
22 * stack. Two methods exist to indicate that the given action is completed:
23 *
24 * \verbatim
25 void sp_document_done( SPDocument *document );
26 void sp_document_maybe_done( SPDocument *document, const unsigned char *key ) \endverbatim
27 *
28 * Both move the recent action list into the undo stack and clear the
29 * list afterwards. While the first method does an unconditional push,
30 * the second one first checks the key of the most recent stack entry. If
31 * the keys are identical, the current action list is appended to the
32 * existing stack entry, instead of pushing it onto its own. This
33 * behaviour can be used to collect multi-step actions (like winding the
34 * Gtk spinbutton) from the UI into a single undoable step.
35 *
36 * For controls implemented by Sodipodi itself, implementing undo as a
37 * single step is usually done in a more efficent way. Most controls have
38 * the abstract model of grab, drag, release, and change user
39 * action. During the grab phase, all modifications are done to the
40 * SPObject directly - i.e. they do not change XML tree, and thus do not
41 * generate undo actions either. Only at the release phase (normally
42 * associated with releasing the mousebutton), changes are written back
43 * to the XML tree, thus generating only a single set of undo actions.
44 * (Lauris Kaplinski)
45 */
47 #ifdef HAVE_CONFIG_H
48 # include "config.h"
49 #endif
53 #if HAVE_STRING_H
54 #endif
57 #if HAVE_STDLIB_H
58 #endif
60 #include <string>
61 #include <cstring>
62 #include "xml/repr.h"
63 #include "document-private.h"
64 #include "inkscape.h"
65 #include "document-undo.h"
66 #include "debug/event-tracker.h"
67 #include "debug/simple-event.h"
68 #include "debug/timestamp.h"
69 #include "event.h"
72 /*
73 * Undo & redo
74 */
76 void Inkscape::DocumentUndo::setUndoSensitive(SPDocument *doc, bool sensitive)
77 {
78 g_assert (doc != NULL);
79 g_assert (doc->priv != NULL);
81 if ( sensitive == doc->priv->sensitive )
82 return;
84 if (sensitive) {
85 sp_repr_begin_transaction (doc->rdoc);
86 } else {
87 doc->priv->partial = sp_repr_coalesce_log (
88 doc->priv->partial,
89 sp_repr_commit_undoable (doc->rdoc)
90 );
91 }
93 doc->priv->sensitive = sensitive;
94 }
96 /*TODO: Throughout the inkscape code tree set/get_undo_sensitive are used for
97 * as is shown above. Perhaps it makes sense to create new functions,
98 * undo_ignore, and undo_recall to replace the start and end parts of the above.
99 * The main complexity with this is that they have to nest, so you have to store
100 * the saved bools in a stack. Perhaps this is why the above solution is better.
101 */
103 bool Inkscape::DocumentUndo::getUndoSensitive(SPDocument const *document) {
104 g_assert(document != NULL);
105 g_assert(document->priv != NULL);
107 return document->priv->sensitive;
108 }
110 void Inkscape::DocumentUndo::done(SPDocument *doc, const unsigned int event_type, Glib::ustring const &event_description)
111 {
112 maybeDone(doc, NULL, event_type, event_description);
113 }
115 void Inkscape::DocumentUndo::resetKey( Inkscape::Application * /*inkscape*/, SPDesktop * /*desktop*/, GtkObject *base )
116 {
117 SPDocument *doc = reinterpret_cast<SPDocument *>(base);
118 doc->actionkey.clear();
119 }
121 namespace {
123 using Inkscape::Debug::Event;
124 using Inkscape::Debug::SimpleEvent;
125 using Inkscape::Util::share_static_string;
126 using Inkscape::Debug::timestamp;
127 using Inkscape::Verb;
129 typedef SimpleEvent<Event::INTERACTION> InteractionEvent;
131 class CommitEvent : public InteractionEvent {
132 public:
134 CommitEvent(SPDocument *doc, const gchar *key, const unsigned int type)
135 : InteractionEvent(share_static_string("commit"))
136 {
137 _addProperty(share_static_string("timestamp"), timestamp());
138 gchar *serial = g_strdup_printf("%lu", doc->serial());
139 _addProperty(share_static_string("document"), serial);
140 g_free(serial);
141 Verb *verb = Verb::get(type);
142 if (verb) {
143 _addProperty(share_static_string("context"), verb->get_id());
144 }
145 if (key) {
146 _addProperty(share_static_string("merge-key"), key);
147 }
148 }
149 };
151 }
153 void Inkscape::DocumentUndo::maybeDone(SPDocument *doc, const gchar *key, const unsigned int event_type,
154 Glib::ustring const &event_description)
155 {
156 g_assert (doc != NULL);
157 g_assert (doc->priv != NULL);
158 g_assert (doc->priv->sensitive);
159 if ( key && !*key ) {
160 g_warning("Blank undo key specified.");
161 }
163 Inkscape::Debug::EventTracker<CommitEvent> tracker(doc, key, event_type);
165 doc->collectOrphans();
167 doc->ensureUpToDate();
169 DocumentUndo::clearRedo(doc);
171 Inkscape::XML::Event *log = sp_repr_coalesce_log (doc->priv->partial, sp_repr_commit_undoable (doc->rdoc));
172 doc->priv->partial = NULL;
174 if (!log) {
175 sp_repr_begin_transaction (doc->rdoc);
176 return;
177 }
179 if (key && !doc->actionkey.empty() && (doc->actionkey == key) && doc->priv->undo) {
180 ((Inkscape::Event *)doc->priv->undo->data)->event =
181 sp_repr_coalesce_log (((Inkscape::Event *)doc->priv->undo->data)->event, log);
182 } else {
183 Inkscape::Event *event = new Inkscape::Event(log, event_type, event_description);
184 doc->priv->undo = g_slist_prepend (doc->priv->undo, event);
185 doc->priv->history_size++;
186 doc->priv->undoStackObservers.notifyUndoCommitEvent(event);
187 }
189 if ( key ) {
190 doc->actionkey = key;
191 } else {
192 doc->actionkey.clear();
193 }
195 doc->virgin = FALSE;
196 doc->setModifiedSinceSave();
198 sp_repr_begin_transaction (doc->rdoc);
200 doc->priv->commit_signal.emit();
201 }
203 void Inkscape::DocumentUndo::cancel(SPDocument *doc)
204 {
205 g_assert (doc != NULL);
206 g_assert (doc->priv != NULL);
207 g_assert (doc->priv->sensitive);
209 sp_repr_rollback (doc->rdoc);
211 if (doc->priv->partial) {
212 sp_repr_undo_log (doc->priv->partial);
213 sp_repr_free_log (doc->priv->partial);
214 doc->priv->partial = NULL;
215 }
217 sp_repr_begin_transaction (doc->rdoc);
218 }
220 static void finish_incomplete_transaction(SPDocument &doc) {
221 SPDocumentPrivate &priv=*doc.priv;
222 Inkscape::XML::Event *log=sp_repr_commit_undoable(doc.rdoc);
223 if (log || priv.partial) {
224 g_warning ("Incomplete undo transaction:");
225 priv.partial = sp_repr_coalesce_log(priv.partial, log);
226 sp_repr_debug_print_log(priv.partial);
227 Inkscape::Event *event = new Inkscape::Event(priv.partial);
228 priv.undo = g_slist_prepend(priv.undo, event);
229 priv.undoStackObservers.notifyUndoCommitEvent(event);
230 priv.partial = NULL;
231 }
232 }
234 gboolean Inkscape::DocumentUndo::undo(SPDocument *doc)
235 {
236 using Inkscape::Debug::EventTracker;
237 using Inkscape::Debug::SimpleEvent;
239 gboolean ret;
241 EventTracker<SimpleEvent<Inkscape::Debug::Event::DOCUMENT> > tracker("undo");
243 g_assert (doc != NULL);
244 g_assert (doc->priv != NULL);
245 g_assert (doc->priv->sensitive);
247 doc->priv->sensitive = FALSE;
248 doc->priv->seeking = true;
250 doc->actionkey.clear();
252 finish_incomplete_transaction(*doc);
254 if (doc->priv->undo) {
255 Inkscape::Event *log=(Inkscape::Event *)doc->priv->undo->data;
256 doc->priv->undo = g_slist_remove (doc->priv->undo, log);
257 sp_repr_undo_log (log->event);
258 doc->priv->redo = g_slist_prepend (doc->priv->redo, log);
260 doc->setModifiedSinceSave();
261 doc->priv->undoStackObservers.notifyUndoEvent(log);
263 ret = TRUE;
264 } else {
265 ret = FALSE;
266 }
268 sp_repr_begin_transaction (doc->rdoc);
270 doc->priv->sensitive = TRUE;
271 doc->priv->seeking = false;
273 if (ret)
274 inkscape_external_change();
276 return ret;
277 }
279 gboolean Inkscape::DocumentUndo::redo(SPDocument *doc)
280 {
281 using Inkscape::Debug::EventTracker;
282 using Inkscape::Debug::SimpleEvent;
284 gboolean ret;
286 EventTracker<SimpleEvent<Inkscape::Debug::Event::DOCUMENT> > tracker("redo");
288 g_assert (doc != NULL);
289 g_assert (doc->priv != NULL);
290 g_assert (doc->priv->sensitive);
292 doc->priv->sensitive = FALSE;
293 doc->priv->seeking = true;
295 doc->actionkey.clear();
297 finish_incomplete_transaction(*doc);
299 if (doc->priv->redo) {
300 Inkscape::Event *log=(Inkscape::Event *)doc->priv->redo->data;
301 doc->priv->redo = g_slist_remove (doc->priv->redo, log);
302 sp_repr_replay_log (log->event);
303 doc->priv->undo = g_slist_prepend (doc->priv->undo, log);
305 doc->setModifiedSinceSave();
306 doc->priv->undoStackObservers.notifyRedoEvent(log);
308 ret = TRUE;
309 } else {
310 ret = FALSE;
311 }
313 sp_repr_begin_transaction (doc->rdoc);
315 doc->priv->sensitive = TRUE;
316 doc->priv->seeking = false;
318 if (ret)
319 inkscape_external_change();
321 return ret;
322 }
324 void Inkscape::DocumentUndo::clearUndo(SPDocument *doc)
325 {
326 if (doc->priv->undo)
327 doc->priv->undoStackObservers.notifyClearUndoEvent();
329 while (doc->priv->undo) {
330 GSList *current;
332 current = doc->priv->undo;
333 doc->priv->undo = current->next;
334 doc->priv->history_size--;
336 delete ((Inkscape::Event *) current->data);
337 g_slist_free_1 (current);
338 }
339 }
341 void Inkscape::DocumentUndo::clearRedo(SPDocument *doc)
342 {
343 if (doc->priv->redo)
344 doc->priv->undoStackObservers.notifyClearRedoEvent();
346 while (doc->priv->redo) {
347 GSList *current;
349 current = doc->priv->redo;
350 doc->priv->redo = current->next;
351 doc->priv->history_size--;
353 delete ((Inkscape::Event *) current->data);
354 g_slist_free_1 (current);
355 }
356 }
357 /*
358 Local Variables:
359 mode:c++
360 c-file-style:"stroustrup"
361 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
362 indent-tabs-mode:nil
363 fill-column:99
364 End:
365 */
366 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :