1 #define __SP_DOCUMENT_UNDO_C__
3 /** \file
4 * Undo/Redo stack implementation
5 *
6 * Authors:
7 * Lauris Kaplinski <lauris@kaplinski.com>
8 * MenTaLguY <mental@rydia.net>
9 *
10 * Copyright (C) 2007 MenTaLguY <mental@rydia.net>
11 * Copyright (C) 1999-2003 authors
12 * Copyright (C) 2001-2002 Ximian, Inc.
13 *
14 * Released under GNU GPL, read the file 'COPYING' for more information
15 *
16 * Using the split document model gives sodipodi a very simple and clean
17 * undo implementation. Whenever mutation occurs in the XML tree,
18 * SPObject invokes one of the five corresponding handlers of its
19 * container document. This writes down a generic description of the
20 * given action, and appends it to the recent action list, kept by the
21 * document. There will be as many action records as there are mutation
22 * events, which are all kept and processed together in the undo
23 * stack. Two methods exist to indicate that the given action is completed:
24 *
25 * \verbatim
26 void sp_document_done (SPDocument *document);
27 void sp_document_maybe_done (SPDocument *document, const unsigned char *key) \endverbatim
28 *
29 * Both move the recent action list into the undo stack and clear the
30 * list afterwards. While the first method does an unconditional push,
31 * the second one first checks the key of the most recent stack entry. If
32 * the keys are identical, the current action list is appended to the
33 * existing stack entry, instead of pushing it onto its own. This
34 * behaviour can be used to collect multi-step actions (like winding the
35 * Gtk spinbutton) from the UI into a single undoable step.
36 *
37 * For controls implemented by Sodipodi itself, implementing undo as a
38 * single step is usually done in a more efficent way. Most controls have
39 * the abstract model of grab, drag, release, and change user
40 * action. During the grab phase, all modifications are done to the
41 * SPObject directly - i.e. they do not change XML tree, and thus do not
42 * generate undo actions either. Only at the release phase (normally
43 * associated with releasing the mousebutton), changes are written back
44 * to the XML tree, thus generating only a single set of undo actions.
45 * (Lauris Kaplinski)
46 */
48 #ifdef HAVE_CONFIG_H
49 # include "config.h"
50 #endif
54 #if HAVE_STRING_H
55 #endif
58 #if HAVE_STDLIB_H
59 #endif
61 #include <string>
62 #include <cstring>
63 #include "xml/repr.h"
64 #include "document-private.h"
65 #include "inkscape.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 */
75 /**
76 * Set undo sensitivity.
77 *
78 * \note
79 * Since undo sensitivity needs to be nested, setting undo sensitivity
80 * should be done like this:
81 *\verbatim
82 bool saved = sp_document_get_undo_sensitive(document);
83 sp_document_set_undo_sensitive(document, false);
84 ... do stuff ...
85 sp_document_set_undo_sensitive(document, saved); \endverbatim
86 */
87 void
88 sp_document_set_undo_sensitive (SPDocument *doc, bool sensitive)
89 {
90 g_assert (doc != NULL);
91 g_assert (doc->priv != NULL);
93 if ( sensitive == doc->priv->sensitive )
94 return;
96 if (sensitive) {
97 sp_repr_begin_transaction (doc->rdoc);
98 } else {
99 doc->priv->partial = sp_repr_coalesce_log (
100 doc->priv->partial,
101 sp_repr_commit_undoable (doc->rdoc)
102 );
103 }
105 doc->priv->sensitive = sensitive;
106 }
108 /*TODO: Throughout the inkscape code tree set/get_undo_sensitive are used for
109 * as is shown above. Perhaps it makes sense to create new functions,
110 * undo_ignore, and undo_recall to replace the start and end parts of the above.
111 * The main complexity with this is that they have to nest, so you have to store
112 * the saved bools in a stack. Perhaps this is why the above solution is better.
113 */
115 bool sp_document_get_undo_sensitive(SPDocument const *document) {
116 g_assert(document != NULL);
117 g_assert(document->priv != NULL);
119 return document->priv->sensitive;
120 }
122 void
123 sp_document_done (SPDocument *doc, const unsigned int event_type, Glib::ustring event_description)
124 {
125 sp_document_maybe_done (doc, NULL, event_type, event_description);
126 }
128 void sp_document_reset_key( Inkscape::Application * /*inkscape*/, SPDesktop * /*desktop*/, GtkObject *base )
129 {
130 SPDocument *doc = reinterpret_cast<SPDocument *>(base);
131 doc->actionkey.clear();
132 }
134 namespace {
136 using Inkscape::Debug::Event;
137 using Inkscape::Debug::SimpleEvent;
138 using Inkscape::Util::share_static_string;
139 using Inkscape::Debug::timestamp;
140 using Inkscape::Verb;
142 typedef SimpleEvent<Event::INTERACTION> InteractionEvent;
144 class CommitEvent : public InteractionEvent {
145 public:
147 CommitEvent(SPDocument *doc, const gchar *key, const unsigned int type)
148 : InteractionEvent(share_static_string("commit"))
149 {
150 _addProperty(share_static_string("timestamp"), timestamp());
151 gchar *serial = g_strdup_printf("%lu", doc->serial());
152 _addProperty(share_static_string("document"), serial);
153 g_free(serial);
154 Verb *verb = Verb::get(type);
155 if (verb) {
156 _addProperty(share_static_string("context"), verb->get_id());
157 }
158 if (key) {
159 _addProperty(share_static_string("merge-key"), key);
160 }
161 }
162 };
164 }
166 void
167 sp_document_maybe_done (SPDocument *doc, const gchar *key, const unsigned int event_type,
168 Glib::ustring event_description)
169 {
170 g_assert (doc != NULL);
171 g_assert (doc->priv != NULL);
172 g_assert (doc->priv->sensitive);
173 if ( key && !*key ) {
174 g_warning("Blank undo key specified.");
175 }
177 Inkscape::Debug::EventTracker<CommitEvent> tracker(doc, key, event_type);
179 doc->collectOrphans();
181 sp_document_ensure_up_to_date (doc);
183 sp_document_clear_redo (doc);
185 Inkscape::XML::Event *log = sp_repr_coalesce_log (doc->priv->partial, sp_repr_commit_undoable (doc->rdoc));
186 doc->priv->partial = NULL;
188 if (!log) {
189 sp_repr_begin_transaction (doc->rdoc);
190 return;
191 }
193 if (key && !doc->actionkey.empty() && (doc->actionkey == key) && doc->priv->undo) {
194 ((Inkscape::Event *)doc->priv->undo->data)->event =
195 sp_repr_coalesce_log (((Inkscape::Event *)doc->priv->undo->data)->event, log);
196 } else {
197 Inkscape::Event *event = new Inkscape::Event(log, event_type, event_description);
198 doc->priv->undo = g_slist_prepend (doc->priv->undo, event);
199 doc->priv->history_size++;
200 doc->priv->undoStackObservers.notifyUndoCommitEvent(event);
201 }
203 if ( key ) {
204 doc->actionkey = key;
205 } else {
206 doc->actionkey.clear();
207 }
209 doc->virgin = FALSE;
210 doc->setModifiedSinceSave();
212 sp_repr_begin_transaction (doc->rdoc);
214 doc->priv->commit_signal.emit();
215 }
217 void
218 sp_document_cancel (SPDocument *doc)
219 {
220 g_assert (doc != NULL);
221 g_assert (doc->priv != NULL);
222 g_assert (doc->priv->sensitive);
224 sp_repr_rollback (doc->rdoc);
226 if (doc->priv->partial) {
227 sp_repr_undo_log (doc->priv->partial);
228 sp_repr_free_log (doc->priv->partial);
229 doc->priv->partial = NULL;
230 }
232 sp_repr_begin_transaction (doc->rdoc);
233 }
235 static void finish_incomplete_transaction(SPDocument &doc) {
236 SPDocumentPrivate &priv=*doc.priv;
237 Inkscape::XML::Event *log=sp_repr_commit_undoable(doc.rdoc);
238 if (log || priv.partial) {
239 g_warning ("Incomplete undo transaction:");
240 priv.partial = sp_repr_coalesce_log(priv.partial, log);
241 sp_repr_debug_print_log(priv.partial);
242 Inkscape::Event *event = new Inkscape::Event(priv.partial);
243 priv.undo = g_slist_prepend(priv.undo, event);
244 priv.undoStackObservers.notifyUndoCommitEvent(event);
245 priv.partial = NULL;
246 }
247 }
249 gboolean
250 sp_document_undo (SPDocument *doc)
251 {
252 using Inkscape::Debug::EventTracker;
253 using Inkscape::Debug::SimpleEvent;
255 gboolean ret;
257 EventTracker<SimpleEvent<Inkscape::Debug::Event::DOCUMENT> > tracker("undo");
259 g_assert (doc != NULL);
260 g_assert (doc->priv != NULL);
261 g_assert (doc->priv->sensitive);
263 doc->priv->sensitive = FALSE;
264 doc->priv->seeking = true;
266 doc->actionkey.clear();
268 finish_incomplete_transaction(*doc);
270 if (doc->priv->undo) {
271 Inkscape::Event *log=(Inkscape::Event *)doc->priv->undo->data;
272 doc->priv->undo = g_slist_remove (doc->priv->undo, log);
273 sp_repr_undo_log (log->event);
274 doc->priv->redo = g_slist_prepend (doc->priv->redo, log);
276 doc->setModifiedSinceSave();
277 doc->priv->undoStackObservers.notifyUndoEvent(log);
279 ret = TRUE;
280 } else {
281 ret = FALSE;
282 }
284 sp_repr_begin_transaction (doc->rdoc);
286 doc->priv->sensitive = TRUE;
287 doc->priv->seeking = false;
289 if (ret)
290 inkscape_external_change();
292 return ret;
293 }
295 gboolean
296 sp_document_redo (SPDocument *doc)
297 {
298 using Inkscape::Debug::EventTracker;
299 using Inkscape::Debug::SimpleEvent;
301 gboolean ret;
303 EventTracker<SimpleEvent<Inkscape::Debug::Event::DOCUMENT> > tracker("redo");
305 g_assert (doc != NULL);
306 g_assert (doc->priv != NULL);
307 g_assert (doc->priv->sensitive);
309 doc->priv->sensitive = FALSE;
310 doc->priv->seeking = true;
312 doc->actionkey.clear();
314 finish_incomplete_transaction(*doc);
316 if (doc->priv->redo) {
317 Inkscape::Event *log=(Inkscape::Event *)doc->priv->redo->data;
318 doc->priv->redo = g_slist_remove (doc->priv->redo, log);
319 sp_repr_replay_log (log->event);
320 doc->priv->undo = g_slist_prepend (doc->priv->undo, log);
322 doc->setModifiedSinceSave();
323 doc->priv->undoStackObservers.notifyRedoEvent(log);
325 ret = TRUE;
326 } else {
327 ret = FALSE;
328 }
330 sp_repr_begin_transaction (doc->rdoc);
332 doc->priv->sensitive = TRUE;
333 doc->priv->seeking = false;
335 if (ret)
336 inkscape_external_change();
338 return ret;
339 }
341 void
342 sp_document_clear_undo (SPDocument *doc)
343 {
344 if (doc->priv->undo)
345 doc->priv->undoStackObservers.notifyClearUndoEvent();
347 while (doc->priv->undo) {
348 GSList *current;
350 current = doc->priv->undo;
351 doc->priv->undo = current->next;
352 doc->priv->history_size--;
354 delete ((Inkscape::Event *) current->data);
355 g_slist_free_1 (current);
356 }
357 }
359 void
360 sp_document_clear_redo (SPDocument *doc)
361 {
362 if (doc->priv->redo)
363 doc->priv->undoStackObservers.notifyClearRedoEvent();
365 while (doc->priv->redo) {
366 GSList *current;
368 current = doc->priv->redo;
369 doc->priv->redo = current->next;
370 doc->priv->history_size--;
372 delete ((Inkscape::Event *) current->data);
373 g_slist_free_1 (current);
374 }
375 }
376 /*
377 Local Variables:
378 mode:c++
379 c-file-style:"stroustrup"
380 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
381 indent-tabs-mode:nil
382 fill-column:99
383 End:
384 */
385 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :