summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2016-07-30 13:10:13 -0400
committerDavid Robillard <d@drobilla.net>2016-07-30 15:32:49 -0400
commitc8ae7295e911c62cf9dedf90187656937cc18cbb (patch)
tree0ba1207bf2c63278e9932eed2d04f961144a02db
parente74c6a3b388ded66fcc4cfb1fa5bece881b63113 (diff)
downloadingen-c8ae7295e911c62cf9dedf90187656937cc18cbb.tar.gz
ingen-c8ae7295e911c62cf9dedf90187656937cc18cbb.tar.bz2
ingen-c8ae7295e911c62cf9dedf90187656937cc18cbb.zip
Add undo support
-rw-r--r--bundles/ingen.lv2/ingen.ttl15
-rw-r--r--ingen/AtomWriter.hpp9
-rw-r--r--ingen/Interface.hpp14
-rw-r--r--ingen/Resource.hpp15
-rw-r--r--ingen/URIs.hpp8
-rw-r--r--ingen/client/ClientStore.hpp4
-rw-r--r--ingen/client/SigClientInterface.hpp4
-rw-r--r--ingen/ingen.h6
-rw-r--r--src/AtomReader.cpp6
-rw-r--r--src/AtomWriter.cpp51
-rw-r--r--src/Resource.cpp15
-rw-r--r--src/URIs.cpp8
-rw-r--r--src/gui/GraphBox.cpp22
-rw-r--r--src/gui/GraphBox.hpp6
-rw-r--r--src/gui/GraphCanvas.cpp4
-rw-r--r--src/gui/ingen_gui.ui26
-rw-r--r--src/server/Broadcaster.hpp4
-rw-r--r--src/server/Engine.cpp14
-rw-r--r--src/server/Engine.hpp14
-rw-r--r--src/server/Event.hpp19
-rw-r--r--src/server/EventWriter.cpp69
-rw-r--r--src/server/EventWriter.hpp14
-rw-r--r--src/server/PreProcessor.cpp40
-rw-r--r--src/server/PreProcessor.hpp8
-rw-r--r--src/server/UndoStack.cpp252
-rw-r--r--src/server/UndoStack.hpp107
-rw-r--r--src/server/events.hpp6
-rw-r--r--src/server/events/Connect.cpp8
-rw-r--r--src/server/events/Connect.hpp3
-rw-r--r--src/server/events/Copy.cpp8
-rw-r--r--src/server/events/Copy.hpp3
-rw-r--r--src/server/events/CreateBlock.cpp8
-rw-r--r--src/server/events/CreateBlock.hpp3
-rw-r--r--src/server/events/CreateGraph.cpp8
-rw-r--r--src/server/events/CreateGraph.hpp1
-rw-r--r--src/server/events/CreatePort.cpp8
-rw-r--r--src/server/events/CreatePort.hpp3
-rw-r--r--src/server/events/Delete.cpp14
-rw-r--r--src/server/events/Delete.hpp3
-rw-r--r--src/server/events/Delta.cpp57
-rw-r--r--src/server/events/Delta.hpp6
-rw-r--r--src/server/events/Disconnect.cpp8
-rw-r--r--src/server/events/Disconnect.hpp6
-rw-r--r--src/server/events/DisconnectAll.cpp10
-rw-r--r--src/server/events/DisconnectAll.hpp3
-rw-r--r--src/server/events/Mark.cpp65
-rw-r--r--src/server/events/Mark.hpp55
-rw-r--r--src/server/events/Move.cpp8
-rw-r--r--src/server/events/Move.hpp3
-rw-r--r--src/server/events/Undo.cpp77
-rw-r--r--src/server/events/Undo.hpp54
-rw-r--r--src/server/wscript3
-rw-r--r--tests/ingen_test.cpp52
-rw-r--r--wscript28
54 files changed, 1167 insertions, 98 deletions
diff --git a/bundles/ingen.lv2/ingen.ttl b/bundles/ingen.lv2/ingen.ttl
index b8bf38f0..3f7f89f5 100644
--- a/bundles/ingen.lv2/ingen.ttl
+++ b/bundles/ingen.lv2/ingen.ttl
@@ -205,3 +205,18 @@ ingen:incidentTo
rdfs:domain ingen:Arc ;
rdfs:label "incident to" ;
rdfs:comment "A special property used to describe any arc incident to a port or block. This is never saved in graph files, but is used in the control protocol to completely disconnect a Block or Port." .
+
+ingen:Undo
+ a rdfs:Class ;
+ rdfs:label "Undo" ;
+ rdfs:comment "A request to undo the previous change." .
+
+ingen:BundleStart
+ a rdfs:Class ;
+ rdfs:label "Bundle Start" ;
+ rdfs:comment "The start of an undo transaction." .
+
+ingen:BundleEnd
+ a rdfs:Class ;
+ rdfs:label "Bundle End" ;
+ rdfs:comment "The end of an undo transaction." .
diff --git a/ingen/AtomWriter.hpp b/ingen/AtomWriter.hpp
index 6ae6bdbc..9ea5fa27 100644
--- a/ingen/AtomWriter.hpp
+++ b/ingen/AtomWriter.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -35,7 +35,8 @@ class INGEN_API AtomWriter : public Interface
{
public:
AtomWriter(URIMap& map, URIs& uris, AtomSink& sink);
- ~AtomWriter() {}
+
+ ~AtomWriter();
Raul::URI uri() const {
return Raul::URI("ingen:/clients/atom_writer");
@@ -74,6 +75,10 @@ public:
const Raul::URI& predicate,
const Atom& value);
+ void undo();
+
+ void redo();
+
void set_response_id(int32_t id);
void get(const Raul::URI& uri);
diff --git a/ingen/Interface.hpp b/ingen/Interface.hpp
index 8f6e25c4..a1a93c06 100644
--- a/ingen/Interface.hpp
+++ b/ingen/Interface.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -55,10 +55,14 @@ public:
virtual void set_respondee(SPtr<Interface> respondee) {}
- /** Begin an atomic bundle */
+ /** Begin a transaction.
+ *
+ * This does not guarantee strict atomicity, but the events in a bundle will be
+ * considered one operation, and they will all be undone at once.
+ */
virtual void bundle_begin() = 0;
- /** End (and send) an atomic bundle */
+ /** End a transaction. */
virtual void bundle_end() = 0;
virtual void put(const Raul::URI& uri,
@@ -90,6 +94,10 @@ public:
const Raul::URI& predicate,
const Atom& value) = 0;
+ virtual void undo() = 0;
+
+ virtual void redo() = 0;
+
/** Set the ID to use to respond to the next message.
* Setting the ID to 0 will disable responses.
*/
diff --git a/ingen/Resource.hpp b/ingen/Resource.hpp
index 31d23dac..604428ce 100644
--- a/ingen/Resource.hpp
+++ b/ingen/Resource.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -122,6 +122,15 @@ public:
Graph ctx = Graph::DEFAULT) {
insert(std::make_pair(key, Property(value, ctx)));
}
+
+ bool contains(const Raul::URI& key, const Atom& value) {
+ for (const_iterator i = find(key); i != end() && i->first == key; ++i) {
+ if (i->second == value) {
+ return true;
+ }
+ }
+ return false;
+ }
};
/** Get a single property value.
@@ -156,8 +165,10 @@ public:
* This will not remove any existing values, so if properties with
* predicate `uri` and values other than `value` exist, this will result
* in multiple values for the property.
+ *
+ * @return True iff a new property was added.
*/
- virtual void add_property(const Raul::URI& uri,
+ virtual bool add_property(const Raul::URI& uri,
const Atom& value,
Graph ctx = Graph::DEFAULT);
diff --git a/ingen/URIs.hpp b/ingen/URIs.hpp
index 290f8c94..52921085 100644
--- a/ingen/URIs.hpp
+++ b/ingen/URIs.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -100,9 +100,13 @@ public:
const Quark doap_name;
const Quark ingen_Arc;
const Quark ingen_Block;
+ const Quark ingen_BundleEnd;
+ const Quark ingen_BundleStart;
const Quark ingen_Graph;
const Quark ingen_GraphPrototype;
const Quark ingen_Internal;
+ const Quark ingen_Redo;
+ const Quark ingen_Undo;
const Quark ingen_activity;
const Quark ingen_arc;
const Quark ingen_block;
@@ -183,9 +187,9 @@ public:
const Quark patch_subject;
const Quark patch_value;
const Quark patch_wildcard;
+ const Quark pprops_logarithmic;
const Quark pset_Preset;
const Quark pset_preset;
- const Quark pprops_logarithmic;
const Quark rdf_type;
const Quark rdfs_Class;
const Quark rdfs_label;
diff --git a/ingen/client/ClientStore.hpp b/ingen/client/ClientStore.hpp
index 9f18f16d..de603458 100644
--- a/ingen/client/ClientStore.hpp
+++ b/ingen/client/ClientStore.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -102,6 +102,8 @@ public:
void del(const Raul::URI& uri);
+ void undo() {}
+ void redo() {}
void set_response_id(int32_t id) {}
void get(const Raul::URI& uri) {}
void response(int32_t id, Status status, const std::string& subject) {}
diff --git a/ingen/client/SigClientInterface.hpp b/ingen/client/SigClientInterface.hpp
index 72e34422..33430480 100644
--- a/ingen/client/SigClientInterface.hpp
+++ b/ingen/client/SigClientInterface.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -114,6 +114,8 @@ protected:
void set_property(const Raul::URI& subject, const Raul::URI& key, const Atom& value)
{ EMIT(property_change, subject, key, value); }
+ void undo() {}
+ void redo() {}
void set_response_id(int32_t id) {}
void get(const Raul::URI& uri) {}
};
diff --git a/ingen/ingen.h b/ingen/ingen.h
index d9091039..3af2042c 100644
--- a/ingen/ingen.h
+++ b/ingen/ingen.h
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2014-2015 David Robillard <http://drobilla.net/>
+ Copyright 2014-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -38,11 +38,15 @@
#define INGEN__Arc INGEN_NS "Arc"
#define INGEN__Block INGEN_NS "Block"
+#define INGEN__BundleEnd INGEN_NS "BundleEnd"
+#define INGEN__BundleStart INGEN_NS "BundleStart"
#define INGEN__Graph INGEN_NS "Graph"
#define INGEN__GraphPrototype INGEN_NS "GraphPrototype"
#define INGEN__Internal INGEN_NS "Internal"
#define INGEN__Node INGEN_NS "Node"
#define INGEN__Plugin INGEN_NS "Plugin"
+#define INGEN__Redo INGEN_NS "Redo"
+#define INGEN__Undo INGEN_NS "Undo"
#define INGEN__activity INGEN_NS "activity"
#define INGEN__arc INGEN_NS "arc"
#define INGEN__block INGEN_NS "block"
diff --git a/src/AtomReader.cpp b/src/AtomReader.cpp
index 9f0119d5..98847b73 100644
--- a/src/AtomReader.cpp
+++ b/src/AtomReader.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -158,6 +158,10 @@ AtomReader::write(const LV2_Atom* msg)
if (subject_uri && !body) {
_iface.del(*subject_uri);
return true;
+ } else if (obj->body.otype == _uris.ingen_BundleStart) {
+ _iface.bundle_begin();
+ } else if (obj->body.otype == _uris.ingen_BundleEnd) {
+ _iface.bundle_end();
} else if (body && body->body.otype == _uris.ingen_Arc) {
const LV2_Atom* tail = NULL;
const LV2_Atom* head = NULL;
diff --git a/src/AtomWriter.cpp b/src/AtomWriter.cpp
index 80f962ec..fe468da6 100644
--- a/src/AtomWriter.cpp
+++ b/src/AtomWriter.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -56,6 +56,11 @@ AtomWriter::AtomWriter(URIMap& map, URIs& uris, AtomSink& sink)
lv2_atom_forge_set_sink(&_forge, forge_sink, forge_deref, &_out);
}
+AtomWriter::~AtomWriter()
+{
+ free((void*)_out.buf);
+}
+
void
AtomWriter::finish_msg()
{
@@ -67,11 +72,17 @@ AtomWriter::finish_msg()
void
AtomWriter::bundle_begin()
{
+ LV2_Atom_Forge_Frame msg;
+ forge_request(&msg, _uris.ingen_BundleStart);
+ finish_msg();
}
void
AtomWriter::bundle_end()
{
+ LV2_Atom_Forge_Frame msg;
+ forge_request(&msg, _uris.ingen_BundleEnd);
+ finish_msg();
}
void
@@ -349,6 +360,44 @@ AtomWriter::set_property(const Raul::URI& subject,
}
/** @page protocol
+ * @subsection Undo
+ *
+ * Use [ingen:Undo](http://drobilla.net/ns/ingen#Undo) to undo the last change
+ * to the engine.
+ *
+ * @code{.ttl}
+ * [] a ingen:Undo .
+ * @endcode
+ */
+void
+AtomWriter::undo()
+{
+ LV2_Atom_Forge_Frame msg;
+ forge_request(&msg, _uris.ingen_Undo);
+ lv2_atom_forge_pop(&_forge, &msg);
+ finish_msg();
+}
+
+/** @page protocol
+ * @subsection Undo
+ *
+ * Use [ingen:Redo](http://drobilla.net/ns/ingen#Redo) to undo the last change
+ * to the engine.
+ *
+ * @code{.ttl}
+ * [] a ingen:Redo .
+ * @endcode
+ */
+void
+AtomWriter::redo()
+{
+ LV2_Atom_Forge_Frame msg;
+ forge_request(&msg, _uris.ingen_Redo);
+ lv2_atom_forge_pop(&_forge, &msg);
+ finish_msg();
+}
+
+/** @page protocol
* @subsection Get
*
* Use [patch:Get](http://lv2plug.in/ns/ext/patch#Get) to get the description
diff --git a/src/Resource.cpp b/src/Resource.cpp
index cbb9c3b7..46cd29f8 100644
--- a/src/Resource.cpp
+++ b/src/Resource.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -24,7 +24,7 @@ using namespace std;
namespace Ingen {
-void
+bool
Resource::add_property(const Raul::URI& uri,
const Atom& value,
Graph ctx)
@@ -34,12 +34,13 @@ Resource::add_property(const Raul::URI& uri,
const std::pair<iterator, iterator> range = _properties.equal_range(uri);
for (iterator i = range.first; i != range.second && i != _properties.end(); ++i) {
if (i->second == value && i->second.context() == ctx) {
- return;
+ return false;
}
}
const Atom& v = _properties.insert(make_pair(uri, Property(value, ctx)))->second;
on_property(uri, v);
+ return true;
}
const Atom&
@@ -102,13 +103,7 @@ Resource::remove_property(const Raul::URI& uri, const URIs::Quark& value)
bool
Resource::has_property(const Raul::URI& uri, const Atom& value) const
{
- Properties::const_iterator i = _properties.find(uri);
- for (; (i != _properties.end()) && (i->first == uri); ++i) {
- if (i->second == value) {
- return true;
- }
- }
- return false;
+ return _properties.contains(uri, value);
}
bool
diff --git a/src/URIs.cpp b/src/URIs.cpp
index 1771ee8a..005490c4 100644
--- a/src/URIs.cpp
+++ b/src/URIs.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -83,9 +83,13 @@ URIs::URIs(Forge& f, URIMap* map, LilvWorld* lworld)
, doap_name (forge, map, lworld, "http://usefulinc.com/ns/doap#name")
, ingen_Arc (forge, map, lworld, INGEN__Arc)
, ingen_Block (forge, map, lworld, INGEN__Block)
+ , ingen_BundleEnd (forge, map, lworld, INGEN__BundleEnd)
+ , ingen_BundleStart (forge, map, lworld, INGEN__BundleStart)
, ingen_Graph (forge, map, lworld, INGEN__Graph)
, ingen_GraphPrototype (forge, map, lworld, INGEN__GraphPrototype)
, ingen_Internal (forge, map, lworld, INGEN__Internal)
+ , ingen_Redo (forge, map, lworld, INGEN__Redo)
+ , ingen_Undo (forge, map, lworld, INGEN__Undo)
, ingen_activity (forge, map, lworld, INGEN__activity)
, ingen_arc (forge, map, lworld, INGEN__arc)
, ingen_block (forge, map, lworld, INGEN__block)
@@ -166,9 +170,9 @@ URIs::URIs(Forge& f, URIMap* map, LilvWorld* lworld)
, patch_subject (forge, map, lworld, LV2_PATCH__subject)
, patch_value (forge, map, lworld, LV2_PATCH__value)
, patch_wildcard (forge, map, lworld, LV2_PATCH__wildcard)
+ , pprops_logarithmic (forge, map, lworld, LV2_PORT_PROPS__logarithmic)
, pset_Preset (forge, map, lworld, LV2_PRESETS__Preset)
, pset_preset (forge, map, lworld, LV2_PRESETS__preset)
- , pprops_logarithmic (forge, map, lworld, LV2_PORT_PROPS__logarithmic)
, rdf_type (forge, map, lworld, NS_RDF "type")
, rdfs_Class (forge, map, lworld, NS_RDFS "Class")
, rdfs_label (forge, map, lworld, NS_RDFS "label")
diff --git a/src/gui/GraphBox.cpp b/src/gui/GraphBox.cpp
index 52ec9672..10f063cc 100644
--- a/src/gui/GraphBox.cpp
+++ b/src/gui/GraphBox.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -77,6 +77,8 @@ GraphBox::GraphBox(BaseObjectType* cobject,
xml->get_widget("graph_save_menuitem", _menu_save);
xml->get_widget("graph_save_as_menuitem", _menu_save_as);
xml->get_widget("graph_export_image_menuitem", _menu_export_image);
+ xml->get_widget("graph_redo_menuitem", _menu_redo);
+ xml->get_widget("graph_undo_menuitem", _menu_undo);
xml->get_widget("graph_cut_menuitem", _menu_cut);
xml->get_widget("graph_copy_menuitem", _menu_copy);
xml->get_widget("graph_paste_menuitem", _menu_paste);
@@ -119,6 +121,10 @@ GraphBox::GraphBox(BaseObjectType* cobject,
sigc::mem_fun(this, &GraphBox::event_save_as));
_menu_export_image->signal_activate().connect(
sigc::mem_fun(this, &GraphBox::event_export_image));
+ _menu_redo->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_redo));
+ _menu_undo->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_undo));
_menu_copy->signal_activate().connect(
sigc::mem_fun(this, &GraphBox::event_copy));
_menu_paste->signal_activate().connect(
@@ -671,6 +677,18 @@ GraphBox::event_copy()
}
void
+GraphBox::event_redo()
+{
+ _app->interface()->redo();
+}
+
+void
+GraphBox::event_undo()
+{
+ _app->interface()->undo();
+}
+
+void
GraphBox::event_paste()
{
if (_view)
@@ -748,7 +766,9 @@ GraphBox::event_normal_font_size()
void
GraphBox::event_arrange()
{
+ _app->interface()->bundle_begin();
_view->canvas()->arrange();
+ _app->interface()->bundle_end();
}
void
diff --git a/src/gui/GraphBox.hpp b/src/gui/GraphBox.hpp
index ab3ee5ff..0a55227e 100644
--- a/src/gui/GraphBox.hpp
+++ b/src/gui/GraphBox.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -108,6 +108,8 @@ private:
void event_save();
void event_save_as();
void event_export_image();
+ void event_redo();
+ void event_undo();
void event_copy();
void event_paste();
void event_delete();
@@ -148,6 +150,8 @@ private:
Gtk::MenuItem* _menu_save;
Gtk::MenuItem* _menu_save_as;
Gtk::MenuItem* _menu_export_image;
+ Gtk::MenuItem* _menu_redo;
+ Gtk::MenuItem* _menu_undo;
Gtk::MenuItem* _menu_cut;
Gtk::MenuItem* _menu_copy;
Gtk::MenuItem* _menu_paste;
diff --git a/src/gui/GraphCanvas.cpp b/src/gui/GraphCanvas.cpp
index 7e237a1c..df950eb7 100644
--- a/src/gui/GraphCanvas.cpp
+++ b/src/gui/GraphCanvas.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -586,8 +586,10 @@ destroy_arc(GanvEdge* arc, void* data)
void
GraphCanvas::destroy_selection()
{
+ _app.interface()->bundle_begin();
for_each_selected_edge(destroy_arc, &_app);
for_each_selected_node(destroy_node, &_app);
+ _app.interface()->bundle_end();
}
static void
diff --git a/src/gui/ingen_gui.ui b/src/gui/ingen_gui.ui
index 69fed01a..646cd34d 100644
--- a/src/gui/ingen_gui.ui
+++ b/src/gui/ingen_gui.ui
@@ -864,6 +864,32 @@ See COPYING file included with this distribution, or http://www.gnu.org/licenses
<object class="GtkMenu" id="edit2_menu">
<property name="can_focus">False</property>
<child>
+ <object class="GtkImageMenuItem" id="graph_undo_menuitem">
+ <property name="label">gtk-undo</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="sensitive">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="Z" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_undo_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_redo_menuitem">
+ <property name="label">gtk-redo</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="sensitive">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="Z" signal="activate" modifiers="GDK_CONTROL_MASK|GDK_SHIFT_MASK"/>
+ <signal name="activate" handler="on_graph_redo_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
<object class="GtkSeparatorMenuItem" id="menuitem5">
<property name="visible">True</property>
<property name="can_focus">False</property>
diff --git a/src/server/Broadcaster.hpp b/src/server/Broadcaster.hpp
index f7952a31..9efb9c30 100644
--- a/src/server/Broadcaster.hpp
+++ b/src/server/Broadcaster.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -148,6 +148,8 @@ public:
Raul::URI uri() const { return Raul::URI("ingen:/broadcaster"); }
+ void undo() {} ///< N/A
+ void redo() {} ///< N/A
void set_response_id(int32_t id) {} ///< N/A
void get(const Raul::URI& uri) {} ///< N/A
void response(int32_t id, Status status, const std::string& subject) {} ///< N/A
diff --git a/src/server/Engine.cpp b/src/server/Engine.cpp
index 7ab7e315..beb38a24 100644
--- a/src/server/Engine.cpp
+++ b/src/server/Engine.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -24,6 +24,7 @@
#include "lv2/lv2plug.in/ns/ext/state/state.h"
#include "events/CreateGraph.hpp"
+#include "ingen/AtomReader.hpp"
#include "ingen/Configuration.hpp"
#include "ingen/Log.hpp"
#include "ingen/Store.hpp"
@@ -47,6 +48,7 @@
#include "PreProcessor.hpp"
#include "ProcessContext.hpp"
#include "ThreadManager.hpp"
+#include "UndoStack.hpp"
#include "Worker.hpp"
#ifdef HAVE_SOCKET
#include "SocketListener.hpp"
@@ -67,9 +69,12 @@ Engine::Engine(Ingen::World* world)
, _buffer_factory(new BufferFactory(*this, world->uris()))
, _control_bindings(NULL)
, _event_writer(new EventWriter(*this))
+ , _atom_interface(new AtomReader(world->uri_map(), world->uris(), world->log(), *_event_writer))
, _maid(new Raul::Maid())
, _options(new LV2Options(world->uris()))
- , _pre_processor(new PreProcessor())
+ , _undo_stack(new UndoStack(_world->uris(), _world->uri_map()))
+ , _redo_stack(new UndoStack(_world->uris(), _world->uri_map()))
+ , _pre_processor(new PreProcessor(*this))
, _post_processor(new PostProcessor(*this))
, _root_graph(NULL)
, _worker(new Worker(world->log(), event_queue_size()))
@@ -134,6 +139,7 @@ Engine::~Engine()
#endif
delete _pre_processor;
delete _post_processor;
+ delete _undo_stack;
delete _block_factory;
delete _control_bindings;
delete _broadcaster;
@@ -307,9 +313,9 @@ Engine::pending_events()
}
void
-Engine::enqueue_event(Event* ev)
+Engine::enqueue_event(Event* ev, Event::Mode mode)
{
- _pre_processor->event(ev);
+ _pre_processor->event(ev, mode);
}
unsigned
diff --git a/src/server/Engine.hpp b/src/server/Engine.hpp
index 4a4d6a56..d4ff4420 100644
--- a/src/server/Engine.hpp
+++ b/src/server/Engine.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -26,12 +26,14 @@
#include "ingen/ingen.h"
#include "ingen/types.hpp"
+#include "Event.hpp"
#include "ProcessContext.hpp"
namespace Raul { class Maid; }
namespace Ingen {
+class AtomReader;
class Store;
class World;
@@ -42,7 +44,6 @@ class Broadcaster;
class BufferFactory;
class ControlBindings;
class Driver;
-class Event;
class EventWriter;
class GraphImpl;
class LV2Options;
@@ -50,6 +51,7 @@ class PostProcessor;
class PreProcessor;
class ProcessContext;
class SocketListener;
+class UndoStack;
class Worker;
/**
@@ -89,7 +91,7 @@ public:
SampleCount event_time();
/** Enqueue an event to be processed (non-realtime threads only). */
- void enqueue_event(Event* ev);
+ void enqueue_event(Event* ev, Event::Mode mode=Event::Mode::NORMAL);
/** Process events (process thread only). */
unsigned process_events();
@@ -101,6 +103,7 @@ public:
Ingen::World* world() const { return _world; }
EventWriter* interface() const { return _event_writer; }
+ AtomReader* atom_interface() const { return _atom_interface; }
BlockFactory* block_factory() const { return _block_factory; }
Broadcaster* broadcaster() const { return _broadcaster; }
BufferFactory* buffer_factory() const { return _buffer_factory; }
@@ -110,6 +113,8 @@ public:
GraphImpl* root_graph() const { return _root_graph; }
PostProcessor* post_processor() const { return _post_processor; }
Raul::Maid* maid() const { return _maid; }
+ UndoStack* undo_stack() const { return _undo_stack; }
+ UndoStack* redo_stack() const { return _redo_stack; }
Worker* worker() const { return _worker; }
ProcessContext& process_context() { return _process_context; }
@@ -127,8 +132,11 @@ private:
ControlBindings* _control_bindings;
SPtr<Driver> _driver;
EventWriter* _event_writer;
+ AtomReader* _atom_interface;
Raul::Maid* _maid;
SPtr<LV2Options> _options;
+ UndoStack* _undo_stack;
+ UndoStack* _redo_stack;
PreProcessor* _pre_processor;
PostProcessor* _post_processor;
GraphImpl* _root_graph;
diff --git a/src/server/Event.hpp b/src/server/Event.hpp
index 77249262..15ce386b 100644
--- a/src/server/Event.hpp
+++ b/src/server/Event.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -53,6 +53,9 @@ class Event : public Raul::Deletable, public Raul::Noncopyable
public:
virtual ~Event() {}
+ /** Event mode to distinguish normal events from undo events. */
+ enum class Mode { NORMAL, UNDO, REDO };
+
/** Pre-process event before execution (non-realtime). */
virtual bool pre_process() = 0;
@@ -62,6 +65,9 @@ public:
/** Post-process event after execution (non-realtime). */
virtual void post_process() = 0;
+ /** Write the inverse of this event to `sink`. */
+ virtual void undo(Interface& target) {}
+
/** Return true iff this event has been pre-processed. */
inline bool is_prepared() const { return _status != Status::NOT_PREPARED; }
@@ -80,6 +86,14 @@ public:
/** Return the status (success or error code) of this event. */
Status status() const { return _status; }
+ /** Return true iff this is a generated undo event. */
+ Mode get_mode() const { return _mode; }
+
+ /** Flag this event as a generated undo event. */
+ void set_mode(Mode mode) { _mode = mode; }
+
+ inline Engine& engine() { return _engine; }
+
protected:
Event(Engine& engine, SPtr<Interface> client, int32_t id, FrameTime time)
: _engine(engine)
@@ -88,6 +102,7 @@ protected:
, _request_id(id)
, _time(time)
, _status(Status::NOT_PREPARED)
+ , _mode(Mode::NORMAL)
{}
/** Constructor for internal events only */
@@ -97,6 +112,7 @@ protected:
, _request_id(0)
, _time(0)
, _status(Status::NOT_PREPARED)
+ , _mode(Mode::NORMAL)
{}
inline bool pre_process_done(Status st) {
@@ -128,6 +144,7 @@ protected:
FrameTime _time;
Status _status;
std::string _err_subject;
+ Mode _mode;
};
} // namespace Server
diff --git a/src/server/EventWriter.cpp b/src/server/EventWriter.cpp
index 9732f04c..5aecf2db 100644
--- a/src/server/EventWriter.cpp
+++ b/src/server/EventWriter.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -28,6 +28,7 @@ namespace Server {
EventWriter::EventWriter(Engine& engine)
: _engine(engine)
, _request_id(0)
+ , _event_mode(Event::Mode::NORMAL)
{
}
@@ -48,13 +49,32 @@ EventWriter::set_response_id(int32_t id)
}
void
+EventWriter::bundle_begin()
+{
+ _engine.enqueue_event(
+ new Events::Mark(_engine, _respondee, _request_id, now(),
+ Events::Mark::Type::BUNDLE_START),
+ _event_mode);
+}
+
+void
+EventWriter::bundle_end()
+{
+ _engine.enqueue_event(
+ new Events::Mark(_engine, _respondee, _request_id, now(),
+ Events::Mark::Type::BUNDLE_END),
+ _event_mode);
+}
+
+void
EventWriter::put(const Raul::URI& uri,
const Resource::Properties& properties,
const Resource::Graph ctx)
{
_engine.enqueue_event(
new Events::Delta(_engine, _respondee, _request_id, now(),
- Events::Delta::Type::PUT, ctx, uri, properties));
+ Events::Delta::Type::PUT, ctx, uri, properties),
+ _event_mode);
}
void
@@ -65,7 +85,8 @@ EventWriter::delta(const Raul::URI& uri,
_engine.enqueue_event(
new Events::Delta(_engine, _respondee, _request_id, now(),
Events::Delta::Type::PATCH, Resource::Graph::DEFAULT,
- uri, add, remove));
+ uri, add, remove),
+ _event_mode);
}
void
@@ -74,7 +95,8 @@ EventWriter::copy(const Raul::URI& old_uri,
{
_engine.enqueue_event(
new Events::Copy(_engine, _respondee, _request_id, now(),
- old_uri, new_uri));
+ old_uri, new_uri),
+ _event_mode);
}
void
@@ -83,14 +105,16 @@ EventWriter::move(const Raul::Path& old_path,
{
_engine.enqueue_event(
new Events::Move(_engine, _respondee, _request_id, now(),
- old_path, new_path));
+ old_path, new_path),
+ _event_mode);
}
void
EventWriter::del(const Raul::URI& uri)
{
_engine.enqueue_event(
- new Events::Delete(_engine, _respondee, _request_id, now(), uri));
+ new Events::Delete(_engine, _respondee, _request_id, now(), uri),
+ _event_mode);
}
void
@@ -99,7 +123,8 @@ EventWriter::connect(const Raul::Path& tail_path,
{
_engine.enqueue_event(
new Events::Connect(_engine, _respondee, _request_id, now(),
- tail_path, head_path));
+ tail_path, head_path),
+ _event_mode);
}
@@ -109,7 +134,8 @@ EventWriter::disconnect(const Raul::Path& src,
{
_engine.enqueue_event(
new Events::Disconnect(_engine, _respondee, _request_id, now(),
- src, dst));
+ src, dst),
+ _event_mode);
}
void
@@ -118,7 +144,8 @@ EventWriter::disconnect_all(const Raul::Path& graph,
{
_engine.enqueue_event(
new Events::DisconnectAll(_engine, _respondee, _request_id, now(),
- graph, path));
+ graph, path),
+ _event_mode);
}
void
@@ -128,15 +155,33 @@ EventWriter::set_property(const Raul::URI& uri,
{
_engine.enqueue_event(
new Events::Delta(_engine, _respondee, _request_id, now(),
- Events::Delta::Type::PUT, Resource::Graph::DEFAULT,
- uri, {{predicate, value}}, {}));
+ Events::Delta::Type::SET, Resource::Graph::DEFAULT,
+ uri, {{predicate, value}}, {}),
+ _event_mode);
+}
+
+void
+EventWriter::undo()
+{
+ _engine.enqueue_event(
+ new Events::Undo(_engine, _respondee, _request_id, now(), false),
+ _event_mode);
+}
+
+void
+EventWriter::redo()
+{
+ _engine.enqueue_event(
+ new Events::Undo(_engine, _respondee, _request_id, now(), true),
+ _event_mode);
}
void
EventWriter::get(const Raul::URI& uri)
{
_engine.enqueue_event(
- new Events::Get(_engine, _respondee, _request_id, now(), uri));
+ new Events::Get(_engine, _respondee, _request_id, now(), uri),
+ _event_mode);
}
} // namespace Server
diff --git a/src/server/EventWriter.hpp b/src/server/EventWriter.hpp
index 15a144ef..7b9b920b 100644
--- a/src/server/EventWriter.hpp
+++ b/src/server/EventWriter.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -25,6 +25,7 @@
#include "ingen/Resource.hpp"
#include "ingen/types.hpp"
+#include "Event.hpp"
#include "types.hpp"
namespace Ingen {
@@ -52,9 +53,9 @@ public:
virtual void set_response_id(int32_t id);
- virtual void bundle_begin() {}
+ virtual void bundle_begin();
- virtual void bundle_end() {}
+ virtual void bundle_end();
virtual void put(const Raul::URI& path,
const Resource::Properties& properties,
@@ -85,16 +86,23 @@ public:
virtual void disconnect_all(const Raul::Path& graph,
const Raul::Path& path);
+ virtual void undo();
+
+ virtual void redo();
+
virtual void get(const Raul::URI& uri);
virtual void response(int32_t id, Status status, const std::string& subject) {} ///< N/A
virtual void error(const std::string& msg) {} ///< N/A
+ void set_event_mode(Event::Mode mode) { _event_mode = mode; }
+
protected:
Engine& _engine;
SPtr<Interface> _respondee;
int32_t _request_id;
+ Event::Mode _event_mode;
private:
SampleCount now() const;
diff --git a/src/server/PreProcessor.cpp b/src/server/PreProcessor.cpp
index e26ebcdc..d57bb6b9 100644
--- a/src/server/PreProcessor.cpp
+++ b/src/server/PreProcessor.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -16,19 +16,25 @@
#include <stdexcept>
+#include "ingen/AtomSink.hpp"
+#include "ingen/AtomWriter.hpp"
+
+#include "Engine.hpp"
#include "Event.hpp"
#include "PostProcessor.hpp"
#include "PreProcessor.hpp"
#include "ProcessContext.hpp"
#include "ThreadManager.hpp"
+#include "UndoStack.hpp"
using namespace std;
namespace Ingen {
namespace Server {
-PreProcessor::PreProcessor()
- : _sem(0)
+PreProcessor::PreProcessor(Engine& engine)
+ : _engine(engine)
+ , _sem(0)
, _head(NULL)
, _prepared_back(NULL)
, _tail(NULL)
@@ -46,7 +52,7 @@ PreProcessor::~PreProcessor()
}
void
-PreProcessor::event(Event* const ev)
+PreProcessor::event(Event* const ev, Event::Mode mode)
{
// TODO: Probably possible to make this lock-free with CAS
ThreadManager::assert_not_thread(THREAD_IS_REAL_TIME);
@@ -54,6 +60,7 @@ PreProcessor::event(Event* const ev)
assert(!ev->is_prepared());
assert(!ev->next());
+ ev->set_mode(mode);
/* Note that tail is only used here, not in process(). The head must be
checked first here, since if it is NULL the tail pointer is junk. */
@@ -113,6 +120,13 @@ PreProcessor::process(ProcessContext& context, PostProcessor& dest, size_t limit
void
PreProcessor::run()
{
+ UndoStack& undo_stack = *_engine.undo_stack();
+ UndoStack& redo_stack = *_engine.redo_stack();
+ AtomWriter undo_writer(
+ _engine.world()->uri_map(), _engine.world()->uris(), undo_stack);
+ AtomWriter redo_writer(
+ _engine.world()->uri_map(), _engine.world()->uris(), redo_stack);
+
ThreadManager::set_flag(THREAD_PRE_PROCESS);
while (!_exit_flag) {
if (!_sem.timed_wait(1000)) {
@@ -125,7 +139,23 @@ PreProcessor::run()
}
assert(!ev->is_prepared());
- ev->pre_process();
+ if (ev->pre_process()) {
+ switch (ev->get_mode()) {
+ case Event::Mode::NORMAL:
+ case Event::Mode::REDO:
+ undo_stack.start_entry();
+ ev->undo(undo_writer);
+ undo_stack.finish_entry();
+ // undo_stack.save(stderr);
+ break;
+ case Event::Mode::UNDO:
+ redo_stack.start_entry();
+ ev->undo(redo_writer);
+ redo_stack.finish_entry();
+ // redo_stack.save(stderr, "redo");
+ break;
+ }
+ }
assert(ev->is_prepared());
_prepared_back = (Event*)ev->next();
diff --git a/src/server/PreProcessor.hpp b/src/server/PreProcessor.hpp
index 586d6dd8..9ef75473 100644
--- a/src/server/PreProcessor.hpp
+++ b/src/server/PreProcessor.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -26,6 +26,7 @@
namespace Ingen {
namespace Server {
+class Engine;
class Event;
class PostProcessor;
class ProcessContext;
@@ -33,7 +34,7 @@ class ProcessContext;
class PreProcessor
{
public:
- explicit PreProcessor();
+ explicit PreProcessor(Engine& engine);
~PreProcessor();
@@ -43,7 +44,7 @@ public:
/** Enqueue an event.
* This is safe to call from any non-realtime thread (it locks).
*/
- void event(Event* ev);
+ void event(Event* ev, Event::Mode mode);
/** Process events for a cycle.
* @return The number of events processed.
@@ -56,6 +57,7 @@ protected:
void run();
private:
+ Engine& _engine;
std::mutex _mutex;
Raul::Semaphore _sem;
std::atomic<Event*> _head;
diff --git a/src/server/UndoStack.cpp b/src/server/UndoStack.cpp
new file mode 100644
index 00000000..f8a7f37b
--- /dev/null
+++ b/src/server/UndoStack.cpp
@@ -0,0 +1,252 @@
+/*
+ This file is part of Ingen.
+ Copyright 2016 David Robillard <http://drobilla.net/>
+
+ Ingen is free software: you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or any later version.
+
+ Ingen is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Ingen. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <ctime>
+#include <iostream>
+
+#include "ingen/URIMap.hpp"
+#include "ingen/URIs.hpp"
+#include "lv2/lv2plug.in/ns/ext/atom/util.h"
+#include "lv2/lv2plug.in/ns/ext/patch/patch.h"
+#include "serd/serd.h"
+#include "sratom/sratom.h"
+
+#include "UndoStack.hpp"
+
+#define NS_RDF (const uint8_t*)"http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+
+#define USTR(s) ((const uint8_t*)(s))
+
+namespace Ingen {
+namespace Server {
+
+void
+UndoStack::start_entry()
+{
+ if (_depth == 0) {
+ time_t now;
+ time(&now);
+ _stack.push_back(Entry(now));
+ }
+ ++_depth;
+}
+
+bool
+UndoStack::write(const LV2_Atom* msg)
+{
+ _stack.back().push_event(msg);
+ return true;
+}
+
+bool
+UndoStack::ignore_later_event(const LV2_Atom* first,
+ const LV2_Atom* second) const
+{
+ if (first->type != _uris.atom_Object || first->type != second->type) {
+ return false;
+ }
+
+ const LV2_Atom_Object* f = (const LV2_Atom_Object*)first;
+ const LV2_Atom_Object* s = (const LV2_Atom_Object*)second;
+ if (f->body.otype == _uris.patch_Set && f->body.otype == s->body.otype) {
+ const LV2_Atom* f_subject = NULL;
+ const LV2_Atom* f_property = NULL;
+ const LV2_Atom* s_subject = NULL;
+ const LV2_Atom* s_property = NULL;
+ lv2_atom_object_get(f,
+ (LV2_URID)_uris.patch_subject, &f_subject,
+ (LV2_URID)_uris.patch_property, &f_property,
+ 0);
+ lv2_atom_object_get(s,
+ (LV2_URID)_uris.patch_subject, &s_subject,
+ (LV2_URID)_uris.patch_property, &s_property,
+ 0);
+ return (lv2_atom_equals(f_subject, s_subject) &&
+ lv2_atom_equals(f_property, s_property));
+ }
+
+ return false;
+}
+
+void
+UndoStack::finish_entry()
+{
+ if (--_depth > 0) {
+ return;
+ } else if (_stack.back().events.empty()) {
+ // Disregard empty entry
+ _stack.pop_back();
+ } else if (_stack.size() > 1 && _stack.back().events.size() == 1) {
+ // This entry and the previous one have one event, attempt to merge
+ auto i = _stack.rbegin();
+ ++i;
+ if (i->events.size() == 1) {
+ if (ignore_later_event(i->events[0], _stack.back().events[0])) {
+ _stack.pop_back();
+ }
+ }
+ }
+}
+
+UndoStack::Entry
+UndoStack::pop()
+{
+ Entry top;
+ if (!_stack.empty()) {
+ top = _stack.back();
+ _stack.pop_back();
+ }
+ return top;
+}
+
+struct BlankIDs {
+ BlankIDs(char c='b') : n(0), c(c) {}
+
+ SerdNode get() {
+ snprintf(buf, sizeof(buf), "%c%u", c, n++);
+ return serd_node_from_string(SERD_BLANK, USTR(buf));
+ }
+
+ char buf[16];
+ unsigned n;
+ const char c;
+};
+
+struct ListContext {
+ explicit ListContext(BlankIDs& ids, unsigned flags, const SerdNode* s, const SerdNode* p)
+ : ids(ids)
+ , s(*s)
+ , p(*p)
+ , flags(flags | SERD_LIST_O_BEGIN)
+ {}
+
+ SerdNode start_node(SerdWriter* writer) {
+ const SerdNode node = ids.get();
+ serd_writer_write_statement(writer, flags, NULL, &s, &p, &node, NULL, NULL);
+ return node;
+ }
+
+ void append(SerdWriter* writer, unsigned oflags, const SerdNode* value) {
+ // s p node
+ const SerdNode node = start_node(writer);
+
+ // node rdf:first value
+ p = serd_node_from_string(SERD_URI, NS_RDF "first");
+ flags = SERD_LIST_CONT;
+ serd_writer_write_statement(writer, flags|oflags, NULL, &node, &p, value, NULL, NULL);
+
+ end_node(writer, &node);
+ }
+
+ void end_node(SerdWriter* writer, const SerdNode* node) {
+ // Prepare for next call: node rdf:rest ...
+ s = *node;
+ p = serd_node_from_string(SERD_URI, NS_RDF "rest");
+ }
+
+ void end(SerdWriter* writer) {
+ const SerdNode nil = serd_node_from_string(SERD_URI, NS_RDF "nil");
+ serd_writer_write_statement(writer, flags, NULL, &s, &p, &nil, NULL, NULL);
+ }
+
+ BlankIDs& ids;
+ SerdNode s;
+ SerdNode p;
+ unsigned flags;
+};
+
+void
+UndoStack::write_entry(Sratom* sratom,
+ SerdWriter* writer,
+ const SerdNode* const subject,
+ const UndoStack::Entry& entry)
+{
+ char time_str[24];
+ strftime(time_str, sizeof(time_str), "%FT%T", gmtime(&entry.time));
+
+ // entry rdf:type ingen:UndoEntry
+ SerdNode p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "time"));
+ SerdNode o = serd_node_from_string(SERD_LITERAL, USTR(time_str));
+ serd_writer_write_statement(writer, SERD_ANON_CONT, NULL, subject, &p, &o, NULL, NULL);
+
+ p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "events"));
+
+ BlankIDs ids('e');
+ ListContext ctx(ids, SERD_ANON_CONT, subject, &p);
+
+ for (const LV2_Atom* atom : entry.events) {
+ const SerdNode node = ctx.start_node(writer);
+
+ p = serd_node_from_string(SERD_URI, NS_RDF "first");
+ ctx.flags = SERD_LIST_CONT;
+ sratom_write(sratom, &_map.urid_unmap_feature()->urid_unmap, SERD_LIST_CONT,
+ &node, &p,
+ atom->type, atom->size, LV2_ATOM_BODY_CONST(atom));
+
+ ctx.end_node(writer, &node);
+ }
+
+ ctx.end(writer);
+}
+
+void
+UndoStack::save(FILE* stream, const char* name)
+{
+ SerdEnv* env = serd_env_new(NULL);
+ serd_env_set_prefix_from_strings(env, USTR("atom"), USTR(LV2_ATOM_PREFIX));
+ serd_env_set_prefix_from_strings(env, USTR("ingen"), USTR(INGEN_NS));
+ serd_env_set_prefix_from_strings(env, USTR("patch"), USTR(LV2_PATCH_PREFIX));
+
+ const SerdNode base = serd_node_from_string(SERD_URI, USTR("ingen:/"));
+ SerdURI base_uri;
+ serd_uri_parse(base.buf, &base_uri);
+
+ SerdWriter* writer = serd_writer_new(
+ SERD_TURTLE,
+ (SerdStyle)(SERD_STYLE_RESOLVED|SERD_STYLE_ABBREVIATED|SERD_STYLE_CURIED),
+ env,
+ &base_uri,
+ serd_file_sink,
+ stream);
+
+ // Configure sratom to write directly to the writer (and thus the socket)
+ Sratom* sratom = sratom_new(&_map.urid_map_feature()->urid_map);
+ sratom_set_sink(sratom,
+ (const char*)base.buf,
+ (SerdStatementSink)serd_writer_write_statement,
+ (SerdEndSink)serd_writer_end_anon,
+ writer);
+
+ SerdNode s = serd_node_from_string(SERD_BLANK, (const uint8_t*)name);
+ SerdNode p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "entries"));
+
+ BlankIDs ids('u');
+ ListContext ctx(ids, 0, &s, &p);
+ for (const Entry& e : _stack) {
+ const SerdNode entry = ids.get();
+ ctx.append(writer, SERD_ANON_O_BEGIN, &entry);
+ write_entry(sratom, writer, &entry, e);
+ serd_writer_end_anon(writer, &entry);
+ }
+ ctx.end(writer);
+
+ sratom_free(sratom);
+ serd_writer_finish(writer);
+ serd_writer_free(writer);
+}
+
+} // namespace Server
+} // namespace Ingen
diff --git a/src/server/UndoStack.hpp b/src/server/UndoStack.hpp
new file mode 100644
index 00000000..aeb18529
--- /dev/null
+++ b/src/server/UndoStack.hpp
@@ -0,0 +1,107 @@
+/*
+ This file is part of Ingen.
+ Copyright 2016 David Robillard <http://drobilla.net/>
+
+ Ingen is free software: you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or any later version.
+
+ Ingen is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Ingen. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef INGEN_ENGINE_UNDOSTACK_HPP
+#define INGEN_ENGINE_UNDOSTACK_HPP
+
+#include <ctime>
+#include <deque>
+
+#include "ingen/AtomSink.hpp"
+#include "ingen/ingen.h"
+#include "lv2/lv2plug.in/ns/ext/atom/atom.h"
+#include "serd/serd.h"
+#include "sratom/sratom.h"
+
+namespace Ingen {
+
+class URIMap;
+class URIs;
+
+namespace Server {
+
+class INGEN_API UndoStack : public AtomSink {
+public:
+ struct Entry {
+ Entry(time_t time=0) : time(time) {}
+
+ Entry(const Entry& copy)
+ : time(copy.time)
+ {
+ for (const LV2_Atom* ev : copy.events) {
+ push_event(ev);
+ }
+ }
+
+ ~Entry() { clear(); }
+
+ Entry& operator=(const Entry& rhs) {
+ clear();
+ time = rhs.time;
+ for (const LV2_Atom* ev : rhs.events) {
+ push_event(ev);
+ }
+ return *this;
+ }
+
+ void clear() {
+ for (LV2_Atom* ev : events) {
+ free(ev);
+ }
+ events.clear();
+ }
+
+ void push_event(const LV2_Atom* ev) {
+ const uint32_t size = lv2_atom_total_size(ev);
+ LV2_Atom* copy = (LV2_Atom*)malloc(size);
+ memcpy(copy, ev, size);
+ events.push_back(copy);
+ }
+
+ time_t time;
+ std::vector<LV2_Atom*> events;
+ };
+
+ UndoStack(URIs& uris, URIMap& map) : _uris(uris), _map(map), _depth(0) {}
+
+ void start_entry();
+ bool write(const LV2_Atom* msg);
+ void finish_entry();
+
+ bool empty() const { return _stack.empty(); }
+ Entry pop();
+
+ void save(FILE* stream, const char* name="undo");
+
+private:
+ bool ignore_later_event(const LV2_Atom* first,
+ const LV2_Atom* second) const;
+
+ void write_entry(Sratom* sratom,
+ SerdWriter* writer,
+ const SerdNode* subject,
+ const Entry& entry);
+
+ URIs& _uris;
+ URIMap& _map;
+ std::deque<Entry> _stack;
+ int _depth;
+};
+
+} // namespace Server
+} // namespace Ingen
+
+#endif // INGEN_ENGINE_UNDOSTACK_HPP
diff --git a/src/server/events.hpp b/src/server/events.hpp
index bbe0aa64..5f77b431 100644
--- a/src/server/events.hpp
+++ b/src/server/events.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -18,6 +18,7 @@
#define INGEN_ENGINE_EVENTS_HPP
#include "events/Connect.hpp"
+#include "events/Copy.hpp"
#include "events/CreateBlock.hpp"
#include "events/CreateGraph.hpp"
#include "events/CreatePort.hpp"
@@ -26,8 +27,9 @@
#include "events/Disconnect.hpp"
#include "events/DisconnectAll.hpp"
#include "events/Get.hpp"
+#include "events/Mark.hpp"
#include "events/Move.hpp"
-#include "events/Copy.hpp"
#include "events/SetPortValue.hpp"
+#include "events/Undo.hpp"
#endif // INGEN_ENGINE_EVENTS_HPP
diff --git a/src/server/events/Connect.cpp b/src/server/events/Connect.cpp
index 8880322d..f0ba39bb 100644
--- a/src/server/events/Connect.cpp
+++ b/src/server/events/Connect.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -168,6 +168,12 @@ Connect::post_process()
}
}
+void
+Connect::undo(Interface& target)
+{
+ target.disconnect(_tail_path, _head_path);
+}
+
} // namespace Events
} // namespace Server
} // namespace Ingen
diff --git a/src/server/events/Connect.hpp b/src/server/events/Connect.hpp
index f6b6cccc..bd15d6d3 100644
--- a/src/server/events/Connect.hpp
+++ b/src/server/events/Connect.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -55,6 +55,7 @@ public:
bool pre_process();
void execute(ProcessContext& context);
void post_process();
+ void undo(Interface& target);
private:
const Raul::Path _tail_path;
diff --git a/src/server/events/Copy.cpp b/src/server/events/Copy.cpp
index eed68d75..34a63e58 100644
--- a/src/server/events/Copy.cpp
+++ b/src/server/events/Copy.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -211,6 +211,12 @@ Copy::post_process()
}
}
+void
+Copy::undo(Interface& target)
+{
+ target.del(_new_uri);
+}
+
} // namespace Events
} // namespace Server
} // namespace Ingen
diff --git a/src/server/events/Copy.hpp b/src/server/events/Copy.hpp
index 2677ba53..a1726cc6 100644
--- a/src/server/events/Copy.hpp
+++ b/src/server/events/Copy.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -49,6 +49,7 @@ public:
bool pre_process();
void execute(ProcessContext& context);
void post_process();
+ void undo(Interface& target);
private:
bool engine_to_engine();
diff --git a/src/server/events/CreateBlock.cpp b/src/server/events/CreateBlock.cpp
index 7ba35d1a..cde15622 100644
--- a/src/server/events/CreateBlock.cpp
+++ b/src/server/events/CreateBlock.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -164,6 +164,12 @@ CreateBlock::post_process()
}
}
+void
+CreateBlock::undo(Interface& target)
+{
+ target.del(_block->uri());
+}
+
} // namespace Events
} // namespace Server
} // namespace Ingen
diff --git a/src/server/events/CreateBlock.hpp b/src/server/events/CreateBlock.hpp
index 40d72f52..1282fe8b 100644
--- a/src/server/events/CreateBlock.hpp
+++ b/src/server/events/CreateBlock.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -50,6 +50,7 @@ public:
bool pre_process();
void execute(ProcessContext& context);
void post_process();
+ void undo(Interface& target);
private:
Raul::Path _path;
diff --git a/src/server/events/CreateGraph.cpp b/src/server/events/CreateGraph.cpp
index e6ad0cb4..93191437 100644
--- a/src/server/events/CreateGraph.cpp
+++ b/src/server/events/CreateGraph.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -213,6 +213,12 @@ CreateGraph::post_process()
_child_events.clear();
}
+void
+CreateGraph::undo(Interface& target)
+{
+ target.del(_graph->uri());
+}
+
} // namespace Events
} // namespace Server
} // namespace Ingen
diff --git a/src/server/events/CreateGraph.hpp b/src/server/events/CreateGraph.hpp
index cf40fb41..efeabf48 100644
--- a/src/server/events/CreateGraph.hpp
+++ b/src/server/events/CreateGraph.hpp
@@ -47,6 +47,7 @@ public:
bool pre_process();
void execute(ProcessContext& context);
void post_process();
+ void undo(Interface& target);
GraphImpl* graph() { return _graph; }
diff --git a/src/server/events/CreatePort.cpp b/src/server/events/CreatePort.cpp
index 0f711f4f..0e512852 100644
--- a/src/server/events/CreatePort.cpp
+++ b/src/server/events/CreatePort.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -197,6 +197,12 @@ CreatePort::post_process()
delete _old_ports_array;
}
+void
+CreatePort::undo(Interface& target)
+{
+ target.del(_graph_port->uri());
+}
+
} // namespace Events
} // namespace Server
} // namespace Ingen
diff --git a/src/server/events/CreatePort.hpp b/src/server/events/CreatePort.hpp
index a2dd55ce..754a238f 100644
--- a/src/server/events/CreatePort.hpp
+++ b/src/server/events/CreatePort.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -54,6 +54,7 @@ public:
bool pre_process();
void execute(ProcessContext& context);
void post_process();
+ void undo(Interface& target);
private:
enum class Flow {
diff --git a/src/server/events/Delete.cpp b/src/server/events/Delete.cpp
index 06a5cb95..7b27e11f 100644
--- a/src/server/events/Delete.cpp
+++ b/src/server/events/Delete.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -162,6 +162,18 @@ Delete::post_process()
}
}
+void
+Delete::undo(Interface& target)
+{
+ auto i = _removed_objects.find(_path);
+ if (i != _removed_objects.end()) {
+ target.put(_uri, i->second->properties());
+ if (_disconnect_event) {
+ _disconnect_event->undo(target);
+ }
+ }
+}
+
} // namespace Events
} // namespace Server
} // namespace Ingen
diff --git a/src/server/events/Delete.hpp b/src/server/events/Delete.hpp
index 4403d4da..c6e38839 100644
--- a/src/server/events/Delete.hpp
+++ b/src/server/events/Delete.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -57,6 +57,7 @@ public:
bool pre_process();
void execute(ProcessContext& context);
void post_process();
+ void undo(Interface& target);
private:
Raul::URI _uri;
diff --git a/src/server/events/Delta.cpp b/src/server/events/Delta.cpp
index 83312814..a765932b 100644
--- a/src/server/events/Delta.cpp
+++ b/src/server/events/Delta.cpp
@@ -277,6 +277,7 @@ Delta::pre_process()
_old_bindings = _engine.control_bindings()->remove(port);
}
if (_object) {
+ _removed.emplace(key, value);
_object->remove_property(key, value);
} else if (is_engine && key == uris.ingen_loadedBundle) {
LilvWorld* lworld = _engine.world()->lilv_world();
@@ -299,8 +300,23 @@ Delta::pre_process()
// Remove all added properties if this is a put or set
if (_object && (_type == Type::PUT || _type == Type::SET)) {
- for (const auto& p : _properties) {
- _object->remove_property(p.first, uris.patch_wildcard);
+ for (auto p = _properties.begin();
+ p != _properties.end();
+ p = _properties.upper_bound(p->first)) {
+ for (auto q = _object->properties().find(p->first);
+ q != _object->properties().end() && q->first == p->first;) {
+ auto next = q;
+ ++next;
+
+ if (!_properties.contains(q->first, q->second)) {
+ const auto r = std::make_pair(q->first, q->second);
+ _object->properties().erase(q);
+ _object->on_property_removed(r.first, r.second);
+ _removed.insert(r);
+ }
+
+ q = next;
+ }
}
}
@@ -311,7 +327,9 @@ Delta::pre_process()
if (obj) {
Resource& resource = *obj;
if (value != uris.patch_wildcard) {
- resource.add_property(key, value, value.context());
+ if (resource.add_property(key, value, value.context())) {
+ _added.emplace(key, value);
+ }
}
BlockImpl* block = NULL;
@@ -589,12 +607,16 @@ Delta::post_process()
/* Kludge to avoid feedback for set events only. The GUI
depends on put responses to e.g. initially place blocks.
Some more sensible way of controlling this is needed. */
- _engine.broadcaster()->set_ignore_client(_request_client);
+ if (_mode == Mode::NORMAL) {
+ _engine.broadcaster()->set_ignore_client(_request_client);
+ }
_engine.broadcaster()->set_property(
_subject,
- (*_properties.begin()).first,
- (*_properties.begin()).second);
- _engine.broadcaster()->clear_ignore_client();
+ _properties.begin()->first,
+ _properties.begin()->second);
+ if (_mode == Mode::NORMAL) {
+ _engine.broadcaster()->clear_ignore_client();
+ }
break;
case Type::PUT:
if (_type == Type::PUT && _subject.substr(0, 5) == "file:") {
@@ -614,6 +636,27 @@ Delta::post_process()
}
}
+void
+Delta::undo(Interface& target)
+{
+ const Ingen::URIs& uris = _engine.world()->uris();
+
+ if (_create_event) {
+ _create_event->undo(target);
+ } else if (_type == Type::PATCH) {
+ target.delta(_subject, _added, _removed);
+ } else if (_type == Type::SET || _type == Type::PUT) {
+ if (_removed.size() == 1) {
+ target.set_property(
+ _subject, _removed.begin()->first, _removed.begin()->second);
+ } else if (_removed.empty()) {
+ target.delta(_subject, _added, {});
+ } else {
+ target.put(_subject, _removed);
+ }
+ }
+}
+
} // namespace Events
} // namespace Server
} // namespace Ingen
diff --git a/src/server/events/Delta.hpp b/src/server/events/Delta.hpp
index e9d1970b..b1f2d66a 100644
--- a/src/server/events/Delta.hpp
+++ b/src/server/events/Delta.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -76,6 +76,7 @@ public:
bool pre_process();
void execute(ProcessContext& context);
void post_process();
+ void undo(Interface& target);
private:
enum class SpecialType {
@@ -107,6 +108,9 @@ private:
ControlBindings::Key _binding;
Type _type;
+ Resource::Properties _added;
+ Resource::Properties _removed;
+
SPtr<ControlBindings::Bindings> _old_bindings;
boost::optional<Resource> _preset;
diff --git a/src/server/events/Disconnect.cpp b/src/server/events/Disconnect.cpp
index 6f84dc1a..13f419ce 100644
--- a/src/server/events/Disconnect.cpp
+++ b/src/server/events/Disconnect.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -219,6 +219,12 @@ Disconnect::post_process()
}
}
+void
+Disconnect::undo(Interface& target)
+{
+ target.connect(_tail_path, _head_path);
+}
+
} // namespace Events
} // namespace Server
} // namespace Ingen
diff --git a/src/server/events/Disconnect.hpp b/src/server/events/Disconnect.hpp
index 64e08246..8a69dac4 100644
--- a/src/server/events/Disconnect.hpp
+++ b/src/server/events/Disconnect.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -57,6 +57,7 @@ public:
bool pre_process();
void execute(ProcessContext& context);
void post_process();
+ void undo(Interface& target);
class Impl {
public:
@@ -67,7 +68,8 @@ public:
bool execute(ProcessContext& context, bool set_head_buffers);
- inline InputPort* head() { return _head; }
+ inline OutputPort* tail() { return _tail; }
+ inline InputPort* head() { return _head; }
private:
Engine& _engine;
diff --git a/src/server/events/DisconnectAll.cpp b/src/server/events/DisconnectAll.cpp
index bd4fef7d..ee19797e 100644
--- a/src/server/events/DisconnectAll.cpp
+++ b/src/server/events/DisconnectAll.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -164,6 +164,14 @@ DisconnectAll::post_process()
}
}
+void
+DisconnectAll::undo(Interface& target)
+{
+ for (auto& i : _impls) {
+ target.connect(i->tail()->path(), i->head()->path());
+ }
+}
+
} // namespace Events
} // namespace Server
} // namespace Ingen
diff --git a/src/server/events/DisconnectAll.hpp b/src/server/events/DisconnectAll.hpp
index 039e3f54..f8123a45 100644
--- a/src/server/events/DisconnectAll.hpp
+++ b/src/server/events/DisconnectAll.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -59,6 +59,7 @@ public:
bool pre_process();
void execute(ProcessContext& context);
void post_process();
+ void undo(Interface& target);
private:
typedef std::list<Disconnect::Impl*> Impls;
diff --git a/src/server/events/Mark.cpp b/src/server/events/Mark.cpp
new file mode 100644
index 00000000..0e14f008
--- /dev/null
+++ b/src/server/events/Mark.cpp
@@ -0,0 +1,65 @@
+/*
+ This file is part of Ingen.
+ Copyright 2016 David Robillard <http://drobilla.net/>
+
+ Ingen is free software: you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or any later version.
+
+ Ingen is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Ingen. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "Engine.hpp"
+#include "UndoStack.hpp"
+#include "events/Mark.hpp"
+
+namespace Ingen {
+namespace Server {
+namespace Events {
+
+Mark::Mark(Engine& engine,
+ SPtr<Interface> client,
+ int32_t id,
+ SampleCount timestamp,
+ Type type)
+ : Event(engine, client, id, timestamp)
+ , _type(type)
+{}
+
+bool
+Mark::pre_process()
+{
+ UndoStack* const stack = ((_mode == Mode::UNDO)
+ ? _engine.redo_stack()
+ : _engine.undo_stack());
+
+ switch (_type) {
+ case Type::BUNDLE_START:
+ stack->start_entry();
+ break;
+ case Type::BUNDLE_END:
+ stack->finish_entry();
+ break;
+ }
+
+ return Event::pre_process_done(Status::SUCCESS);
+}
+
+void
+Mark::execute(ProcessContext& context)
+{}
+
+void
+Mark::post_process()
+{
+ respond();
+}
+
+} // namespace Events
+} // namespace Server
+} // namespace Ingen
diff --git a/src/server/events/Mark.hpp b/src/server/events/Mark.hpp
new file mode 100644
index 00000000..995df746
--- /dev/null
+++ b/src/server/events/Mark.hpp
@@ -0,0 +1,55 @@
+/*
+ This file is part of Ingen.
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
+
+ Ingen is free software: you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or any later version.
+
+ Ingen is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Ingen. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef INGEN_EVENTS_MARK_HPP
+#define INGEN_EVENTS_MARK_HPP
+
+#include "Event.hpp"
+
+namespace Ingen {
+namespace Server {
+
+class Engine;
+
+namespace Events {
+
+/** Set properties of a graph object.
+ * \ingroup engine
+ */
+class Mark : public Event
+{
+public:
+ enum class Type { BUNDLE_START, BUNDLE_END };
+
+ Mark(Engine& engine,
+ SPtr<Interface> client,
+ int32_t id,
+ SampleCount timestamp,
+ Type type);
+
+ bool pre_process();
+ void execute(ProcessContext& context);
+ void post_process();
+
+private:
+ Type _type;
+};
+
+} // namespace Events
+} // namespace Server
+} // namespace Ingen
+
+#endif // INGEN_EVENTS_MARK_HPP
diff --git a/src/server/events/Move.cpp b/src/server/events/Move.cpp
index 2c689fe5..a51617cb 100644
--- a/src/server/events/Move.cpp
+++ b/src/server/events/Move.cpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -87,6 +87,12 @@ Move::post_process()
}
}
+void
+Move::undo(Interface& target)
+{
+ target.move(_new_path, _old_path);
+}
+
} // namespace Events
} // namespace Server
} // namespace Ingen
diff --git a/src/server/events/Move.hpp b/src/server/events/Move.hpp
index ae811138..74d32c61 100644
--- a/src/server/events/Move.hpp
+++ b/src/server/events/Move.hpp
@@ -1,6 +1,6 @@
/*
This file is part of Ingen.
- Copyright 2007-2015 David Robillard <http://drobilla.net/>
+ Copyright 2007-2016 David Robillard <http://drobilla.net/>
Ingen is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
@@ -48,6 +48,7 @@ public:
bool pre_process();
void execute(ProcessContext& context);
void post_process();
+ void undo(Interface& target);
private:
const Raul::Path _old_path;
diff --git a/src/server/events/Undo.cpp b/src/server/events/Undo.cpp
new file mode 100644
index 00000000..28b8e188
--- /dev/null
+++ b/src/server/events/Undo.cpp
@@ -0,0 +1,77 @@
+/*
+ This file is part of Ingen.
+ Copyright 2016 David Robillard <http://drobilla.net/>
+
+ Ingen is free software: you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or any later version.
+
+ Ingen is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Ingen. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "ingen/AtomReader.hpp"
+
+#include "Engine.hpp"
+#include "EventWriter.hpp"
+#include "Undo.hpp"
+
+namespace Ingen {
+namespace Server {
+namespace Events {
+
+Undo::Undo(Engine& engine,
+ SPtr<Interface> client,
+ int32_t id,
+ SampleCount timestamp,
+ bool is_redo)
+ : Event(engine, client, id, timestamp)
+ , _is_redo(is_redo)
+{}
+
+bool
+Undo::pre_process()
+{
+ UndoStack* stack = _is_redo ? _engine.redo_stack() : _engine.undo_stack();
+ Event::Mode mode = _is_redo ? Event::Mode::REDO : Event::Mode::UNDO;
+
+ if (stack->empty()) {
+ return Event::pre_process_done(Status::NOT_FOUND);
+ }
+
+ _entry = stack->pop();
+ _engine.interface()->set_event_mode(mode);
+ if (_entry.events.size() > 1) {
+ _engine.interface()->bundle_begin();
+ }
+
+ for (const LV2_Atom* ev : _entry.events) {
+ _engine.atom_interface()->write(ev);
+ }
+
+ if (_entry.events.size() > 1) {
+ _engine.interface()->bundle_end();
+ }
+ _engine.interface()->set_event_mode(mode);
+
+ return Event::pre_process_done(Status::SUCCESS);
+}
+
+void
+Undo::execute(ProcessContext& context)
+{
+}
+
+void
+Undo::post_process()
+{
+ respond();
+}
+
+} // namespace Events
+} // namespace Server
+} // namespace Ingen
diff --git a/src/server/events/Undo.hpp b/src/server/events/Undo.hpp
new file mode 100644
index 00000000..fff06b8d
--- /dev/null
+++ b/src/server/events/Undo.hpp
@@ -0,0 +1,54 @@
+/*
+ This file is part of Ingen.
+ Copyright 2016 David Robillard <http://drobilla.net/>
+
+ Ingen is free software: you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or any later version.
+
+ Ingen is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Ingen. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef INGEN_EVENTS_UNDO_HPP
+#define INGEN_EVENTS_UNDO_HPP
+
+#include "Event.hpp"
+#include "UndoStack.hpp"
+#include "types.hpp"
+
+namespace Ingen {
+namespace Server {
+namespace Events {
+
+/** A request to undo the last change to the engine.
+ *
+ * \ingroup engine
+ */
+class Undo : public Event
+{
+public:
+ Undo(Engine& engine,
+ SPtr<Interface> client,
+ int32_t id,
+ SampleCount timestamp,
+ bool is_redo);
+
+ bool pre_process();
+ void execute(ProcessContext& context);
+ void post_process();
+
+private:
+ UndoStack::Entry _entry;
+ bool _is_redo;
+};
+
+} // namespace Events
+} // namespace Server
+} // namespace Ingen
+
+#endif // INGEN_EVENTS_UNDO_HPP
diff --git a/src/server/wscript b/src/server/wscript
index 4d1beecf..4bb9b1f3 100644
--- a/src/server/wscript
+++ b/src/server/wscript
@@ -27,6 +27,7 @@ def build(bld):
PostProcessor.cpp
PreProcessor.cpp
SocketListener.cpp
+ UndoStack.cpp
Worker.cpp
events/Connect.cpp
events/Copy.cpp
@@ -38,8 +39,10 @@ def build(bld):
events/Disconnect.cpp
events/DisconnectAll.cpp
events/Get.cpp
+ events/Mark.cpp
events/Move.cpp
events/SetPortValue.cpp
+ events/Undo.cpp
ingen_engine.cpp
internals/Controller.cpp
internals/Delay.cpp
diff --git a/tests/ingen_test.cpp b/tests/ingen_test.cpp
index ee2c4924..12e9e9f9 100644
--- a/tests/ingen_test.cpp
+++ b/tests/ingen_test.cpp
@@ -42,6 +42,8 @@
#include "ingen/EngineBase.hpp"
#include "ingen/Interface.hpp"
#include "ingen/Parser.hpp"
+#include "ingen/Serialiser.hpp"
+#include "ingen/Store.hpp"
#include "ingen/URIMap.hpp"
#include "ingen/World.hpp"
#include "ingen/client/ThreadedSigClientInterface.hpp"
@@ -98,6 +100,10 @@ public:
void get(const Raul::URI& uri) {}
+ void undo() {}
+
+ void redo() {}
+
void response(int32_t id, Status status, const std::string& subject) {
if (status != Status::SUCCESS) {
_log.error(fmt("error on message %1%: %2% (%3%)\n")
@@ -220,8 +226,9 @@ main(int argc, char** argv)
SerdEnv* env = serd_env_new(&cmds_file_uri);
cmds->load_file(env, SERD_TURTLE, cmds_file_path);
Sord::Node nil;
- for (int i = 0; ; ++i) {
- std::string subject_str = (fmt("msg%1%") % i).str();
+ int n_events = 0;
+ for (;; ++n_events) {
+ std::string subject_str = (fmt("msg%1%") % n_events).str();
Sord::URI subject(*world->rdf_world(), subject_str,
(const char*)cmds_file_uri.buf);
Sord::Iter iter = cmds->find(subject, nil, nil);
@@ -247,17 +254,46 @@ main(int argc, char** argv)
return EXIT_FAILURE;
}
- while (world->engine()->pending_events()) {
- world->engine()->run(4096);
- world->engine()->main_iteration();
- g_usleep(1000);
- }
+ flush_events(world);
+ }
+
+ delete cmds;
+
+ // Save resulting graph
+ Store::iterator r = world->store()->find(Raul::Path("/"));
+ const std::string base = Glib::path_get_basename(cmds_file_path);
+ const std::string out_name = base.substr(0, base.find('.')) + ".out.ingen";
+ const std::string out_path = Glib::build_filename(Glib::get_current_dir(), out_name);
+ world->serialiser()->write_bundle(r->second, Glib::filename_to_uri(out_path));
+
+ // Undo every event (should result in a graph identical to the original)
+ for (int i = 0; i < n_events; ++i) {
+ world->interface()->undo();
+ flush_events(world);
}
+
+ // Save completely undone graph
+ r = world->store()->find(Raul::Path("/"));
+ const std::string undo_name = base.substr(0, base.find('.')) + ".undo.ingen";
+ const std::string undo_path = Glib::build_filename(Glib::get_current_dir(), undo_name);
+ world->serialiser()->write_bundle(r->second, Glib::filename_to_uri(undo_path));
+
+ // Redo every event (should result in a graph identical to the pre-undo output)
+ for (int i = 0; i < n_events; ++i) {
+ world->interface()->redo();
+ flush_events(world);
+ }
+
+ // Save completely redone graph
+ r = world->store()->find(Raul::Path("/"));
+ const std::string redo_name = base.substr(0, base.find('.')) + ".redo.ingen";
+ const std::string redo_path = Glib::build_filename(Glib::get_current_dir(), redo_name);
+ world->serialiser()->write_bundle(r->second, Glib::filename_to_uri(redo_path));
+
free((void*)out.buf);
serd_env_free(env);
sratom_free(sratom);
serd_node_free(&cmds_file_uri);
- delete cmds;
// Shut down
world->engine()->deactivate();
diff --git a/wscript b/wscript
index 71160936..ce3ab0b5 100644
--- a/wscript
+++ b/wscript
@@ -1,6 +1,7 @@
#!/usr/bin/env python
import os
import subprocess
+import waflib.Logs as Logs
import waflib.Options as Options
import waflib.Utils as Utils
import waflib.extras.autowaf as autowaf
@@ -265,6 +266,19 @@ def upload_docs(ctx):
def test(ctx):
+ import difflib
+ import sys
+
+ def test_file_equals(path1, path2):
+ diff = list(difflib.unified_diff(open(path1).readlines(),
+ open(path2).readlines(),
+ path1,
+ path2))
+ autowaf.run_test(ctx, APPNAME, [path2, len(diff) != 0])
+ if len(diff) > 0:
+ for line in diff:
+ sys.stdout.write(line)
+
os.environ['PATH'] = 'tests' + os.pathsep + os.getenv('PATH')
os.environ['LD_LIBRARY_PATH'] = os.path.join('src')
os.environ['INGEN_MODULE_PATH'] = os.pathsep.join([
@@ -273,12 +287,24 @@ def test(ctx):
autowaf.pre_test(ctx, APPNAME, dirs=['.', 'src', 'tests'])
autowaf.begin_tests(ctx, APPNAME)
+ empty = ctx.path.find_node('tests/empty.ingen')
+ empty_path = os.path.join(empty.abspath(), 'graph.ttl')
for i in ctx.path.ant_glob('tests/*.ttl'):
- empty = ctx.path.find_node('tests/empty.ingen')
+ # Run test
autowaf.run_test(ctx, APPNAME,
'ingen_test --load %s --execute %s' % (empty.abspath(), i.abspath()),
dirs=['.', 'src', 'tests'])
+ # Check undo output for changes
+ base = os.path.basename(i.abspath().replace('.ttl', ''))
+ undone_path = base + '.undo.ingen/graph.ttl'
+ test_file_equals(empty_path, os.path.abspath(undone_path))
+
+ # Check redo output for changes
+ out_path = base + '.out.ingen/graph.ttl'
+ redone_path = base + '.redo.ingen/graph.ttl'
+ test_file_equals(out_path, os.path.abspath(redone_path))
+
autowaf.end_tests(ctx, APPNAME)
autowaf.post_test(ctx, APPNAME, dirs=['.', 'src', 'tests'],
remove=['/usr*'])