summaryrefslogtreecommitdiffstats
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/BlockModel.cpp285
-rw-r--r--src/client/ClientStore.cpp487
-rw-r--r--src/client/GraphModel.cpp176
-rw-r--r--src/client/ObjectModel.cpp108
-rw-r--r--src/client/PluginModel.cpp360
-rw-r--r--src/client/PluginUI.cpp336
-rw-r--r--src/client/PortModel.cpp78
-rw-r--r--src/client/ingen_client.cpp34
-rw-r--r--src/client/wscript23
9 files changed, 1887 insertions, 0 deletions
diff --git a/src/client/BlockModel.cpp b/src/client/BlockModel.cpp
new file mode 100644
index 00000000..910f7037
--- /dev/null
+++ b/src/client/BlockModel.cpp
@@ -0,0 +1,285 @@
+/*
+ This file is part of Ingen.
+ Copyright 2007-2015 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 <cassert>
+#include <cmath>
+#include <string>
+
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/URIs.hpp"
+#include "ingen/World.hpp"
+
+namespace Ingen {
+namespace Client {
+
+BlockModel::BlockModel(URIs& uris,
+ SPtr<PluginModel> plugin,
+ const Raul::Path& path)
+ : ObjectModel(uris, path)
+ , _plugin_uri(plugin->uri())
+ , _plugin(plugin)
+ , _num_values(0)
+ , _min_values(nullptr)
+ , _max_values(nullptr)
+{
+}
+
+BlockModel::BlockModel(URIs& uris,
+ const URI& plugin_uri,
+ const Raul::Path& path)
+ : ObjectModel(uris, path)
+ , _plugin_uri(plugin_uri)
+ , _num_values(0)
+ , _min_values(nullptr)
+ , _max_values(nullptr)
+{
+}
+
+BlockModel::BlockModel(const BlockModel& copy)
+ : ObjectModel(copy)
+ , _plugin_uri(copy._plugin_uri)
+ , _num_values(copy._num_values)
+ , _min_values((float*)malloc(sizeof(float) * _num_values))
+ , _max_values((float*)malloc(sizeof(float) * _num_values))
+{
+ memcpy(_min_values, copy._min_values, sizeof(float) * _num_values);
+ memcpy(_max_values, copy._max_values, sizeof(float) * _num_values);
+}
+
+BlockModel::~BlockModel()
+{
+ clear();
+}
+
+void
+BlockModel::remove_port(SPtr<PortModel> port)
+{
+ for (auto i = _ports.begin(); i != _ports.end(); ++i) {
+ if ((*i) == port) {
+ _ports.erase(i);
+ break;
+ }
+ }
+ _signal_removed_port.emit(port);
+}
+
+void
+BlockModel::remove_port(const Raul::Path& port_path)
+{
+ for (auto i = _ports.begin(); i != _ports.end(); ++i) {
+ if ((*i)->path() == port_path) {
+ _ports.erase(i);
+ break;
+ }
+ }
+}
+
+void
+BlockModel::clear()
+{
+ _ports.clear();
+ assert(_ports.empty());
+ delete[] _min_values;
+ delete[] _max_values;
+ _min_values = nullptr;
+ _max_values = nullptr;
+}
+
+void
+BlockModel::add_child(SPtr<ObjectModel> c)
+{
+ assert(c->parent().get() == this);
+
+ //ObjectModel::add_child(c);
+
+ SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(c);
+ assert(pm);
+ add_port(pm);
+}
+
+bool
+BlockModel::remove_child(SPtr<ObjectModel> c)
+{
+ assert(c->path().is_child_of(path()));
+ assert(c->parent().get() == this);
+
+ //bool ret = ObjectModel::remove_child(c);
+
+ SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(c);
+ assert(pm);
+ remove_port(pm);
+
+ //return ret;
+ return true;
+}
+
+void
+BlockModel::add_port(SPtr<PortModel> pm)
+{
+ assert(pm);
+ assert(pm->path().is_child_of(path()));
+ assert(pm->parent().get() == this);
+
+ // Store should have handled this by merging the two
+ assert(find(_ports.begin(), _ports.end(), pm) == _ports.end());
+
+ _ports.push_back(pm);
+ _signal_new_port.emit(pm);
+}
+
+SPtr<const PortModel>
+BlockModel::get_port(const Raul::Symbol& symbol) const
+{
+ for (auto p : _ports) {
+ if (p->symbol() == symbol) {
+ return p;
+ }
+ }
+ return SPtr<PortModel>();
+}
+
+SPtr<const PortModel>
+BlockModel::get_port(uint32_t index) const
+{
+ return _ports[index];
+}
+
+Ingen::Node*
+BlockModel::port(uint32_t index) const
+{
+ assert(index < num_ports());
+ return const_cast<Ingen::Node*>(
+ dynamic_cast<const Ingen::Node*>(_ports[index].get()));
+}
+
+void
+BlockModel::default_port_value_range(SPtr<const PortModel> port,
+ float& min,
+ float& max,
+ uint32_t srate) const
+{
+ // Default control values
+ min = 0.0;
+ max = 1.0;
+
+ // Get range from client-side LV2 data
+ if (_plugin && _plugin->lilv_plugin()) {
+ if (!_min_values) {
+ _num_values = lilv_plugin_get_num_ports(_plugin->lilv_plugin());
+ _min_values = new float[_num_values];
+ _max_values = new float[_num_values];
+ lilv_plugin_get_port_ranges_float(_plugin->lilv_plugin(),
+ _min_values, _max_values, nullptr);
+ }
+
+ if (!std::isnan(_min_values[port->index()])) {
+ min = _min_values[port->index()];
+ }
+ if (!std::isnan(_max_values[port->index()])) {
+ max = _max_values[port->index()];
+ }
+ }
+
+ if (port->port_property(_uris.lv2_sampleRate)) {
+ min *= srate;
+ max *= srate;
+ }
+}
+
+void
+BlockModel::port_value_range(SPtr<const PortModel> port,
+ float& min,
+ float& max,
+ uint32_t srate) const
+{
+ assert(port->parent().get() == this);
+
+ default_port_value_range(port, min, max);
+
+ // Possibly overriden
+ const Atom& min_atom = port->get_property(_uris.lv2_minimum);
+ const Atom& max_atom = port->get_property(_uris.lv2_maximum);
+ if (min_atom.type() == _uris.forge.Float) {
+ min = min_atom.get<float>();
+ }
+ if (max_atom.type() == _uris.forge.Float) {
+ max = max_atom.get<float>();
+ }
+
+ if (max <= min) {
+ max = min + 1.0;
+ }
+
+ if (port->port_property(_uris.lv2_sampleRate)) {
+ min *= srate;
+ max *= srate;
+ }
+}
+
+std::string
+BlockModel::label() const
+{
+ const Atom& name_property = get_property(_uris.lv2_name);
+ if (name_property.type() == _uris.forge.String) {
+ return name_property.ptr<char>();
+ } else if (plugin_model()) {
+ return plugin_model()->human_name();
+ } else {
+ return symbol().c_str();
+ }
+}
+
+std::string
+BlockModel::port_label(SPtr<const PortModel> port) const
+{
+ const Atom& name = port->get_property(URI(LV2_CORE__name));
+ if (name.is_valid() && name.type() == _uris.forge.String) {
+ return name.ptr<char>();
+ }
+
+ if (_plugin && _plugin->lilv_plugin()) {
+ LilvWorld* w = _plugin->lilv_world();
+ const LilvPlugin* plug = _plugin->lilv_plugin();
+ LilvNode* sym = lilv_new_string(w, port->symbol().c_str());
+ const LilvPort* lport = lilv_plugin_get_port_by_symbol(plug, sym);
+ if (lport) {
+ LilvNode* lname = lilv_port_get_name(plug, lport);
+ if (lname && lilv_node_is_string(lname)) {
+ std::string ret(lilv_node_as_string(lname));
+ lilv_node_free(lname);
+ return ret;
+ }
+ lilv_node_free(lname);
+ }
+ }
+
+ return port->symbol().c_str();
+}
+
+void
+BlockModel::set(SPtr<ObjectModel> model)
+{
+ SPtr<BlockModel> block = dynamic_ptr_cast<BlockModel>(model);
+ if (block) {
+ _plugin_uri = block->_plugin_uri;
+ _plugin = block->_plugin;
+ }
+
+ ObjectModel::set(model);
+}
+
+} // namespace Client
+} // namespace Ingen
diff --git a/src/client/ClientStore.cpp b/src/client/ClientStore.cpp
new file mode 100644
index 00000000..792f8949
--- /dev/null
+++ b/src/client/ClientStore.cpp
@@ -0,0 +1,487 @@
+/*
+ This file is part of Ingen.
+ Copyright 2007-2017 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 <boost/variant/apply_visitor.hpp>
+
+#include "ingen/Log.hpp"
+#include "ingen/client/ArcModel.hpp"
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/client/ObjectModel.hpp"
+#include "ingen/client/PluginModel.hpp"
+#include "ingen/client/PortModel.hpp"
+#include "ingen/client/SigClientInterface.hpp"
+
+namespace Ingen {
+namespace Client {
+
+ClientStore::ClientStore(URIs& uris,
+ Log& log,
+ SPtr<SigClientInterface> emitter)
+ : _uris(uris)
+ , _log(log)
+ , _emitter(emitter)
+ , _plugins(new Plugins())
+{
+ if (emitter) {
+ emitter->signal_message().connect(
+ sigc::mem_fun(this, &ClientStore::message));
+ }
+}
+
+void
+ClientStore::clear()
+{
+ Store::clear();
+ _plugins->clear();
+}
+
+void
+ClientStore::add_object(SPtr<ObjectModel> object)
+{
+ // If we already have "this" object, merge the existing one into the new
+ // one (with precedence to the new values).
+ auto existing = find(object->path());
+ if (existing != end()) {
+ dynamic_ptr_cast<ObjectModel>(existing->second)->set(object);
+ } else {
+ if (!object->path().is_root()) {
+ SPtr<ObjectModel> parent = _object(object->path().parent());
+ if (parent) {
+ assert(object->path().is_child_of(parent->path()));
+ object->set_parent(parent);
+ parent->add_child(object);
+ assert(parent && (object->parent() == parent));
+
+ (*this)[object->path()] = object;
+ _signal_new_object.emit(object);
+ } else {
+ _log.error(fmt("Object %1% with no parent\n") % object->path());
+ }
+ } else {
+ (*this)[object->path()] = object;
+ _signal_new_object.emit(object);
+ }
+ }
+
+ for (auto p : object->properties()) {
+ object->signal_property().emit(p.first, p.second);
+ }
+}
+
+SPtr<ObjectModel>
+ClientStore::remove_object(const Raul::Path& path)
+{
+ // Find the object, the "top" of the tree to remove
+ const iterator top = find(path);
+ if (top == end()) {
+ return SPtr<ObjectModel>();
+ }
+
+ SPtr<ObjectModel> object = dynamic_ptr_cast<ObjectModel>(top->second);
+
+ // Remove object and any adjacent arcs from parent if applicable
+ if (object && object->parent()) {
+ SPtr<PortModel> port = dynamic_ptr_cast<PortModel>(object);
+ if (port && dynamic_ptr_cast<GraphModel>(port->parent())) {
+ disconnect_all(port->parent()->path(), path);
+ if (port->parent()->parent()) {
+ disconnect_all(port->parent()->parent()->path(), path);
+ }
+ } else if (port && port->parent()->parent()) {
+ disconnect_all(port->parent()->parent()->path(), path);
+ } else {
+ disconnect_all(object->parent()->path(), path);
+ }
+
+ object->parent()->remove_child(object);
+ }
+
+ // Remove the object and all its descendants
+ Objects removed;
+ remove(top, removed);
+
+ // Notify everything that needs to know this object has been removed
+ if (object) {
+ object->signal_destroyed().emit();
+ }
+
+ return object;
+}
+
+SPtr<PluginModel>
+ClientStore::_plugin(const URI& uri)
+{
+ const Plugins::iterator i = _plugins->find(uri);
+ return (i == _plugins->end()) ? SPtr<PluginModel>() : (*i).second;
+}
+
+SPtr<PluginModel>
+ClientStore::_plugin(const Atom& uri)
+{
+ /* FIXME: Should probably be stored with URIs rather than strings, to make
+ this a fast case. */
+
+ const Plugins::iterator i = _plugins->find(URI(_uris.forge.str(uri, false)));
+ return (i == _plugins->end()) ? SPtr<PluginModel>() : (*i).second;
+}
+
+SPtr<const PluginModel>
+ClientStore::plugin(const URI& uri) const
+{
+ return const_cast<ClientStore*>(this)->_plugin(uri);
+}
+
+SPtr<ObjectModel>
+ClientStore::_object(const Raul::Path& path)
+{
+ const iterator i = find(path);
+ if (i == end()) {
+ return SPtr<ObjectModel>();
+ } else {
+ SPtr<ObjectModel> model = dynamic_ptr_cast<ObjectModel>(i->second);
+ assert(model);
+ assert(model->path().is_root() || model->parent());
+ return model;
+ }
+}
+
+SPtr<const ObjectModel>
+ClientStore::object(const Raul::Path& path) const
+{
+ return const_cast<ClientStore*>(this)->_object(path);
+}
+
+SPtr<Resource>
+ClientStore::_resource(const URI& uri)
+{
+ if (uri_is_path(uri)) {
+ return _object(uri_to_path(uri));
+ } else {
+ return _plugin(uri);
+ }
+}
+
+SPtr<const Resource>
+ClientStore::resource(const URI& uri) const
+{
+ return const_cast<ClientStore*>(this)->_resource(uri);
+}
+
+void
+ClientStore::add_plugin(SPtr<PluginModel> pm)
+{
+ SPtr<PluginModel> existing = _plugin(pm->uri());
+ if (existing) {
+ existing->set(pm);
+ } else {
+ _plugins->emplace(pm->uri(), pm);
+ _signal_new_plugin.emit(pm);
+ }
+}
+
+/* ****** Signal Handlers ******** */
+
+void
+ClientStore::operator()(const Del& del)
+{
+ if (uri_is_path(del.uri)) {
+ remove_object(uri_to_path(del.uri));
+ } else {
+ auto p = _plugins->find(del.uri);
+ if (p != _plugins->end()) {
+ _plugins->erase(p);
+ _signal_plugin_deleted.emit(del.uri);
+ }
+ }
+}
+
+void
+ClientStore::operator()(const Copy&)
+{
+ _log.error("Client store copy unsupported\n");
+}
+
+void
+ClientStore::operator()(const Move& msg)
+{
+ const iterator top = find(msg.old_path);
+ if (top != end()) {
+ rename(top, msg.new_path);
+ }
+}
+
+void
+ClientStore::message(const Message& msg)
+{
+ boost::apply_visitor(*this, msg);
+}
+
+void
+ClientStore::operator()(const Put& msg)
+{
+ typedef Properties::const_iterator Iterator;
+
+ const auto& uri = msg.uri;
+ const auto& properties = msg.properties;
+
+ bool is_graph, is_block, is_port, is_output;
+ Resource::type(uris(), properties,
+ is_graph, is_block, is_port, is_output);
+
+ // Check for specially handled types
+ const Iterator t = properties.find(_uris.rdf_type);
+ if (t != properties.end()) {
+ const Atom& type(t->second);
+ if (_uris.pset_Preset == type) {
+ const Iterator p = properties.find(_uris.lv2_appliesTo);
+ const Iterator l = properties.find(_uris.rdfs_label);
+ SPtr<PluginModel> plug;
+ if (p == properties.end()) {
+ _log.error(fmt("Preset <%1%> with no plugin\n") % uri.c_str());
+ } else if (l == properties.end()) {
+ _log.error(fmt("Preset <%1%> with no label\n") % uri.c_str());
+ } else if (l->second.type() != _uris.forge.String) {
+ _log.error(fmt("Preset <%1%> label is not a string\n") % uri.c_str());
+ } else if (!(plug = _plugin(p->second))) {
+ _log.error(fmt("Preset <%1%> for unknown plugin %2%\n")
+ % uri.c_str() % _uris.forge.str(p->second, true));
+ } else {
+ plug->add_preset(uri, l->second.ptr<char>());
+ }
+ return;
+ } else if (_uris.ingen_Graph == type) {
+ is_graph = true;
+ } else if (_uris.ingen_Internal == type || _uris.lv2_Plugin == type) {
+ SPtr<PluginModel> p(new PluginModel(uris(), uri, type, properties));
+ add_plugin(p);
+ return;
+ }
+ }
+
+ if (!uri_is_path(uri)) {
+ _log.error(fmt("Put for unknown subject <%1%>\n")
+ % uri.c_str());
+ return;
+ }
+
+ const Raul::Path path(uri_to_path(uri));
+
+ SPtr<ObjectModel> obj = dynamic_ptr_cast<ObjectModel>(_object(path));
+ if (obj) {
+ obj->set_properties(properties);
+ return;
+ }
+
+ if (path.is_root()) {
+ is_graph = true;
+ }
+
+ if (is_graph) {
+ SPtr<GraphModel> model(new GraphModel(uris(), path));
+ model->set_properties(properties);
+ add_object(model);
+ } else if (is_block) {
+ auto p = properties.find(_uris.lv2_prototype);
+ if (p == properties.end()) {
+ p = properties.find(_uris.ingen_prototype);
+ }
+
+ SPtr<PluginModel> plug;
+ if (p->second.is_valid() && (p->second.type() == _uris.forge.URI ||
+ p->second.type() == _uris.forge.URID)) {
+ const URI uri(_uris.forge.str(p->second, false));
+ if (!(plug = _plugin(uri))) {
+ plug = SPtr<PluginModel>(
+ new PluginModel(uris(), uri, Atom(), Properties()));
+ add_plugin(plug);
+ }
+
+ SPtr<BlockModel> bm(new BlockModel(uris(), plug, path));
+ bm->set_properties(properties);
+ add_object(bm);
+ } else {
+ _log.warn(fmt("Block %1% has no prototype\n") % path.c_str());
+ }
+ } else if (is_port) {
+ PortModel::Direction pdir = (is_output)
+ ? PortModel::Direction::OUTPUT
+ : PortModel::Direction::INPUT;
+ uint32_t index = 0;
+ const Iterator i = properties.find(_uris.lv2_index);
+ if (i != properties.end() && i->second.type() == _uris.forge.Int) {
+ index = i->second.get<int32_t>();
+ }
+
+ SPtr<PortModel> p(new PortModel(uris(), path, index, pdir));
+ p->set_properties(properties);
+ add_object(p);
+ } else {
+ _log.warn(fmt("Ignoring %1% of unknown type\n") % path.c_str());
+ }
+}
+
+void
+ClientStore::operator()(const Delta& msg)
+{
+ const auto& uri = msg.uri;
+ if (uri == URI("ingen:/clients/this")) {
+ // Client property, which we don't store (yet?)
+ return;
+ }
+
+ if (!uri_is_path(uri)) {
+ _log.error(fmt("Delta for unknown subject <%1%>\n")
+ % uri.c_str());
+ return;
+ }
+
+ const Raul::Path path(uri_to_path(uri));
+
+ SPtr<ObjectModel> obj = _object(path);
+ if (obj) {
+ obj->remove_properties(msg.remove);
+ obj->add_properties(msg.add);
+ } else {
+ _log.warn(fmt("Failed to find object `%1%'\n") % path.c_str());
+ }
+}
+
+void
+ClientStore::operator()(const SetProperty& msg)
+{
+ const auto& subject_uri = msg.subject;
+ const auto& predicate = msg.predicate;
+ const auto& value = msg.value;
+
+ if (subject_uri == URI("ingen:/engine")) {
+ _log.info(fmt("Engine property <%1%> = %2%\n")
+ % predicate.c_str() % _uris.forge.str(value, false));
+ return;
+ }
+ SPtr<Resource> subject = _resource(subject_uri);
+ if (subject) {
+ if (predicate == _uris.ingen_activity) {
+ /* Activity is transient, trigger any live actions (like GUI
+ blinkenlights) but do not store the property. */
+ subject->on_property(predicate, value);
+ } else {
+ subject->set_property(predicate, value, msg.ctx);
+ }
+ } else {
+ SPtr<PluginModel> plugin = _plugin(subject_uri);
+ if (plugin) {
+ plugin->set_property(predicate, value);
+ } else if (predicate != _uris.ingen_activity) {
+ _log.warn(fmt("Property <%1%> for unknown object %2%\n")
+ % predicate.c_str() % subject_uri.c_str());
+ }
+ }
+}
+
+SPtr<GraphModel>
+ClientStore::connection_graph(const Raul::Path& tail_path,
+ const Raul::Path& head_path)
+{
+ SPtr<GraphModel> graph;
+
+ if (tail_path.parent() == head_path.parent()) {
+ graph = dynamic_ptr_cast<GraphModel>(_object(tail_path.parent()));
+ }
+
+ if (!graph && tail_path.parent() == head_path.parent().parent()) {
+ graph = dynamic_ptr_cast<GraphModel>(_object(tail_path.parent()));
+ }
+
+ if (!graph && tail_path.parent().parent() == head_path.parent()) {
+ graph = dynamic_ptr_cast<GraphModel>(_object(head_path.parent()));
+ }
+
+ if (!graph) {
+ graph = dynamic_ptr_cast<GraphModel>(_object(tail_path.parent().parent()));
+ }
+
+ if (!graph) {
+ _log.error(fmt("Unable to find graph for arc %1% => %2%\n")
+ % tail_path % head_path);
+ }
+
+ return graph;
+}
+
+bool
+ClientStore::attempt_connection(const Raul::Path& tail_path,
+ const Raul::Path& head_path)
+{
+ SPtr<PortModel> tail = dynamic_ptr_cast<PortModel>(_object(tail_path));
+ SPtr<PortModel> head = dynamic_ptr_cast<PortModel>(_object(head_path));
+
+ if (tail && head) {
+ SPtr<GraphModel> graph = connection_graph(tail_path, head_path);
+ SPtr<ArcModel> arc(new ArcModel(tail, head));
+ graph->add_arc(arc);
+ return true;
+ } else {
+ _log.warn(fmt("Failed to connect %1% => %2%\n")
+ % tail_path % head_path);
+ return false;
+ }
+}
+
+void
+ClientStore::operator()(const Connect& msg)
+{
+ attempt_connection(msg.tail, msg.head);
+}
+
+void
+ClientStore::operator()(const Disconnect& msg)
+{
+ SPtr<PortModel> tail = dynamic_ptr_cast<PortModel>(_object(msg.tail));
+ SPtr<PortModel> head = dynamic_ptr_cast<PortModel>(_object(msg.head));
+ SPtr<GraphModel> graph = connection_graph(msg.tail, msg.head);
+ if (graph) {
+ graph->remove_arc(tail.get(), head.get());
+ }
+}
+
+void
+ClientStore::operator()(const DisconnectAll& msg)
+{
+ SPtr<GraphModel> graph = dynamic_ptr_cast<GraphModel>(_object(msg.graph));
+ SPtr<ObjectModel> object = _object(msg.path);
+
+ if (!graph || !object) {
+ _log.error(fmt("Bad disconnect all notification %1% in %2%\n")
+ % msg.path % msg.graph);
+ return;
+ }
+
+ const GraphModel::Arcs arcs = graph->arcs();
+ for (auto a : arcs) {
+ SPtr<ArcModel> arc = dynamic_ptr_cast<ArcModel>(a.second);
+ if (arc->tail()->parent() == object
+ || arc->head()->parent() == object
+ || arc->tail()->path() == msg.path
+ || arc->head()->path() == msg.path) {
+ graph->remove_arc(arc->tail().get(), arc->head().get());
+ }
+ }
+}
+
+} // namespace Client
+} // namespace Ingen
diff --git a/src/client/GraphModel.cpp b/src/client/GraphModel.cpp
new file mode 100644
index 00000000..0723e59b
--- /dev/null
+++ b/src/client/GraphModel.cpp
@@ -0,0 +1,176 @@
+/*
+ This file is part of Ingen.
+ Copyright 2007-2015 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 <cassert>
+
+#include "ingen/URIs.hpp"
+#include "ingen/client/ArcModel.hpp"
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+
+namespace Ingen {
+namespace Client {
+
+void
+GraphModel::add_child(SPtr<ObjectModel> c)
+{
+ assert(c->parent().get() == this);
+
+ SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(c);
+ if (pm) {
+ add_port(pm);
+ return;
+ }
+
+ SPtr<BlockModel> bm = dynamic_ptr_cast<BlockModel>(c);
+ if (bm) {
+ _signal_new_block.emit(bm);
+ }
+}
+
+bool
+GraphModel::remove_child(SPtr<ObjectModel> o)
+{
+ assert(o->path().is_child_of(path()));
+ assert(o->parent().get() == this);
+
+ SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(o);
+ if (pm) {
+ remove_arcs_on(pm);
+ remove_port(pm);
+ }
+
+ SPtr<BlockModel> bm = dynamic_ptr_cast<BlockModel>(o);
+ if (bm) {
+ _signal_removed_block.emit(bm);
+ }
+
+ return true;
+}
+
+void
+GraphModel::remove_arcs_on(SPtr<PortModel> p)
+{
+ // Remove any connections which referred to this object,
+ // since they can't possibly exist anymore
+ for (auto j = _arcs.begin(); j != _arcs.end();) {
+ auto next = j;
+ ++next;
+
+ SPtr<ArcModel> arc = dynamic_ptr_cast<ArcModel>(j->second);
+ if (arc->tail_path().parent() == p->path()
+ || arc->tail_path() == p->path()
+ || arc->head_path().parent() == p->path()
+ || arc->head_path() == p->path()) {
+ _signal_removed_arc.emit(arc);
+ _arcs.erase(j); // Cuts our reference
+ }
+ j = next;
+ }
+}
+
+void
+GraphModel::clear()
+{
+ _arcs.clear();
+
+ BlockModel::clear();
+
+ assert(_arcs.empty());
+ assert(_ports.empty());
+}
+
+SPtr<ArcModel>
+GraphModel::get_arc(const Node* tail, const Node* head)
+{
+ auto i = _arcs.find(std::make_pair(tail, head));
+ if (i != _arcs.end()) {
+ return dynamic_ptr_cast<ArcModel>(i->second);
+ } else {
+ return SPtr<ArcModel>();
+ }
+}
+
+/** Add a connection to this graph.
+ *
+ * A reference to `arc` is taken, released on deletion or removal.
+ * If `arc` only contains paths (not pointers to the actual ports), the ports
+ * will be found and set. The ports referred to not existing as children of
+ * this graph is a fatal error.
+ */
+void
+GraphModel::add_arc(SPtr<ArcModel> arc)
+{
+ // Store should have 'resolved' the connection already
+ assert(arc);
+ assert(arc->tail());
+ assert(arc->head());
+ assert(arc->tail()->parent());
+ assert(arc->head()->parent());
+ assert(arc->tail_path() != arc->head_path());
+ assert(arc->tail()->parent().get() == this
+ || arc->tail()->parent()->parent().get() == this);
+ assert(arc->head()->parent().get() == this
+ || arc->head()->parent()->parent().get() == this);
+
+ SPtr<ArcModel> existing = get_arc(
+ arc->tail().get(), arc->head().get());
+
+ if (existing) {
+ assert(arc->tail() == existing->tail());
+ assert(arc->head() == existing->head());
+ } else {
+ _arcs.emplace(std::make_pair(arc->tail().get(), arc->head().get()),
+ arc);
+ _signal_new_arc.emit(arc);
+ }
+}
+
+void
+GraphModel::remove_arc(const Node* tail, const Node* head)
+{
+ auto i = _arcs.find(std::make_pair(tail, head));
+ if (i != _arcs.end()) {
+ SPtr<ArcModel> arc = dynamic_ptr_cast<ArcModel>(i->second);
+ _signal_removed_arc.emit(arc);
+ _arcs.erase(i);
+ }
+}
+
+bool
+GraphModel::enabled() const
+{
+ const Atom& enabled = get_property(_uris.ingen_enabled);
+ return (enabled.is_valid() && enabled.get<int32_t>());
+}
+
+uint32_t
+GraphModel::internal_poly() const
+{
+ const Atom& poly = get_property(_uris.ingen_polyphony);
+ return poly.is_valid() ? poly.get<int32_t>() : 1;
+}
+
+bool
+GraphModel::polyphonic() const
+{
+ const Atom& poly = get_property(_uris.ingen_polyphonic);
+ return poly.is_valid() && poly.get<int32_t>();
+}
+
+} // namespace Client
+} // namespace Ingen
diff --git a/src/client/ObjectModel.cpp b/src/client/ObjectModel.cpp
new file mode 100644
index 00000000..8d40b120
--- /dev/null
+++ b/src/client/ObjectModel.cpp
@@ -0,0 +1,108 @@
+/*
+ This file is part of Ingen.
+ Copyright 2007-2015 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/Node.hpp"
+#include "ingen/URIs.hpp"
+#include "ingen/client/ObjectModel.hpp"
+
+namespace Ingen {
+namespace Client {
+
+ObjectModel::ObjectModel(URIs& uris, const Raul::Path& path)
+ : Node(uris, path)
+ , _path(path)
+ , _symbol((path == "/") ? "root" : path.symbol())
+{
+}
+
+ObjectModel::ObjectModel(const ObjectModel& copy)
+ : Node(copy)
+ , _parent(copy._parent)
+ , _path(copy._path)
+ , _symbol(copy._symbol)
+{
+}
+
+bool
+ObjectModel::is_a(const URIs::Quark& type) const
+{
+ return has_property(_uris.rdf_type, type);
+}
+
+void
+ObjectModel::on_property(const URI& uri, const Atom& value)
+{
+ _signal_property.emit(uri, value);
+}
+
+void
+ObjectModel::on_property_removed(const URI& uri, const Atom& value)
+{
+ _signal_property_removed.emit(uri, value);
+}
+
+const Atom&
+ObjectModel::get_property(const URI& key) const
+{
+ static const Atom null_atom;
+ auto i = properties().find(key);
+ return (i != properties().end()) ? i->second : null_atom;
+}
+
+bool
+ObjectModel::polyphonic() const
+{
+ const Atom& polyphonic = get_property(_uris.ingen_polyphonic);
+ return (polyphonic.is_valid() && polyphonic.get<int32_t>());
+}
+
+/** Merge the data of `o` with self, as much as possible.
+ *
+ * This will merge the two models, but with any conflict take the value in
+ * `o` as correct. The paths of the two models MUST be equal.
+ */
+void
+ObjectModel::set(SPtr<ObjectModel> o)
+{
+ assert(_path == o->path());
+ if (o->_parent) {
+ _parent = o->_parent;
+ }
+
+ for (auto v : o->properties()) {
+ Resource::set_property(v.first, v.second);
+ _signal_property.emit(v.first, v.second);
+ }
+}
+
+void
+ObjectModel::set_path(const Raul::Path& p)
+{
+ _path = p;
+ _symbol = Raul::Symbol(p.is_root() ? "root" : p.symbol());
+ set_uri(path_to_uri(p));
+ _signal_moved.emit();
+}
+
+void
+ObjectModel::set_parent(SPtr<ObjectModel> p)
+{
+ assert(_path.is_child_of(p->path()));
+ _parent = p;
+}
+
+} // namespace Client
+} // namespace Ingen
diff --git a/src/client/PluginModel.cpp b/src/client/PluginModel.cpp
new file mode 100644
index 00000000..5427b75e
--- /dev/null
+++ b/src/client/PluginModel.cpp
@@ -0,0 +1,360 @@
+/*
+ This file is part of Ingen.
+ Copyright 2007-2015 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 <string>
+#include <algorithm>
+
+#include <boost/optional.hpp>
+
+#include "raul/Path.hpp"
+
+#include "ingen/Atom.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/client/PluginModel.hpp"
+#include "ingen/client/PluginUI.hpp"
+
+#include "ingen_config.h"
+
+using std::string;
+
+namespace Ingen {
+namespace Client {
+
+LilvWorld* PluginModel::_lilv_world = nullptr;
+const LilvPlugins* PluginModel::_lilv_plugins = nullptr;
+
+Sord::World* PluginModel::_rdf_world = nullptr;
+
+PluginModel::PluginModel(URIs& uris,
+ const URI& uri,
+ const Atom& type,
+ const Properties& properties)
+ : Resource(uris, uri)
+ , _type(type)
+ , _fetched(false)
+{
+ if (!_type.is_valid()) {
+ if (uri.string().find("ingen-internals") != string::npos) {
+ _type = uris.ingen_Internal.urid;
+ } else {
+ _type = uris.lv2_Plugin.urid; // Assume LV2 and hope for the best...
+ }
+ }
+
+ add_property(uris.rdf_type, type);
+ add_properties(properties);
+
+ LilvNode* plugin_uri = lilv_new_uri(_lilv_world, uri.c_str());
+ _lilv_plugin = lilv_plugins_get_by_uri(_lilv_plugins, plugin_uri);
+ lilv_node_free(plugin_uri);
+
+ if (uris.ingen_Internal == _type) {
+ set_property(uris.doap_name,
+ uris.forge.alloc(std::string(uri.fragment().substr(1))));
+ }
+}
+
+static size_t
+last_uri_delim(const std::string& str)
+{
+ for (size_t i = str.length() - 1; i > 0; --i) {
+ switch (str[i]) {
+ case ':': case '/': case '?': case '#':
+ return i;
+ }
+ }
+ return string::npos;
+}
+
+static bool
+contains_alpha_after(const std::string& str, size_t begin)
+{
+ for (size_t i = begin; i < str.length(); ++i) {
+ if (isalpha(str[i])) {
+ return true;
+ }
+ }
+ return false;
+}
+
+const Atom&
+PluginModel::get_property(const URI& key) const
+{
+ static const Atom nil;
+ const Atom& val = Resource::get_property(key);
+ if (val.is_valid()) {
+ return val;
+ }
+
+ // No lv2:symbol from data or engine, invent one
+ if (key == _uris.lv2_symbol) {
+ string str = this->uri();
+ size_t last_delim = last_uri_delim(str);
+ while (last_delim != string::npos &&
+ !contains_alpha_after(str, last_delim)) {
+ str = str.substr(0, last_delim);
+ last_delim = last_uri_delim(str);
+ }
+ str = str.substr(last_delim + 1);
+
+ std::string symbol = Raul::Symbol::symbolify(str);
+ set_property(_uris.lv2_symbol, _uris.forge.alloc(symbol));
+ return get_property(key);
+ }
+
+ if (_lilv_plugin) {
+ boost::optional<const Atom&> ret;
+ LilvNode* lv2_pred = lilv_new_uri(_lilv_world, key.c_str());
+ LilvNodes* values = lilv_plugin_get_value(_lilv_plugin, lv2_pred);
+ lilv_node_free(lv2_pred);
+ LILV_FOREACH(nodes, i, values) {
+ const LilvNode* val = lilv_nodes_get(values, i);
+ if (lilv_node_is_uri(val)) {
+ ret = set_property(
+ key, _uris.forge.make_urid(URI(lilv_node_as_uri(val))));
+ break;
+ } else if (lilv_node_is_string(val)) {
+ ret = set_property(
+ key, _uris.forge.alloc(lilv_node_as_string(val)));
+ break;
+ } else if (lilv_node_is_float(val)) {
+ ret = set_property(
+ key, _uris.forge.make(lilv_node_as_float(val)));
+ break;
+ } else if (lilv_node_is_int(val)) {
+ ret = set_property(
+ key, _uris.forge.make(lilv_node_as_int(val)));
+ break;
+ }
+ }
+ lilv_nodes_free(values);
+
+ if (ret) {
+ return *ret;
+ }
+ }
+
+ return nil;
+}
+
+void
+PluginModel::set(SPtr<PluginModel> p)
+{
+ _type = p->_type;
+
+ if (p->_lilv_plugin) {
+ _lilv_plugin = p->_lilv_plugin;
+ }
+
+ for (auto v : p->properties()) {
+ Resource::set_property(v.first, v.second);
+ _signal_property.emit(v.first, v.second);
+ }
+
+ _signal_changed.emit();
+}
+
+void
+PluginModel::add_preset(const URI& uri, const std::string& label)
+{
+ _presets.emplace(uri, label);
+ _signal_preset.emit(uri, label);
+}
+
+Raul::Symbol
+PluginModel::default_block_symbol() const
+{
+ const Atom& name_atom = get_property(_uris.lv2_symbol);
+ if (name_atom.is_valid() && name_atom.type() == _uris.forge.String) {
+ return Raul::Symbol::symbolify(name_atom.ptr<char>());
+ } else {
+ return Raul::Symbol("_");
+ }
+}
+
+string
+PluginModel::human_name() const
+{
+ const Atom& name_atom = get_property(_uris.doap_name);
+ if (name_atom.type() == _uris.forge.String) {
+ return name_atom.ptr<char>();
+ } else {
+ return default_block_symbol().c_str();
+ }
+}
+
+string
+PluginModel::port_human_name(uint32_t i) const
+{
+ if (_lilv_plugin) {
+ const LilvPort* port = lilv_plugin_get_port_by_index(_lilv_plugin, i);
+ LilvNode* name = lilv_port_get_name(_lilv_plugin, port);
+ const string ret(lilv_node_as_string(name));
+ lilv_node_free(name);
+ return ret;
+ }
+ return "";
+}
+
+PluginModel::ScalePoints
+PluginModel::port_scale_points(uint32_t i) const
+{
+ // TODO: Non-float scale points
+ ScalePoints points;
+ if (_lilv_plugin) {
+ const LilvPort* port = lilv_plugin_get_port_by_index(_lilv_plugin, i);
+ LilvScalePoints* sp = lilv_port_get_scale_points(_lilv_plugin, port);
+ LILV_FOREACH(scale_points, i, sp) {
+ const LilvScalePoint* p = lilv_scale_points_get(sp, i);
+ points.emplace(
+ lilv_node_as_float(lilv_scale_point_get_value(p)),
+ lilv_node_as_string(lilv_scale_point_get_label(p)));
+ }
+ }
+ return points;
+}
+
+bool
+PluginModel::has_ui() const
+{
+ if (_lilv_plugin) {
+ LilvUIs* uis = lilv_plugin_get_uis(_lilv_plugin);
+ const bool ret = (lilv_nodes_size(uis) > 0);
+ lilv_uis_free(uis);
+ return ret;
+ }
+ return false;
+}
+
+SPtr<PluginUI>
+PluginModel::ui(Ingen::World* world,
+ SPtr<const BlockModel> block) const
+{
+ if (!_lilv_plugin) {
+ return SPtr<PluginUI>();
+ }
+
+ return PluginUI::create(world, block, _lilv_plugin);
+}
+
+static std::string
+heading(const std::string& text, bool html, unsigned level)
+{
+ if (html) {
+ const std::string tag = std::string("h") + std::to_string(level);
+ return std::string("<") + tag + ">" + text + "</" + tag + ">\n";
+ } else {
+ return text + ":\n\n";
+ }
+}
+
+static std::string
+link(const std::string& addr, bool html)
+{
+ if (html) {
+ return std::string("<a href=\"") + addr + "\">" + addr + "</a>";
+ } else {
+ return addr;
+ }
+}
+
+std::string
+PluginModel::get_documentation(const LilvNode* subject, bool html) const
+{
+ std::string doc;
+
+ LilvNode* lv2_documentation = lilv_new_uri(_lilv_world,
+ LV2_CORE__documentation);
+ LilvNode* rdfs_comment = lilv_new_uri(_lilv_world,
+ LILV_NS_RDFS "comment");
+
+ LilvNodes* vals = lilv_world_find_nodes(
+ _lilv_world, subject, lv2_documentation, nullptr);
+ const bool doc_is_html = vals;
+ if (!vals) {
+ vals = lilv_world_find_nodes(
+ _lilv_world, subject, rdfs_comment, nullptr);
+ }
+
+ if (vals) {
+ const LilvNode* val = lilv_nodes_get_first(vals);
+ if (lilv_node_is_string(val)) {
+ doc += lilv_node_as_string(val);
+ }
+ }
+
+ if (html && !doc_is_html) {
+ for (std::size_t i = 0; i < doc.size(); ++i) {
+ if (doc.substr(i, 2) == "\n\n") {
+ doc.replace(i, 2, "<br/><br/>");
+ i += strlen("<br/><br/>");
+ }
+ }
+ }
+
+ lilv_node_free(rdfs_comment);
+ lilv_node_free(lv2_documentation);
+
+ return doc;
+}
+
+std::string
+PluginModel::documentation(const bool html) const
+{
+ LilvNode* subject = (_lilv_plugin)
+ ? lilv_node_duplicate(lilv_plugin_get_uri(_lilv_plugin))
+ : lilv_new_uri(_lilv_world, uri().c_str());
+
+ const std::string doc(get_documentation(subject, html));
+
+ lilv_node_free(subject);
+
+ return (heading(human_name(), html, 2) +
+ link(uri(), html) + (html ? "<br/><br/>" : "\n\n") +
+ doc);
+}
+
+std::string
+PluginModel::port_documentation(uint32_t index, bool html) const
+{
+ if (!_lilv_plugin) {
+ return "";
+ }
+
+ const LilvPort* port = lilv_plugin_get_port_by_index(_lilv_plugin, index);
+ if (!port) {
+ return "";
+ }
+
+ return (heading(port_human_name(index), html, 2) +
+ get_documentation(lilv_port_get_node(_lilv_plugin, port), html));
+}
+
+const LilvPort*
+PluginModel::lilv_port(uint32_t index) const
+{
+ return lilv_plugin_get_port_by_index(_lilv_plugin, index);
+}
+
+void
+PluginModel::set_lilv_world(LilvWorld* world)
+{
+ _lilv_world = world;
+ _lilv_plugins = lilv_world_get_all_plugins(_lilv_world);
+}
+
+} // namespace Client
+} // namespace Ingen
diff --git a/src/client/PluginUI.cpp b/src/client/PluginUI.cpp
new file mode 100644
index 00000000..df983f7f
--- /dev/null
+++ b/src/client/PluginUI.cpp
@@ -0,0 +1,336 @@
+/*
+ This file is part of Ingen.
+ Copyright 2007-2015 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/Interface.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/URIs.hpp"
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/client/PluginUI.hpp"
+#include "ingen/client/PortModel.hpp"
+#include "lv2/lv2plug.in/ns/ext/atom/atom.h"
+#include "lv2/lv2plug.in/ns/extensions/ui/ui.h"
+
+namespace Ingen {
+namespace Client {
+
+SuilHost* PluginUI::ui_host = nullptr;
+
+static SPtr<const PortModel>
+get_port(PluginUI* ui, uint32_t port_index)
+{
+ if (port_index >= ui->block()->ports().size()) {
+ ui->world()->log().error(
+ fmt("%1% UI tried to access invalid port %2%\n")
+ % ui->block()->plugin()->uri().c_str() % port_index);
+ return SPtr<const PortModel>();
+ }
+ return ui->block()->ports()[port_index];
+}
+
+static void
+lv2_ui_write(SuilController controller,
+ uint32_t port_index,
+ uint32_t buffer_size,
+ uint32_t format,
+ const void* buffer)
+{
+ PluginUI* const ui = (PluginUI*)controller;
+ const URIs& uris = ui->world()->uris();
+ SPtr<const PortModel> port = get_port(ui, port_index);
+ if (!port) {
+ return;
+ }
+
+ // float (special case, always 0)
+ if (format == 0) {
+ if (buffer_size != 4) {
+ ui->world()->log().error(
+ fmt("%1% UI wrote corrupt float with bad size\n")
+ % ui->block()->plugin()->uri().c_str());
+ return;
+ }
+ const float value = *(const float*)buffer;
+ if (port->value().type() == uris.atom_Float &&
+ value == port->value().get<float>()) {
+ return; // Ignore feedback
+ }
+
+ ui->signal_property_changed()(
+ port->uri(),
+ uris.ingen_value,
+ ui->world()->forge().make(value),
+ Resource::Graph::DEFAULT);
+
+ } else if (format == uris.atom_eventTransfer.urid.get<LV2_URID>()) {
+ const LV2_Atom* atom = (const LV2_Atom*)buffer;
+ Atom val = ui->world()->forge().alloc(
+ atom->size, atom->type, LV2_ATOM_BODY_CONST(atom));
+ ui->signal_property_changed()(port->uri(),
+ uris.ingen_activity,
+ val,
+ Resource::Graph::DEFAULT);
+ } else {
+ ui->world()->log().warn(
+ fmt("Unknown value format %1% from LV2 UI\n")
+ % format % ui->block()->plugin()->uri().c_str());
+ }
+}
+
+static uint32_t
+lv2_ui_port_index(SuilController controller, const char* port_symbol)
+{
+ PluginUI* const ui = (PluginUI*)controller;
+
+ const BlockModel::Ports& ports = ui->block()->ports();
+ for (uint32_t i = 0; i < ports.size(); ++i) {
+ if (ports[i]->symbol() == port_symbol) {
+ return i;
+ }
+ }
+ return LV2UI_INVALID_PORT_INDEX;
+}
+
+static uint32_t
+lv2_ui_subscribe(SuilController controller,
+ uint32_t port_index,
+ uint32_t protocol,
+ const LV2_Feature* const* features)
+{
+ PluginUI* const ui = (PluginUI*)controller;
+ SPtr<const PortModel> port = get_port(ui, port_index);
+ if (!port) {
+ return 1;
+ }
+
+ ui->signal_property_changed()(
+ ui->block()->ports()[port_index]->uri(),
+ ui->world()->uris().ingen_broadcast,
+ ui->world()->forge().make(true),
+ Resource::Graph::DEFAULT);
+
+ return 0;
+}
+
+static uint32_t
+lv2_ui_unsubscribe(SuilController controller,
+ uint32_t port_index,
+ uint32_t protocol,
+ const LV2_Feature* const* features)
+{
+ PluginUI* const ui = (PluginUI*)controller;
+ SPtr<const PortModel> port = get_port(ui, port_index);
+ if (!port) {
+ return 1;
+ }
+
+ ui->signal_property_changed()(
+ ui->block()->ports()[port_index]->uri(),
+ ui->world()->uris().ingen_broadcast,
+ ui->world()->forge().make(false),
+ Resource::Graph::DEFAULT);
+
+ return 0;
+}
+
+PluginUI::PluginUI(Ingen::World* world,
+ SPtr<const BlockModel> block,
+ LilvUIs* uis,
+ const LilvUI* ui,
+ const LilvNode* ui_type)
+ : _world(world)
+ , _block(std::move(block))
+ , _instance(nullptr)
+ , _uis(uis)
+ , _ui(ui)
+ , _ui_node(lilv_node_duplicate(lilv_ui_get_uri(ui)))
+ , _ui_type(lilv_node_duplicate(ui_type))
+{
+}
+
+PluginUI::~PluginUI()
+{
+ for (uint32_t i : _subscribed_ports) {
+ lv2_ui_unsubscribe(this, i, 0, nullptr);
+ }
+ suil_instance_free(_instance);
+ lilv_node_free(_ui_node);
+ lilv_node_free(_ui_type);
+ lilv_uis_free(_uis);
+ lilv_world_unload_resource(_world->lilv_world(), lilv_ui_get_uri(_ui));
+}
+
+SPtr<PluginUI>
+PluginUI::create(Ingen::World* world,
+ SPtr<const BlockModel> block,
+ const LilvPlugin* plugin)
+{
+ if (!PluginUI::ui_host) {
+ PluginUI::ui_host = suil_host_new(lv2_ui_write,
+ lv2_ui_port_index,
+ lv2_ui_subscribe,
+ lv2_ui_unsubscribe);
+ }
+
+ static const char* gtk_ui_uri = LV2_UI__GtkUI;
+
+ LilvNode* gtk_ui = lilv_new_uri(world->lilv_world(), gtk_ui_uri);
+
+ LilvUIs* uis = lilv_plugin_get_uis(plugin);
+ const LilvUI* ui = nullptr;
+ const LilvNode* ui_type = nullptr;
+ LILV_FOREACH(uis, u, uis) {
+ const LilvUI* this_ui = lilv_uis_get(uis, u);
+ if (lilv_ui_is_supported(this_ui,
+ suil_ui_supported,
+ gtk_ui,
+ &ui_type)) {
+ // TODO: Multiple UI support
+ ui = this_ui;
+ break;
+ }
+ }
+
+ if (!ui) {
+ lilv_node_free(gtk_ui);
+ return SPtr<PluginUI>();
+ }
+
+ // Create the PluginUI, but don't instantiate yet
+ SPtr<PluginUI> ret(new PluginUI(world, block, uis, ui, ui_type));
+ ret->_features = world->lv2_features().lv2_features(
+ world, const_cast<BlockModel*>(block.get()));
+
+ return ret;
+}
+
+bool
+PluginUI::instantiate()
+{
+ const URIs& uris = _world->uris();
+ const std::string plugin_uri = _block->plugin()->uri();
+ LilvWorld* lworld = _world->lilv_world();
+
+ // Load seeAlso files to access data like portNotification descriptions
+ lilv_world_load_resource(lworld, lilv_ui_get_uri(_ui));
+
+ /* Subscribe (enable broadcast) for any requested port notifications. This
+ must be done before instantiation so responses to any events sent by the
+ UI's init() will be sent back to this client. */
+ LilvNode* ui_portNotification = lilv_new_uri(lworld, LV2_UI__portNotification);
+ LilvNode* ui_plugin = lilv_new_uri(lworld, LV2_UI__plugin);
+ LilvNodes* notes = lilv_world_find_nodes(
+ lworld, lilv_ui_get_uri(_ui), ui_portNotification, nullptr);
+ LILV_FOREACH(nodes, n, notes) {
+ const LilvNode* note = lilv_nodes_get(notes, n);
+ const LilvNode* sym = lilv_world_get(lworld, note, uris.lv2_symbol, nullptr);
+ const LilvNode* plug = lilv_world_get(lworld, note, ui_plugin, nullptr);
+ if (!plug) {
+ _world->log().error(fmt("%1% UI %2% notification missing plugin\n")
+ % plugin_uri % lilv_node_as_string(_ui_node));
+ } else if (!sym) {
+ _world->log().error(fmt("%1% UI %2% notification missing symbol\n")
+ % plugin_uri % lilv_node_as_string(_ui_node));
+ } else if (!lilv_node_is_uri(plug)) {
+ _world->log().error(fmt("%1% UI %2% notification has non-URI plugin\n")
+ % plugin_uri % lilv_node_as_string(_ui_node));
+ } else if (!strcmp(lilv_node_as_uri(plug), plugin_uri.c_str())) {
+ // Notification is valid and for this plugin
+ uint32_t index = lv2_ui_port_index(this, lilv_node_as_string(sym));
+ if (index != LV2UI_INVALID_PORT_INDEX) {
+ lv2_ui_subscribe(this, index, 0, nullptr);
+ _subscribed_ports.insert(index);
+ }
+ }
+ }
+ lilv_nodes_free(notes);
+ lilv_node_free(ui_plugin);
+ lilv_node_free(ui_portNotification);
+
+ const char* bundle_uri = lilv_node_as_uri(lilv_ui_get_bundle_uri(_ui));
+ const char* binary_uri = lilv_node_as_uri(lilv_ui_get_binary_uri(_ui));
+ char* bundle_path = lilv_file_uri_parse(bundle_uri, nullptr);
+ char* binary_path = lilv_file_uri_parse(binary_uri, nullptr);
+
+ // Instantiate the actual plugin UI via Suil
+ _instance = suil_instance_new(
+ PluginUI::ui_host,
+ this,
+ LV2_UI__GtkUI,
+ plugin_uri.c_str(),
+ lilv_node_as_uri(lilv_ui_get_uri(_ui)),
+ lilv_node_as_uri(_ui_type),
+ bundle_path,
+ binary_path,
+ _features->array());
+
+ lilv_free(binary_path);
+ lilv_free(bundle_path);
+
+ if (!_instance) {
+ _world->log().error("Failed to instantiate LV2 UI\n");
+ // Cancel any subscriptions
+ for (uint32_t i : _subscribed_ports) {
+ lv2_ui_unsubscribe(this, i, 0, nullptr);
+ }
+ return false;
+ }
+
+ return true;
+}
+
+SuilWidget
+PluginUI::get_widget()
+{
+ return (SuilWidget*)suil_instance_get_widget(_instance);
+}
+
+void
+PluginUI::port_event(uint32_t port_index,
+ uint32_t buffer_size,
+ uint32_t format,
+ const void* buffer)
+{
+ if (_instance) {
+ suil_instance_port_event(
+ _instance, port_index, buffer_size, format, buffer);
+ } else {
+ _world->log().warn("LV2 UI port event with no instance\n");
+ }
+}
+
+bool
+PluginUI::is_resizable() const
+{
+ LilvWorld* w = _world->lilv_world();
+ const LilvNode* s = _ui_node;
+ LilvNode* p = lilv_new_uri(w, LV2_CORE__optionalFeature);
+ LilvNode* fs = lilv_new_uri(w, LV2_UI__fixedSize);
+ LilvNode* nrs = lilv_new_uri(w, LV2_UI__noUserResize);
+
+ LilvNodes* fs_matches = lilv_world_find_nodes(w, s, p, fs);
+ LilvNodes* nrs_matches = lilv_world_find_nodes(w, s, p, nrs);
+
+ lilv_nodes_free(nrs_matches);
+ lilv_nodes_free(fs_matches);
+ lilv_node_free(nrs);
+ lilv_node_free(fs);
+ lilv_node_free(p);
+
+ return !fs_matches && !nrs_matches;
+}
+
+} // namespace Client
+} // namespace Ingen
diff --git a/src/client/PortModel.cpp b/src/client/PortModel.cpp
new file mode 100644
index 00000000..5c9a8c77
--- /dev/null
+++ b/src/client/PortModel.cpp
@@ -0,0 +1,78 @@
+/*
+ This file is part of Ingen.
+ Copyright 2007-2015 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/client/BlockModel.hpp"
+#include "ingen/client/PortModel.hpp"
+
+namespace Ingen {
+namespace Client {
+
+void
+PortModel::on_property(const URI& uri, const Atom& value)
+{
+ if (uri == _uris.ingen_activity) {
+ // Don't store activity, it is transient
+ signal_activity().emit(value);
+ return;
+ }
+
+ ObjectModel::on_property(uri, value);
+
+ if (uri == _uris.ingen_value) {
+ signal_value_changed().emit(value);
+ }
+}
+
+bool
+PortModel::supports(const URIs::Quark& value_type) const
+{
+ return has_property(_uris.atom_supports, value_type);
+}
+
+bool
+PortModel::port_property(const URIs::Quark& uri) const
+{
+ return has_property(_uris.lv2_portProperty, uri);
+}
+
+bool
+PortModel::is_uri() const
+{
+ // FIXME: Resource::has_property doesn't work, URI != URID
+ for (auto p : properties()) {
+ if (p.second.type() == _uris.atom_URID &&
+ static_cast<LV2_URID>(p.second.get<int32_t>()) == _uris.atom_URID) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void
+PortModel::set(SPtr<ObjectModel> model)
+{
+ ObjectModel::set(model);
+
+ SPtr<PortModel> port = dynamic_ptr_cast<PortModel>(model);
+ if (port) {
+ _index = port->_index;
+ _direction = port->_direction;
+ _signal_value_changed.emit(get_property(_uris.ingen_value));
+ }
+}
+
+} // namespace Client
+} // namespace Ingen
diff --git a/src/client/ingen_client.cpp b/src/client/ingen_client.cpp
new file mode 100644
index 00000000..fe9d6605
--- /dev/null
+++ b/src/client/ingen_client.cpp
@@ -0,0 +1,34 @@
+/*
+ This file is part of Ingen.
+ Copyright 2007-2015 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/Module.hpp"
+#include "ingen/World.hpp"
+
+#include "ingen_config.h"
+
+struct IngenClientModule : public Ingen::Module {
+ void load(Ingen::World* world) {}
+};
+
+extern "C" {
+
+Ingen::Module*
+ingen_module_load()
+{
+ return new IngenClientModule();
+}
+
+} // extern "C"
diff --git a/src/client/wscript b/src/client/wscript
new file mode 100644
index 00000000..df575c0d
--- /dev/null
+++ b/src/client/wscript
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+from waflib.extras import autowaf as autowaf
+
+def build(bld):
+ obj = bld(features = 'cxx cxxshlib',
+ includes = ['../..'],
+ export_includes = ['../..'],
+ name = 'libingen_client',
+ target = 'ingen_client',
+ install_path = '${LIBDIR}',
+ use = 'libingen')
+ autowaf.use_lib(bld, obj, 'GLIBMM LV2 LILV SUIL RAUL SERD SORD SIGCPP')
+
+ obj.source = '''
+ BlockModel.cpp
+ ClientStore.cpp
+ GraphModel.cpp
+ ObjectModel.cpp
+ PluginModel.cpp
+ PluginUI.cpp
+ PortModel.cpp
+ ingen_client.cpp
+ '''