summaryrefslogtreecommitdiffstats
path: root/src/document-undo.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/document-undo.cpp366
1 files changed, 366 insertions, 0 deletions
diff --git a/src/document-undo.cpp b/src/document-undo.cpp
new file mode 100644
index 0000000..46f88b6
--- /dev/null
+++ b/src/document-undo.cpp
@@ -0,0 +1,366 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Undo/Redo stack implementation
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * MenTaLguY <mental@rydia.net>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2007 MenTaLguY <mental@rydia.net>
+ * Copyright (C) 1999-2003 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ * Using the split document model gives sodipodi a very simple and clean
+ * undo implementation. Whenever mutation occurs in the XML tree,
+ * SPObject invokes one of the five corresponding handlers of its
+ * container document. This writes down a generic description of the
+ * given action, and appends it to the recent action list, kept by the
+ * document. There will be as many action records as there are mutation
+ * events, which are all kept and processed together in the undo
+ * stack. Two methods exist to indicate that the given action is completed:
+ *
+ * \verbatim
+ void sp_document_done( SPDocument *document );
+ void sp_document_maybe_done( SPDocument *document, const unsigned char *key ) \endverbatim
+ *
+ * Both move the recent action list into the undo stack and clear the
+ * list afterwards. While the first method does an unconditional push,
+ * the second one first checks the key of the most recent stack entry. If
+ * the keys are identical, the current action list is appended to the
+ * existing stack entry, instead of pushing it onto its own. This
+ * behaviour can be used to collect multi-step actions (like winding the
+ * Gtk spinbutton) from the UI into a single undoable step.
+ *
+ * For controls implemented by Sodipodi itself, implementing undo as a
+ * single step is usually done in a more efficient way. Most controls have
+ * the abstract model of grab, drag, release, and change user
+ * action. During the grab phase, all modifications are done to the
+ * SPObject directly - i.e. they do not change XML tree, and thus do not
+ * generate undo actions either. Only at the release phase (normally
+ * associated with releasing the mousebutton), changes are written back
+ * to the XML tree, thus generating only a single set of undo actions.
+ * (Lauris Kaplinski)
+ */
+
+#include "document-undo.h"
+
+#include <string>
+
+#include "event.h"
+#include "inkscape.h"
+
+#include "debug/event-tracker.h"
+#include "debug/simple-event.h"
+#include "debug/timestamp.h"
+#include "xml/repr.h"
+
+
+/*
+ * Undo & redo
+ */
+
+void Inkscape::DocumentUndo::setUndoSensitive(SPDocument *doc, bool sensitive)
+{
+ g_assert (doc != nullptr);
+
+ if ( sensitive == doc->sensitive )
+ return;
+
+ if (sensitive) {
+ sp_repr_begin_transaction (doc->rdoc);
+ } else {
+ doc->partial = sp_repr_coalesce_log (
+ doc->partial,
+ sp_repr_commit_undoable (doc->rdoc)
+ );
+ }
+
+ doc->sensitive = sensitive;
+}
+
+bool Inkscape::DocumentUndo::getUndoSensitive(SPDocument const *document) {
+ g_assert(document != nullptr);
+
+ return document->sensitive;
+}
+
+void Inkscape::DocumentUndo::done(SPDocument *doc,
+ Glib::ustring const &event_description,
+ Glib::ustring const &icon_name)
+{
+ if (doc->sensitive) {
+ maybeDone(doc, nullptr, event_description, icon_name);
+ }
+}
+
+void Inkscape::DocumentUndo::resetKey( SPDocument *doc )
+{
+ doc->actionkey.clear();
+}
+
+namespace {
+
+using Inkscape::Debug::Event;
+using Inkscape::Debug::SimpleEvent;
+using Inkscape::Debug::timestamp;
+
+typedef SimpleEvent<Event::INTERACTION> InteractionEvent;
+
+class CommitEvent : public InteractionEvent {
+public:
+
+ CommitEvent(SPDocument *doc, const gchar *key, const gchar* event_description, const gchar *icon_name)
+ : InteractionEvent("commit")
+ {
+ _addProperty("timestamp", timestamp());
+ _addProperty("document", doc->serial());
+
+ if (key) {
+ _addProperty("merge-key", key);
+ }
+
+ if (event_description) {
+ _addProperty("description", event_description);
+ }
+
+ if (icon_name) {
+ _addProperty("icon-name", icon_name);
+ }
+ }
+};
+
+}
+
+// 'key' is used to coalesce changes of the same type.
+// 'event_description' and 'icon_name' are used in the Undo History dialog.
+void Inkscape::DocumentUndo::maybeDone(SPDocument *doc,
+ const gchar *key,
+ Glib::ustring const &event_description,
+ Glib::ustring const &icon_name)
+{
+ g_assert (doc != nullptr);
+ g_assert (doc->sensitive);
+ if ( key && !*key ) {
+ g_warning("Blank undo key specified.");
+ }
+ doc->before_commit_signal.emit();
+ // This is only used for output to debug log file (and not for undo).
+ Inkscape::Debug::EventTracker<CommitEvent> tracker(doc, key, event_description.c_str(), icon_name.c_str());
+
+ doc->collectOrphans();
+
+ doc->ensureUpToDate();
+
+ DocumentUndo::clearRedo(doc);
+
+ Inkscape::XML::Event *log = sp_repr_coalesce_log (doc->partial, sp_repr_commit_undoable (doc->rdoc));
+ doc->partial = nullptr;
+
+ if (!log) {
+ sp_repr_begin_transaction (doc->rdoc);
+ return;
+ }
+
+ if (key && !doc->actionkey.empty() && (doc->actionkey == key) && !doc->undo.empty()) {
+ (doc->undo.back())->event =
+ sp_repr_coalesce_log ((doc->undo.back())->event, log);
+ } else {
+ Inkscape::Event *event = new Inkscape::Event(log, event_description, icon_name);
+ doc->undo.push_back(event);
+ doc->history_size++;
+ doc->undoStackObservers.notifyUndoCommitEvent(event);
+ }
+
+ if ( key ) {
+ doc->actionkey = key;
+ } else {
+ doc->actionkey.clear();
+ }
+
+ doc->virgin = FALSE;
+ doc->setModifiedSinceSave();
+
+ sp_repr_begin_transaction (doc->rdoc);
+
+ doc->commit_signal.emit();
+}
+
+void Inkscape::DocumentUndo::cancel(SPDocument *doc)
+{
+ g_assert (doc != nullptr);
+ g_assert (doc->sensitive);
+
+ sp_repr_rollback (doc->rdoc);
+
+ if (doc->partial) {
+ sp_repr_undo_log (doc->partial);
+ doc->emitReconstructionFinish();
+ sp_repr_free_log (doc->partial);
+ doc->partial = nullptr;
+ }
+
+ sp_repr_begin_transaction (doc->rdoc);
+}
+
+// Member function for friend access to SPDocument privates.
+void Inkscape::DocumentUndo::finish_incomplete_transaction(SPDocument &doc) {
+ Inkscape::XML::Event *log=sp_repr_commit_undoable(doc.rdoc);
+ if (log || doc.partial) {
+ g_warning ("Incomplete undo transaction:");
+ doc.partial = sp_repr_coalesce_log(doc.partial, log);
+ sp_repr_debug_print_log(doc.partial);
+ Inkscape::Event *event = new Inkscape::Event(doc.partial);
+ doc.undo.push_back(event);
+ doc.undoStackObservers.notifyUndoCommitEvent(event);
+ doc.partial = nullptr;
+ }
+}
+
+// Member function for friend access to SPDocument privates.
+void Inkscape::DocumentUndo::perform_document_update(SPDocument &doc) {
+ sp_repr_begin_transaction(doc.rdoc);
+ doc.ensureUpToDate();
+
+ Inkscape::XML::Event *update_log=sp_repr_commit_undoable(doc.rdoc);
+ doc.emitReconstructionFinish();
+
+ if (update_log != nullptr) {
+ g_warning("Document was modified while being updated after undo operation");
+ sp_repr_debug_print_log(update_log);
+
+ //Coalesce the update changes with the last action performed by user
+ if (!doc.undo.empty()) {
+ Inkscape::Event* undo_stack_top = doc.undo.back();
+ undo_stack_top->event = sp_repr_coalesce_log(undo_stack_top->event, update_log);
+ } else {
+ sp_repr_free_log(update_log);
+ }
+ }
+}
+
+gboolean Inkscape::DocumentUndo::undo(SPDocument *doc)
+{
+ using Inkscape::Debug::EventTracker;
+ using Inkscape::Debug::SimpleEvent;
+
+ gboolean ret;
+
+ EventTracker<SimpleEvent<Inkscape::Debug::Event::DOCUMENT> > tracker("undo");
+
+ g_assert (doc != nullptr);
+ g_assert (doc->sensitive);
+
+ doc->sensitive = FALSE;
+ doc->seeking = true;
+
+ doc->actionkey.clear();
+
+ finish_incomplete_transaction(*doc);
+
+ if (! doc->undo.empty()) {
+ Inkscape::Event *log = doc->undo.back();
+ doc->undo.pop_back();
+ sp_repr_undo_log (log->event);
+ perform_document_update(*doc);
+
+ doc->redo.push_back(log);
+ doc->setModifiedSinceSave();
+ doc->undoStackObservers.notifyUndoEvent(log);
+ ret = TRUE;
+ } else {
+ ret = FALSE;
+ }
+
+ sp_repr_begin_transaction (doc->rdoc);
+
+ doc->sensitive = TRUE;
+ doc->seeking = false;
+
+ if (ret) INKSCAPE.external_change();
+
+ return ret;
+}
+
+gboolean Inkscape::DocumentUndo::redo(SPDocument *doc)
+{
+ using Inkscape::Debug::EventTracker;
+ using Inkscape::Debug::SimpleEvent;
+
+ gboolean ret;
+
+ EventTracker<SimpleEvent<Inkscape::Debug::Event::DOCUMENT> > tracker("redo");
+
+ g_assert (doc != nullptr);
+ g_assert (doc->sensitive);
+
+ doc->sensitive = FALSE;
+ doc->seeking = true;
+
+ doc->actionkey.clear();
+
+ finish_incomplete_transaction(*doc);
+
+ if (! doc->redo.empty()) {
+ Inkscape::Event *log = doc->redo.back();
+ doc->redo.pop_back();
+ sp_repr_replay_log (log->event);
+ doc->undo.push_back(log);
+
+ doc->setModifiedSinceSave();
+ doc->undoStackObservers.notifyRedoEvent(log);
+ ret = TRUE;
+ } else {
+ ret = FALSE;
+ }
+
+ sp_repr_begin_transaction (doc->rdoc);
+
+ doc->sensitive = TRUE;
+ doc->seeking = false;
+
+ if (ret) {
+ INKSCAPE.external_change();
+ doc->emitReconstructionFinish();
+ }
+
+ return ret;
+}
+
+void Inkscape::DocumentUndo::clearUndo(SPDocument *doc)
+{
+ if (! doc->undo.empty())
+ doc->undoStackObservers.notifyClearUndoEvent();
+ while (! doc->undo.empty()) {
+ Inkscape::Event *e = doc->undo.back();
+ doc->undo.pop_back();
+ delete e;
+ doc->history_size--;
+ }
+}
+
+void Inkscape::DocumentUndo::clearRedo(SPDocument *doc)
+{
+ if (!doc->redo.empty())
+ doc->undoStackObservers.notifyClearRedoEvent();
+
+ while (! doc->redo.empty()) {
+ Inkscape::Event *e = doc->redo.back();
+ doc->redo.pop_back();
+ delete e;
+ doc->history_size--;
+ }
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :