diff options
author | David Robillard <d@drobilla.net> | 2016-07-30 13:10:13 -0400 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2016-07-30 15:32:49 -0400 |
commit | c8ae7295e911c62cf9dedf90187656937cc18cbb (patch) | |
tree | 0ba1207bf2c63278e9932eed2d04f961144a02db | |
parent | e74c6a3b388ded66fcc4cfb1fa5bece881b63113 (diff) | |
download | ingen-c8ae7295e911c62cf9dedf90187656937cc18cbb.tar.gz ingen-c8ae7295e911c62cf9dedf90187656937cc18cbb.tar.bz2 ingen-c8ae7295e911c62cf9dedf90187656937cc18cbb.zip |
Add undo support
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(); @@ -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*']) |