From dd79e76e41446833088482588456afed37231bff Mon Sep 17 00:00:00 2001 From: David Robillard Date: Wed, 12 Aug 2015 04:46:29 +0000 Subject: Server-side presets. git-svn-id: http://svn.drobilla.net/lad/trunk/ingen@5703 a436a847-0d15-0410-975c-d299462d15a1 --- src/server/BlockImpl.cpp | 8 ++++- src/server/BlockImpl.hpp | 16 ++++++---- src/server/ControlBindings.cpp | 2 +- src/server/DuplexPort.cpp | 19 +++++------ src/server/EventWriter.cpp | 8 ++--- src/server/GraphPlugin.hpp | 2 +- src/server/InputPort.cpp | 2 +- src/server/InternalPlugin.cpp | 2 +- src/server/LV2Block.cpp | 71 ++++++++++++++++++++++++++++++++++++++++++ src/server/LV2Block.hpp | 3 ++ src/server/LV2Plugin.cpp | 44 +++++++++++++++++++++++++- src/server/LV2Plugin.hpp | 2 ++ src/server/NodeImpl.cpp | 2 +- src/server/NodeImpl.hpp | 2 +- src/server/PluginImpl.hpp | 30 +++++++++++++----- src/server/events/Delta.cpp | 54 +++++++++++++++++++++++++++++--- src/server/events/Delta.hpp | 5 +++ src/server/events/Get.cpp | 38 +++++++++++++++++----- src/server/events/Get.hpp | 7 ++++- 19 files changed, 268 insertions(+), 49 deletions(-) (limited to 'src/server') diff --git a/src/server/BlockImpl.cpp b/src/server/BlockImpl.cpp index ecc73090..103747fe 100644 --- a/src/server/BlockImpl.cpp +++ b/src/server/BlockImpl.cpp @@ -71,12 +71,18 @@ BlockImpl::port(uint32_t index) const return (*_ports)[index]; } -const Plugin* +const Resource* BlockImpl::plugin() const { return _plugin; } +const PluginImpl* +BlockImpl::plugin_impl() const +{ + return _plugin; +} + void BlockImpl::activate(BufferFactory& bufs) { diff --git a/src/server/BlockImpl.hpp b/src/server/BlockImpl.hpp index 44271cc3..2dcc4762 100644 --- a/src/server/BlockImpl.hpp +++ b/src/server/BlockImpl.hpp @@ -20,6 +20,7 @@ #include #include +#include #include "lilv/lilv.h" @@ -28,6 +29,7 @@ #include "BufferRef.hpp" #include "Context.hpp" #include "NodeImpl.hpp" +#include "PluginImpl.hpp" #include "PortType.hpp" #include "types.hpp" @@ -36,9 +38,6 @@ class Maid; } namespace Ingen { - -class Plugin; - namespace Server { class Buffer; @@ -106,6 +105,11 @@ public: /** Restore `state`. */ virtual void apply_state(LilvState* state) {} + /** Save current state as preset. */ + virtual boost::optional + save_preset(const Raul::URI& bundle, + const Properties& props) { return boost::optional(); } + /** Learn the next incoming MIDI event (for internals) */ virtual void learn() {} @@ -152,14 +156,14 @@ public: ProcessContext& context, Raul::Maid& maid, uint32_t poly); /** Information about the Plugin this Block is an instance of. - * Not the best name - not all blocks come from plugins (e.g. Graph) + * Not the best name - not all blocks come from plugins (ie Graph) */ - virtual PluginImpl* plugin_impl() const { return _plugin; } + virtual const Resource* plugin() const; /** Information about the Plugin this Block is an instance of. * Not the best name - not all blocks come from plugins (ie Graph) */ - virtual const Plugin* plugin() const; + virtual const PluginImpl* plugin_impl() const; virtual void plugin(PluginImpl* pi) { _plugin = pi; } diff --git a/src/server/ControlBindings.cpp b/src/server/ControlBindings.cpp index 40255fff..c4a95476 100644 --- a/src/server/ControlBindings.cpp +++ b/src/server/ControlBindings.cpp @@ -180,7 +180,7 @@ ControlBindings::port_value_changed(ProcessContext& context, break; } if (size > 0) { - _feedback->append_event(0, size, uris.midi_MidiEvent.id, buf); + _feedback->append_event(0, size, (LV2_URID)uris.midi_MidiEvent, buf); } } } diff --git a/src/server/DuplexPort.cpp b/src/server/DuplexPort.cpp index 0a928a3f..f681e250 100644 --- a/src/server/DuplexPort.cpp +++ b/src/server/DuplexPort.cpp @@ -91,28 +91,25 @@ DuplexPort::inherit_neighbour(const PortImpl* port, Resource::Properties& remove, Resource::Properties& add) { + const URIs& uris = _bufs.uris(); + /* TODO: This needs to become more sophisticated, and correct the situation if the port is disconnected. */ if (_type == PortType::CONTROL || _type == PortType::CV) { if (port->minimum().get() < _min.get()) { _min = port->minimum(); - remove.insert(std::make_pair(_bufs.uris().lv2_minimum, - Property(_bufs.uris().patch_wildcard))); - add.insert(std::make_pair(_bufs.uris().lv2_minimum, - port->minimum())); + remove.emplace(uris.lv2_minimum, uris.patch_wildcard); + add.emplace(uris.lv2_minimum, port->minimum()); } if (port->maximum().get() > _max.get()) { _max = port->maximum(); - remove.insert(std::make_pair(_bufs.uris().lv2_maximum, - Property(_bufs.uris().patch_wildcard))); - add.insert(std::make_pair(_bufs.uris().lv2_maximum, - port->maximum())); + remove.emplace(uris.lv2_maximum, uris.patch_wildcard); + add.emplace(uris.lv2_maximum, port->maximum()); } } else if (_type == PortType::ATOM) { for (Resource::Properties::const_iterator i = port->properties().find( - _bufs.uris().atom_supports); - i != port->properties().end() && - i->first == _bufs.uris().atom_supports; + uris.atom_supports); + i != port->properties().end() && i->first == uris.atom_supports; ++i) { set_property(i->first, i->second); add.insert(*i); diff --git a/src/server/EventWriter.cpp b/src/server/EventWriter.cpp index 1ec5e1a7..6c04c73d 100644 --- a/src/server/EventWriter.cpp +++ b/src/server/EventWriter.cpp @@ -127,11 +127,11 @@ EventWriter::set_property(const Raul::URI& uri, const Atom& value) { Resource::Properties remove; - remove.insert( - make_pair(predicate, - Resource::Property(_engine.world()->uris().patch_wildcard))); + remove.emplace(predicate, + _engine.world()->uris().patch_wildcard.urid); + Resource::Properties add; - add.insert(make_pair(predicate, value)); + add.emplace(predicate, value); _engine.enqueue_event( new Events::Delta(_engine, _respondee, _request_id, now(), Events::Delta::Type::SET, Resource::Graph::DEFAULT, diff --git a/src/server/GraphPlugin.hpp b/src/server/GraphPlugin.hpp index 8cd08c1b..e18e173a 100644 --- a/src/server/GraphPlugin.hpp +++ b/src/server/GraphPlugin.hpp @@ -36,7 +36,7 @@ public: const Raul::URI& uri, const Raul::Symbol& symbol, const std::string& name) - : PluginImpl(uris, Plugin::Graph, uri) + : PluginImpl(uris, uris.ingen_Graph, uri) {} BlockImpl* instantiate(BufferFactory& bufs, diff --git a/src/server/InputPort.cpp b/src/server/InputPort.cpp index 974fe3ab..f2aacea1 100644 --- a/src/server/InputPort.cpp +++ b/src/server/InputPort.cpp @@ -51,7 +51,7 @@ InputPort::InputPort(BufferFactory& bufs, const Ingen::URIs& uris = bufs.uris(); if (parent->graph_type() != Node::GraphType::GRAPH) { - add_property(uris.rdf_type, uris.lv2_InputPort); + add_property(uris.rdf_type, uris.lv2_InputPort.urid); } } diff --git a/src/server/InternalPlugin.cpp b/src/server/InternalPlugin.cpp index f1ee9024..647823f7 100644 --- a/src/server/InternalPlugin.cpp +++ b/src/server/InternalPlugin.cpp @@ -35,7 +35,7 @@ using namespace Internals; InternalPlugin::InternalPlugin(URIs& uris, const Raul::URI& uri, const Raul::Symbol& symbol) - : PluginImpl(uris, Plugin::Internal, uri) + : PluginImpl(uris, uris.ingen_Internal, uri) , _symbol(symbol) { set_property(uris.rdf_type, uris.ingen_Internal); diff --git a/src/server/LV2Block.cpp b/src/server/LV2Block.cpp index 30d1f090..6b644e5c 100644 --- a/src/server/LV2Block.cpp +++ b/src/server/LV2Block.cpp @@ -19,6 +19,9 @@ #include #include +#include +#include + #include "lv2/lv2plug.in/ns/ext/morph/morph.h" #include "lv2/lv2plug.in/ns/ext/presets/presets.h" #include "lv2/lv2plug.in/ns/ext/options/options.h" @@ -577,6 +580,74 @@ LV2Block::apply_state(LilvState* state) } } +static const void* +get_port_value(const char* port_symbol, + void* user_data, + uint32_t* size, + uint32_t* type) +{ + LV2Block* const block = (LV2Block*)user_data; + PortImpl* const port = block->port_by_symbol(port_symbol); + + if (port && port->is_input() && port->value().is_valid()) { + *size = port->value().size(); + *type = port->value().type(); + return port->value().get_body(); + } + + return NULL; +} + +boost::optional +LV2Block::save_preset(const Raul::URI& uri, + const Properties& props) +{ + World* world = parent_graph()->engine().world(); + LilvWorld* lworld = _lv2_plugin->lv2_info()->lv2_world(); + LV2_URID_Map* lmap = &world->uri_map().urid_map_feature()->urid_map; + LV2_URID_Unmap* lunmap = &world->uri_map().urid_unmap_feature()->urid_unmap; + + const std::string path = Glib::filename_from_uri(uri); + const std::string dirname = Glib::path_get_dirname(path); + const std::string basename = Glib::path_get_basename(path); + + LilvState* state = lilv_state_new_from_instance( + _lv2_plugin->lilv_plugin(), instance(0), lmap, + NULL, NULL, NULL, path.c_str(), + get_port_value, this, LV2_STATE_IS_NATIVE, NULL); + + if (state) { + const Properties::const_iterator l = props.find(_uris.rdfs_label); + if (l != props.end() && l->second.type() == _uris.atom_String) { + lilv_state_set_label(state, l->second.ptr()); + } + + lilv_state_save(lworld, lmap, lunmap, state, NULL, + dirname.c_str(), basename.c_str()); + + const Raul::URI uri(lilv_node_as_uri(lilv_state_get_uri(state))); + const std::string label(lilv_state_get_label(state) + ? lilv_state_get_label(state) + : basename); + lilv_state_free(state); + + Resource preset(_uris, uri); + preset.set_property(_uris.rdf_type, _uris.pset_Preset); + preset.set_property(_uris.rdfs_label, world->forge().alloc(label)); + preset.set_property(_uris.lv2_appliesTo, + world->forge().make_urid(_lv2_plugin->uri())); + + LilvNode* lbundle = lilv_new_uri( + lworld, Glib::filename_to_uri(dirname + "/").c_str()); + lilv_world_load_bundle(lworld, lbundle); + lilv_node_free(lbundle); + + return preset; + } + + return boost::optional(); +} + void LV2Block::set_port_buffer(uint32_t voice, uint32_t port_num, diff --git a/src/server/LV2Block.hpp b/src/server/LV2Block.hpp index f07f845f..ffb86d0a 100644 --- a/src/server/LV2Block.hpp +++ b/src/server/LV2Block.hpp @@ -67,6 +67,9 @@ public: void apply_state(LilvState* state); + boost::optional save_preset(const Raul::URI& bundle, + const Properties& props); + void set_port_buffer(uint32_t voice, uint32_t port_num, BufferRef buf, diff --git a/src/server/LV2Plugin.cpp b/src/server/LV2Plugin.cpp index 58491f4c..d88689ca 100644 --- a/src/server/LV2Plugin.cpp +++ b/src/server/LV2Plugin.cpp @@ -16,7 +16,9 @@ #include +#include "ingen/Log.hpp" #include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/presets/presets.h" #include "Driver.hpp" #include "Engine.hpp" @@ -29,7 +31,9 @@ namespace Ingen { namespace Server { LV2Plugin::LV2Plugin(SPtr lv2_info, const Raul::URI& uri) - : PluginImpl(lv2_info->world().uris(), Plugin::LV2, uri) + : PluginImpl(lv2_info->world().uris(), + lv2_info->world().uris().lv2_Plugin, + uri) , _lilv_plugin(NULL) , _lv2_info(lv2_info) { @@ -80,5 +84,43 @@ LV2Plugin::lilv_plugin(const LilvPlugin* p) _lilv_plugin = p; } +void +LV2Plugin::load_presets() +{ + LilvWorld* lworld = _lv2_info->world().lilv_world(); + LilvNode* pset_Preset = lilv_new_uri(lworld, LV2_PRESETS__Preset); + LilvNode* rdfs_label = lilv_new_uri(lworld, LILV_NS_RDFS "label"); + LilvNodes* presets = lilv_plugin_get_related(_lilv_plugin, pset_Preset); + + if (presets) { + LILV_FOREACH(nodes, i, presets) { + const LilvNode* preset = lilv_nodes_get(presets, i); + lilv_world_load_resource(lworld, preset); + + LilvNodes* labels = lilv_world_find_nodes( + lworld, preset, rdfs_label, NULL); + if (labels) { + const LilvNode* label = lilv_nodes_get_first(labels); + + _presets.emplace(Raul::URI(lilv_node_as_uri(preset)), + lilv_node_as_string(label)); + + lilv_nodes_free(labels); + } else { + _lv2_info->world().log().error( + fmt("Preset <%1%> has no rdfs:label\n") + % lilv_node_as_string(lilv_nodes_get(presets, i))); + } + } + + lilv_nodes_free(presets); + } + + lilv_node_free(rdfs_label); + lilv_node_free(pset_Preset); + + PluginImpl::load_presets(); +} + } // namespace Server } // namespace Ingen diff --git a/src/server/LV2Plugin.hpp b/src/server/LV2Plugin.hpp index 0dde120c..63946071 100644 --- a/src/server/LV2Plugin.hpp +++ b/src/server/LV2Plugin.hpp @@ -52,6 +52,8 @@ public: const LilvPlugin* lilv_plugin() const { return _lilv_plugin; } void lilv_plugin(const LilvPlugin* p); + void load_presets(); + private: const LilvPlugin* _lilv_plugin; SPtr _lv2_info; diff --git a/src/server/NodeImpl.cpp b/src/server/NodeImpl.cpp index 27d1ba4e..c95f3c5e 100644 --- a/src/server/NodeImpl.cpp +++ b/src/server/NodeImpl.cpp @@ -23,7 +23,7 @@ using namespace std; namespace Ingen { namespace Server { -NodeImpl::NodeImpl(Ingen::URIs& uris, +NodeImpl::NodeImpl(const Ingen::URIs& uris, NodeImpl* parent, const Raul::Symbol& symbol) : Node(uris, parent ? parent->path().child(symbol) : Raul::Path("/")) diff --git a/src/server/NodeImpl.hpp b/src/server/NodeImpl.hpp index 17238a74..457834f2 100644 --- a/src/server/NodeImpl.hpp +++ b/src/server/NodeImpl.hpp @@ -92,7 +92,7 @@ public: ProcessContext& context, Raul::Maid& maid, uint32_t poly) = 0; protected: - NodeImpl(Ingen::URIs& uris, + NodeImpl(const Ingen::URIs& uris, NodeImpl* parent, const Raul::Symbol& symbol); diff --git a/src/server/PluginImpl.hpp b/src/server/PluginImpl.hpp index 92338b70..414bd1f1 100644 --- a/src/server/PluginImpl.hpp +++ b/src/server/PluginImpl.hpp @@ -21,7 +21,6 @@ #include -#include "ingen/Plugin.hpp" #include "ingen/Resource.hpp" #include "raul/Symbol.hpp" #include "raul/URI.hpp" @@ -41,17 +40,19 @@ class GraphImpl; * * Conceptually, a Block is an instance of this. */ -class PluginImpl : public Plugin +class PluginImpl : public Resource , public boost::noncopyable { public: PluginImpl(Ingen::URIs& uris, - Type type, + const Atom& type, const Raul::URI& uri) - : Plugin(uris, uri) + : Resource(uris, uri) , _type(type) {} + virtual ~PluginImpl() {} + virtual BlockImpl* instantiate(BufferFactory& bufs, const Raul::Symbol& symbol, bool polyphonic, @@ -60,11 +61,26 @@ public: virtual const Raul::Symbol symbol() const = 0; - Plugin::Type type() const { return _type; } - void type(Plugin::Type t) { _type = t; } + const Atom& type() const { return _type; } + void set_type(const Atom& t) { _type = t; } + + typedef std::pair Preset; + typedef std::map Presets; + + const Presets& presets(bool force_reload=false) { + if (!_presets_loaded || force_reload) { + load_presets(); + } + + return _presets; + } + + virtual void load_presets() { _presets_loaded = true; } protected: - Plugin::Type _type; + Atom _type; + Presets _presets; + bool _presets_loaded; }; } // namespace Server diff --git a/src/server/events/Delta.cpp b/src/server/events/Delta.cpp index 7d2f896d..dc6759b0 100644 --- a/src/server/events/Delta.cpp +++ b/src/server/events/Delta.cpp @@ -34,6 +34,7 @@ #include "PortImpl.hpp" #include "PortType.hpp" #include "SetPortValue.hpp" +#include "events/Get.hpp" // #define DUMP 1 // #include "ingen/URIMap.hpp" @@ -134,10 +135,45 @@ s_add_set_event(const char* port_symbol, bool Delta::pre_process() { + const Ingen::URIs& uris = _engine.world()->uris(); + const bool is_graph_object = Node::uri_is_path(_subject); const bool is_client = (_subject == "ingen:/clients/this"); + const bool is_file = (_subject.substr(0, 5) == "file:"); bool poly_changed = false; + if (_type == Type::PUT && is_file) { + // Ensure type is Preset, the only supported file put + const auto t = _properties.find(uris.rdf_type); + if (t == _properties.end() || t->second != uris.pset_Preset) { + return Event::pre_process_done(Status::BAD_REQUEST, _subject); + } + + // Get "prototype" for preset (node to save state for) + const auto p = _properties.find(uris.lv2_prototype); + if (p == _properties.end()) { + return Event::pre_process_done(Status::BAD_REQUEST, _subject); + } + + const Raul::URI prot(_engine.world()->forge().str(p->second, false)); + + Node* node = _engine.store()->get(Node::uri_to_path(Raul::URI(prot))); + if (!node) { + return Event::pre_process_done(Status::NOT_FOUND, prot); + } + + BlockImpl* block = dynamic_cast(node); + if (!block) { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, prot); + } + + if ((_preset = block->save_preset(_subject, _properties))) { + return Event::pre_process_done(Status::SUCCESS); + } else { + return Event::pre_process_done(Status::FAILURE); + } + } + // Take a writer lock while we modify the store std::unique_lock lock(_engine.store()->mutex()); @@ -149,8 +185,6 @@ Delta::pre_process() return Event::pre_process_done(Status::NOT_FOUND, _subject); } - const Ingen::URIs& uris = _engine.world()->uris(); - if (is_graph_object && !_object) { Raul::Path path(Node::uri_to_path(_subject)); bool is_graph = false, is_block = false, is_port = false, is_output = false; @@ -258,6 +292,8 @@ Delta::pre_process() if ((_state = block->load_preset(Raul::URI(str)))) { lilv_state_emit_port_values( _state, s_add_set_event, this); + } else { + _engine.log().warn(fmt("Failed to load preset <%1%>\n") % str); } } else { _status = Status::BAD_VALUE; @@ -341,7 +377,7 @@ Delta::pre_process() void Delta::execute(ProcessContext& context) { - if (_status != Status::SUCCESS) { + if (_status != Status::SUCCESS || _preset) { return; } @@ -406,7 +442,7 @@ Delta::execute(ProcessContext& context) if (port) { _engine.control_bindings()->port_binding_changed(context, port, value); } else if (block) { - if (block->plugin_impl()->type() == Plugin::Internal) { + if (uris.ingen_Internal == block->plugin_impl()->type()) { block->learn(); } } @@ -472,7 +508,15 @@ Delta::post_process() _engine.broadcaster()->clear_ignore_client(); break; case Type::PUT: - _engine.broadcaster()->put(_subject, _properties, _context); + if (_type == Type::PUT && _subject.substr(0, 5) == "file:") { + // Preset save + Get::Response response; + response.put(_preset->uri(), _preset->properties()); + response.send(_engine.broadcaster()); + } else { + // Graph object put + _engine.broadcaster()->put(_subject, _properties, _context); + } break; case Type::PATCH: _engine.broadcaster()->delta(_subject, _remove, _properties); diff --git a/src/server/events/Delta.hpp b/src/server/events/Delta.hpp index 0b5934ac..7c303fc2 100644 --- a/src/server/events/Delta.hpp +++ b/src/server/events/Delta.hpp @@ -19,10 +19,13 @@ #include +#include + #include "lilv/lilv.h" #include "raul/URI.hpp" +#include "PluginImpl.hpp" #include "ControlBindings.hpp" #include "Event.hpp" @@ -128,6 +131,8 @@ private: SPtr _old_bindings; + boost::optional _preset; + std::unique_lock _poly_lock; ///< Long-term lock for poly changes }; diff --git a/src/server/events/Get.cpp b/src/server/events/Get.cpp index bba95485..185f275a 100644 --- a/src/server/events/Get.cpp +++ b/src/server/events/Get.cpp @@ -60,8 +60,10 @@ Get::Response::put_port(const PortImpl* port) void Get::Response::put_block(const BlockImpl* block) { - PluginImpl* const plugin = block->plugin_impl(); - if (plugin->type() == Plugin::Graph) { + const PluginImpl* const plugin = block->plugin_impl(); + const URIs& uris = plugin->uris(); + + if (uris.ingen_Graph == plugin->type()) { put_graph((const GraphImpl*)block); } else { put(block->uri(), block->properties()); @@ -82,7 +84,7 @@ Get::Response::put_graph(const GraphImpl* graph) graph->properties(Resource::Graph::EXTERNAL), Resource::Graph::EXTERNAL); - // Enqueue locks + // Enqueue blocks for (const auto& b : graph->blocks()) { put_block(&b); } @@ -100,6 +102,29 @@ Get::Response::put_graph(const GraphImpl* graph) } } +void +Get::Response::put_plugin(PluginImpl* plugin) +{ + put(plugin->uri(), plugin->properties()); + + for (const auto& p : plugin->presets()) { + put_preset(plugin->uris(), plugin->uri(), p.first, p.second); + } +} + +void +Get::Response::put_preset(const URIs& uris, + const Raul::URI& plugin, + const Raul::URI& preset, + const std::string& label) +{ + Resource::Properties props; + props.emplace(uris.rdf_type, uris.pset_Preset.urid); + props.emplace(uris.rdfs_label, uris.forge.alloc(label)); + props.emplace(uris.lv2_appliesTo, uris.forge.make_urid(plugin)); + put(preset, props); +} + /** Returns true if a is closer to the root than b. */ static inline bool put_higher_than(const Get::Response::Put& a, const Get::Response::Put& b) @@ -159,11 +184,10 @@ Get::pre_process() return Event::pre_process_done(Status::SUCCESS); } return Event::pre_process_done(Status::NOT_FOUND, _uri); + } else if ((_plugin = _engine.block_factory()->plugin(_uri))) { + _response.put_plugin(_plugin); + return Event::pre_process_done(Status::SUCCESS); } else { - if ((_plugin = _engine.block_factory()->plugin(_uri))) { - _response.put(_uri, _plugin->properties()); - return Event::pre_process_done(Status::SUCCESS); - } return Event::pre_process_done(Status::NOT_FOUND, _uri); } } diff --git a/src/server/events/Get.hpp b/src/server/events/Get.hpp index 5a4bde23..f24e42e0 100644 --- a/src/server/events/Get.hpp +++ b/src/server/events/Get.hpp @@ -67,6 +67,11 @@ public: void put_port(const PortImpl* port); void put_block(const BlockImpl* block); void put_graph(const GraphImpl* graph); + void put_plugin(PluginImpl* plugin); + void put_preset(const URIs& uris, + const Raul::URI& plugin, + const Raul::URI& preset, + const std::string& label); void send(Interface* dest); @@ -88,7 +93,7 @@ public: private: const Raul::URI _uri; const Node* _object; - const PluginImpl* _plugin; + PluginImpl* _plugin; BlockFactory::Plugins _plugins; Response _response; }; -- cgit v1.2.1