summaryrefslogtreecommitdiffstats
path: root/src/gui
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/App.cpp499
-rw-r--r--src/gui/App.hpp196
-rw-r--r--src/gui/Arc.cpp45
-rw-r--r--src/gui/Arc.hpp52
-rw-r--r--src/gui/BreadCrumbs.cpp229
-rw-r--r--src/gui/BreadCrumbs.hpp121
-rw-r--r--src/gui/ConnectWindow.cpp573
-rw-r--r--src/gui/ConnectWindow.hpp124
-rw-r--r--src/gui/GraphBox.cpp917
-rw-r--r--src/gui/GraphBox.hpp213
-rw-r--r--src/gui/GraphCanvas.cpp912
-rw-r--r--src/gui/GraphCanvas.hpp158
-rw-r--r--src/gui/GraphPortModule.cpp166
-rw-r--r--src/gui/GraphPortModule.hpp79
-rw-r--r--src/gui/GraphTreeWindow.cpp233
-rw-r--r--src/gui/GraphTreeWindow.hpp123
-rw-r--r--src/gui/GraphView.cpp154
-rw-r--r--src/gui/GraphView.hpp101
-rw-r--r--src/gui/GraphWindow.cpp85
-rw-r--r--src/gui/GraphWindow.hpp80
-rw-r--r--src/gui/LoadGraphWindow.cpp259
-rw-r--r--src/gui/LoadGraphWindow.hpp97
-rw-r--r--src/gui/LoadPluginWindow.cpp508
-rw-r--r--src/gui/LoadPluginWindow.hpp160
-rw-r--r--src/gui/MessagesWindow.cpp145
-rw-r--r--src/gui/MessagesWindow.hpp73
-rw-r--r--src/gui/NewSubgraphWindow.cpp121
-rw-r--r--src/gui/NewSubgraphWindow.hpp72
-rw-r--r--src/gui/NodeMenu.cpp258
-rw-r--r--src/gui/NodeMenu.hpp76
-rw-r--r--src/gui/NodeModule.cpp518
-rw-r--r--src/gui/NodeModule.hpp104
-rw-r--r--src/gui/ObjectMenu.cpp146
-rw-r--r--src/gui/ObjectMenu.hpp78
-rw-r--r--src/gui/PluginMenu.cpp178
-rw-r--r--src/gui/PluginMenu.hpp81
-rw-r--r--src/gui/Port.cpp535
-rw-r--r--src/gui/Port.hpp103
-rw-r--r--src/gui/PortMenu.cpp175
-rw-r--r--src/gui/PortMenu.hpp66
-rw-r--r--src/gui/PropertiesWindow.cpp593
-rw-r--r--src/gui/PropertiesWindow.hpp131
-rw-r--r--src/gui/RDFS.cpp261
-rw-r--r--src/gui/RDFS.hpp81
-rw-r--r--src/gui/RenameWindow.cpp137
-rw-r--r--src/gui/RenameWindow.hpp64
-rw-r--r--src/gui/Style.cpp107
-rw-r--r--src/gui/Style.hpp56
-rw-r--r--src/gui/SubgraphModule.cpp103
-rw-r--r--src/gui/SubgraphModule.hpp64
-rw-r--r--src/gui/ThreadedLoader.cpp149
-rw-r--r--src/gui/ThreadedLoader.hpp99
-rw-r--r--src/gui/URIEntry.cpp194
-rw-r--r--src/gui/URIEntry.hpp72
-rw-r--r--src/gui/WidgetFactory.cpp82
-rw-r--r--src/gui/WidgetFactory.hpp61
-rw-r--r--src/gui/Window.hpp79
-rw-r--r--src/gui/WindowFactory.cpp304
-rw-r--r--src/gui/WindowFactory.hpp100
-rw-r--r--src/gui/ingen_gui.cpp67
-rw-r--r--src/gui/ingen_gui.gladep9
-rw-r--r--src/gui/ingen_gui.ui3049
-rw-r--r--src/gui/ingen_gui_lv2.cpp226
-rw-r--r--src/gui/ingen_style.rc150
-rw-r--r--src/gui/rgba.hpp58
-rw-r--r--src/gui/wscript111
66 files changed, 15220 insertions, 0 deletions
diff --git a/src/gui/App.cpp b/src/gui/App.cpp
new file mode 100644
index 00000000..dfa34998
--- /dev/null
+++ b/src/gui/App.cpp
@@ -0,0 +1,499 @@
+/*
+ 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 "App.hpp"
+
+#include "ConnectWindow.hpp"
+#include "GraphTreeWindow.hpp"
+#include "GraphWindow.hpp"
+#include "LoadPluginWindow.hpp"
+#include "MessagesWindow.hpp"
+#include "NodeModule.hpp"
+#include "Port.hpp"
+#include "RDFS.hpp"
+#include "Style.hpp"
+#include "SubgraphModule.hpp"
+#include "ThreadedLoader.hpp"
+#include "WidgetFactory.hpp"
+#include "WindowFactory.hpp"
+#include "rgba.hpp"
+
+#include "ganv/Edge.hpp"
+#include "ingen/Configuration.hpp"
+#include "ingen/EngineBase.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/QueuedInterface.hpp"
+#include "ingen/StreamWriter.hpp"
+#include "ingen/World.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/client/ObjectModel.hpp"
+#include "ingen/client/PortModel.hpp"
+#include "ingen/client/SigClientInterface.hpp"
+#include "ingen/runtime_paths.hpp"
+#include "lilv/lilv.h"
+#include "raul/Path.hpp"
+#include "suil/suil.h"
+
+#include <boost/variant/get.hpp>
+#include <gtk/gtkwindow.h>
+#include <gtkmm/stock.h>
+
+#include <cassert>
+#include <fstream>
+#include <string>
+#include <utility>
+
+namespace Raul { class Deletable; }
+
+namespace ingen {
+
+namespace client { class PluginModel; }
+
+using namespace client;
+
+namespace gui {
+
+class Port;
+
+Gtk::Main* App::_main = nullptr;
+
+App::App(ingen::World& world)
+ : _style(new Style(*this))
+ , _about_dialog(nullptr)
+ , _window_factory(new WindowFactory(*this))
+ , _world(world)
+ , _sample_rate(48000)
+ , _block_length(1024)
+ , _n_threads(1)
+ , _mean_run_load(0.0f)
+ , _min_run_load(0.0f)
+ , _max_run_load(0.0f)
+ , _enable_signal(true)
+ , _requested_plugins(false)
+ , _is_plugin(false)
+{
+ _world.conf().load_default("ingen", "gui.ttl");
+
+ WidgetFactory::get_widget_derived("connect_win", _connect_window);
+ WidgetFactory::get_widget_derived("messages_win", _messages_window);
+ WidgetFactory::get_widget_derived("graph_tree_win", _graph_tree_window);
+ WidgetFactory::get_widget("about_win", _about_dialog);
+ _connect_window->init_dialog(*this);
+ _messages_window->init_window(*this);
+ _graph_tree_window->init_window(*this);
+ _about_dialog->property_program_name() = "Ingen";
+ _about_dialog->property_logo_icon_name() = "ingen";
+
+ PluginModel::set_rdf_world(*world.rdf_world());
+ PluginModel::set_lilv_world(world.lilv_world());
+
+ using namespace std::placeholders;
+ world.log().set_sink(std::bind(&MessagesWindow::log, _messages_window, _1, _2, _3));
+}
+
+App::~App()
+{
+ delete _style;
+ delete _window_factory;
+}
+
+SPtr<App>
+App::create(ingen::World& world)
+{
+ suil_init(&world.argc(), &world.argv(), SUIL_ARG_NONE);
+
+ // Add RC file for embedded GUI Gtk style
+ const std::string rc_path = ingen::data_file_path("ingen_style.rc");
+ Gtk::RC::add_default_file(rc_path);
+
+ _main = Gtk::Main::instance();
+ if (!_main) {
+ Glib::set_application_name("Ingen");
+ gtk_window_set_default_icon_name("ingen");
+ _main = new Gtk::Main(&world.argc(), &world.argv());
+ }
+
+ auto app = SPtr<App>{new App(world)};
+
+ // Load configuration settings
+ app->style()->load_settings();
+ app->style()->apply_settings();
+
+ // Set default window icon
+ app->_about_dialog->property_program_name() = "Ingen";
+ app->_about_dialog->property_logo_icon_name() = "ingen";
+ gtk_window_set_default_icon_name("ingen");
+
+ return app;
+}
+
+void
+App::run()
+{
+ _connect_window->start(*this, world());
+
+ // Run main iterations here until we're attached to the engine. Otherwise
+ // with 'ingen -egl' we'd get a bunch of notifications about load
+ // immediately before even knowing about the root graph or plugins)
+ while (!_connect_window->attached()) {
+ if (_main->iteration()) {
+ break;
+ }
+ }
+
+ _main->run();
+}
+
+void
+App::attach(SPtr<ingen::Interface> client)
+{
+ assert(!_client);
+ assert(!_store);
+ assert(!_loader);
+
+ if (_world.engine()) {
+ _world.engine()->register_client(client);
+ }
+
+ _client = client;
+ _store = SPtr<ClientStore>(new ClientStore(_world.uris(), _world.log(), sig_client()));
+ _loader = SPtr<ThreadedLoader>(new ThreadedLoader(*this, _world.interface()));
+ if (!_world.store()) {
+ _world.set_store(_store);
+ }
+
+ if (_world.conf().option("dump").get<int32_t>()) {
+ _dumper = SPtr<StreamWriter>(new StreamWriter(_world.uri_map(),
+ _world.uris(),
+ URI("ingen:/client"),
+ stderr,
+ ColorContext::Color::CYAN));
+
+ sig_client()->signal_message().connect(
+ sigc::mem_fun(*_dumper.get(), &StreamWriter::message));
+ }
+
+ _graph_tree_window->init(*this, *_store);
+ sig_client()->signal_message().connect(sigc::mem_fun(this, &App::message));
+}
+
+void
+App::detach()
+{
+ if (_world.interface()) {
+ _window_factory->clear();
+ _store->clear();
+
+ _loader.reset();
+ _store.reset();
+ _client.reset();
+ _world.set_interface(SPtr<Interface>());
+ }
+}
+
+void
+App::request_plugins_if_necessary()
+{
+ if (!_requested_plugins) {
+ _world.interface()->get(URI("ingen:/plugins"));
+ _requested_plugins = true;
+ }
+}
+
+SPtr<SigClientInterface>
+App::sig_client()
+{
+ SPtr<QueuedInterface> qi = dynamic_ptr_cast<QueuedInterface>(_client);
+ if (qi) {
+ return dynamic_ptr_cast<SigClientInterface>(qi->sink());
+ }
+ return dynamic_ptr_cast<SigClientInterface>(_client);
+}
+
+SPtr<Serialiser>
+App::serialiser()
+{
+ return _world.serialiser();
+}
+
+void
+App::message(const Message& msg)
+{
+ if (const Response* const r = boost::get<Response>(&msg)) {
+ response(r->id, r->status, r->subject);
+ } else if (const Error* const e = boost::get<Error>(&msg)) {
+ error_message(e->message);
+ } else if (const Put* const p = boost::get<Put>(&msg)) {
+ put(p->uri, p->properties, p->ctx);
+ } else if (const SetProperty* const s = boost::get<SetProperty>(&msg)) {
+ property_change(s->subject, s->predicate, s->value, s->ctx);
+ }
+}
+
+void
+App::response(int32_t id, Status status, const std::string& subject)
+{
+ if (status != Status::SUCCESS) {
+ std::string msg = ingen_status_string(status);
+ if (!subject.empty()) {
+ msg += ": " + subject;
+ }
+ error_message(msg);
+ }
+}
+
+void
+App::error_message(const std::string& str)
+{
+ _messages_window->post_error(str);
+}
+
+void
+App::set_property(const URI& subject,
+ const URI& key,
+ const Atom& value,
+ Resource::Graph ctx)
+{
+ // Send message to server
+ interface()->set_property(subject, key, value, ctx);
+
+ /* The server does not feed back set messages (kludge to prevent control
+ feedback and bandwidth wastage, see Delta.cpp). So, assume everything
+ went as planned here and fire the signal ourselves as if the server
+ feedback came back immediately. */
+ if (key != uris().ingen_activity) {
+ sig_client()->signal_message().emit(SetProperty{0, subject, key, value, ctx});
+ }
+}
+
+void
+App::set_tooltip(Gtk::Widget* widget, const LilvNode* node)
+{
+ const std::string comment = rdfs::comment(_world, node);
+ if (!comment.empty()) {
+ widget->set_tooltip_text(comment);
+ }
+}
+
+void
+App::put(const URI& uri,
+ const Properties& properties,
+ Resource::Graph ctx)
+{
+ _enable_signal = false;
+ for (const auto& p : properties) {
+ property_change(uri, p.first, p.second);
+ }
+ _enable_signal = true;
+ _status_text = status_text();
+ signal_status_text_changed.emit(_status_text);
+}
+
+void
+App::property_change(const URI& subject,
+ const URI& key,
+ const Atom& value,
+ Resource::Graph ctx)
+{
+ if (subject != URI("ingen:/engine")) {
+ return;
+ } else if (key == uris().param_sampleRate && value.type() == forge().Int) {
+ _sample_rate = value.get<int32_t>();
+ } else if (key == uris().bufsz_maxBlockLength && value.type() == forge().Int) {
+ _block_length = value.get<int32_t>();
+ } else if (key == uris().ingen_numThreads && value.type() == forge().Int) {
+ _n_threads = value.get<int>();
+ } else if (key == uris().ingen_minRunLoad && value.type() == forge().Float) {
+ _min_run_load = value.get<float>();
+ } else if (key == uris().ingen_meanRunLoad && value.type() == forge().Float) {
+ _mean_run_load = value.get<float>();
+ } else if (key == uris().ingen_maxRunLoad && value.type() == forge().Float) {
+ _max_run_load = value.get<float>();
+ } else {
+ _world.log().warn("Unknown engine property %1%\n", key);
+ return;
+ }
+
+ if (_enable_signal) {
+ _status_text = status_text();
+ signal_status_text_changed.emit(_status_text);
+ }
+}
+
+static std::string
+fraction_label(float f)
+{
+ static const uint32_t GREEN = 0x4A8A0EFF;
+ static const uint32_t RED = 0x960909FF;
+
+ const uint32_t col = rgba_interpolate(GREEN, RED, std::min(f, 1.0f));
+ char col_str[8];
+ snprintf(col_str, sizeof(col_str), "%02X%02X%02X",
+ RGBA_R(col), RGBA_G(col), RGBA_B(col));
+ return fmt("<span color='#%s'>%d%%</span>", col_str, (f * 100));
+}
+
+std::string
+App::status_text() const
+{
+ return fmt(
+ "%2.1f kHz / %.1f ms, %s, %s DSP",
+ (_sample_rate / 1e3f),
+ (_block_length * 1e3f / (float)_sample_rate),
+ ((_n_threads == 1) ? "1 thread" : fmt("%1% threads", _n_threads)),
+ fraction_label(_max_run_load));
+}
+
+void
+App::port_activity(Port* port)
+{
+ std::pair<ActivityPorts::iterator, bool> inserted = _activity_ports.emplace(port, false);
+ if (inserted.second) {
+ inserted.first->second = false;
+ }
+
+ port->set_highlighted(true);
+}
+
+void
+App::activity_port_destroyed(Port* port)
+{
+ auto i = _activity_ports.find(port);
+ if (i != _activity_ports.end()) {
+ _activity_ports.erase(i);
+ }
+}
+
+bool
+App::animate()
+{
+ for (auto i = _activity_ports.begin(); i != _activity_ports.end(); ) {
+ auto next = i;
+ ++next;
+
+ if ((*i).second) { // saw it last time, unhighlight and pop
+ (*i).first->set_highlighted(false);
+ _activity_ports.erase(i);
+ } else {
+ (*i).second = true;
+ }
+
+ i = next;
+ }
+
+ return true;
+}
+
+/******** Event Handlers ************/
+
+void
+App::register_callbacks()
+{
+ Glib::signal_timeout().connect(
+ sigc::mem_fun(*this, &App::gtk_main_iteration), 33, G_PRIORITY_DEFAULT);
+}
+
+bool
+App::gtk_main_iteration()
+{
+ if (!_client) {
+ return false;
+ }
+
+ animate();
+
+ if (_messages_window) {
+ _messages_window->flush();
+ }
+
+ _enable_signal = false;
+ if (_world.engine()) {
+ if (!_world.engine()->main_iteration()) {
+ Gtk::Main::quit();
+ return false;
+ }
+ } else {
+ dynamic_ptr_cast<QueuedInterface>(_client)->emit();
+ }
+ _enable_signal = true;
+
+ return true;
+}
+
+void
+App::show_about()
+{
+ _about_dialog->run();
+ _about_dialog->hide();
+}
+
+/** Prompt (if necessary) and quit application (if confirmed).
+ * @return true iff the application quit.
+ */
+bool
+App::quit(Gtk::Window* dialog_parent)
+{
+ bool quit = true;
+ if (_world.engine() && _connect_window->attached()) {
+ Gtk::MessageDialog d(
+ "The engine is running in this process. Quitting will terminate Ingen."
+ "\n\n" "Are you sure you want to quit?",
+ true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE, true);
+ if (dialog_parent) {
+ d.set_transient_for(*dialog_parent);
+ }
+ d.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
+ d.add_button(Gtk::Stock::QUIT, Gtk::RESPONSE_CLOSE);
+ quit = (d.run() == Gtk::RESPONSE_CLOSE);
+ }
+
+ if (!quit) {
+ return false;
+ }
+
+ Gtk::Main::quit();
+
+ try {
+ const std::string path = _world.conf().save(
+ _world.uri_map(), "ingen", "gui.ttl", Configuration::GUI);
+ std::cout << fmt("Saved GUI settings to %1%\n", path);
+ } catch (const std::exception& e) {
+ std::cerr << fmt("Error saving GUI settings (%1%)\n", e.what());
+ }
+
+ return true;
+}
+
+bool
+App::can_control(const client::PortModel* port) const
+{
+ return port->is_a(uris().lv2_ControlPort)
+ || port->is_a(uris().lv2_CVPort)
+ || (port->is_a(uris().atom_AtomPort)
+ && (port->supports(uris().atom_Float)
+ || port->supports(uris().atom_String)));
+}
+
+uint32_t
+App::sample_rate() const
+{
+ return _sample_rate;
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/App.hpp b/src/gui/App.hpp
new file mode 100644
index 00000000..47352f50
--- /dev/null
+++ b/src/gui/App.hpp
@@ -0,0 +1,196 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_APP_HPP
+#define INGEN_GUI_APP_HPP
+
+#include "ingen/Atom.hpp"
+#include "ingen/Message.hpp"
+#include "ingen/Resource.hpp"
+#include "ingen/Status.hpp"
+#include "ingen/World.hpp"
+#include "ingen/ingen.h"
+#include "ingen/types.hpp"
+#include "lilv/lilv.h"
+#include "raul/Deletable.hpp"
+
+#include <gtkmm/aboutdialog.h>
+#include <gtkmm/main.h>
+#include <gtkmm/window.h>
+
+#include <unordered_map>
+#include <string>
+
+namespace ingen {
+
+class Interface;
+class Log;
+class Port;
+class Serialiser;
+class StreamWriter;
+class World;
+
+namespace client {
+
+class ClientStore;
+class GraphModel;
+class PluginModel;
+class PortModel;
+class SigClientInterface;
+
+}
+
+namespace gui {
+
+class ConnectWindow;
+class GraphCanvas;
+class GraphTreeView;
+class GraphTreeWindow;
+class MessagesWindow;
+class Port;
+class Style;
+class ThreadedLoader;
+class WindowFactory;
+
+/** Ingen Gtk Application.
+ * \ingroup GUI
+ */
+class INGEN_API App
+{
+public:
+ ~App();
+
+ void error_message(const std::string& str);
+
+ void attach(SPtr<ingen::Interface> client);
+
+ void detach();
+
+ void request_plugins_if_necessary();
+
+ void register_callbacks();
+ bool gtk_main_iteration();
+
+ void show_about();
+ bool quit(Gtk::Window* dialog_parent);
+
+ void port_activity(Port* port);
+ void activity_port_destroyed(Port* port);
+ bool can_control(const client::PortModel* port) const;
+
+ bool signal() const { return _enable_signal; }
+ void enable_signals(bool b) { _enable_signal = b; }
+ bool disable_signals() {
+ bool old = _enable_signal;
+ _enable_signal = false;
+ return old;
+ }
+
+ void set_property(const URI& subject,
+ const URI& key,
+ const Atom& value,
+ Resource::Graph ctx = Resource::Graph::DEFAULT);
+
+ /** Set the tooltip for a widget from its RDF documentation. */
+ void set_tooltip(Gtk::Widget* widget, const LilvNode* node);
+
+ uint32_t sample_rate() const;
+
+ bool is_plugin() const { return _is_plugin; }
+ void set_is_plugin(bool b) { _is_plugin = b; }
+
+ ConnectWindow* connect_window() const { return _connect_window; }
+ MessagesWindow* messages_dialog() const { return _messages_window; }
+ GraphTreeWindow* graph_tree() const { return _graph_tree_window; }
+ Style* style() const { return _style; }
+ WindowFactory* window_factory() const { return _window_factory; }
+
+ ingen::Forge& forge() const { return _world.forge(); }
+ SPtr<ingen::Interface> interface() const { return _world.interface(); }
+ SPtr<ingen::Interface> client() const { return _client; }
+ SPtr<client::ClientStore> store() const { return _store; }
+ SPtr<ThreadedLoader> loader() const { return _loader; }
+
+ SPtr<client::SigClientInterface> sig_client();
+
+ SPtr<Serialiser> serialiser();
+
+ static SPtr<App> create(ingen::World& world);
+
+ void run();
+
+ std::string status_text() const;
+
+ sigc::signal<void, const std::string&> signal_status_text_changed;
+
+ inline ingen::World& world() const { return _world; }
+ inline ingen::URIs& uris() const { return _world.uris(); }
+ inline ingen::Log& log() const { return _world.log(); }
+
+protected:
+ explicit App(ingen::World& world);
+
+ void message(const ingen::Message& msg);
+
+ bool animate();
+ void response(int32_t id, ingen::Status status, const std::string& subject);
+
+ void put(const URI& uri,
+ const Properties& properties,
+ Resource::Graph ctx);
+
+ void property_change(const URI& subject,
+ const URI& key,
+ const Atom& value,
+ Resource::Graph ctx = Resource::Graph::DEFAULT);
+
+ static Gtk::Main* _main;
+
+ SPtr<ingen::Interface> _client;
+ SPtr<client::ClientStore> _store;
+ SPtr<ThreadedLoader> _loader;
+ SPtr<StreamWriter> _dumper;
+
+ Style* _style;
+
+ ConnectWindow* _connect_window;
+ MessagesWindow* _messages_window;
+ GraphTreeWindow* _graph_tree_window;
+ Gtk::AboutDialog* _about_dialog;
+ WindowFactory* _window_factory;
+
+ ingen::World& _world;
+
+ int32_t _sample_rate;
+ int32_t _block_length;
+ int32_t _n_threads;
+ float _mean_run_load;
+ float _min_run_load;
+ float _max_run_load;
+ std::string _status_text;
+
+ typedef std::unordered_map<Port*, bool> ActivityPorts;
+ ActivityPorts _activity_ports;
+
+ bool _enable_signal;
+ bool _requested_plugins;
+ bool _is_plugin;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_APP_HPP
diff --git a/src/gui/Arc.cpp b/src/gui/Arc.cpp
new file mode 100644
index 00000000..d811bd22
--- /dev/null
+++ b/src/gui/Arc.cpp
@@ -0,0 +1,45 @@
+/*
+ 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 "Arc.hpp"
+
+#include "ingen/client/ArcModel.hpp"
+#include "ingen/client/BlockModel.hpp"
+
+#define NS_INTERNALS "http://drobilla.net/ns/ingen-internals#"
+
+namespace ingen {
+namespace gui {
+
+Arc::Arc(Ganv::Canvas& canvas,
+ SPtr<const client::ArcModel> model,
+ Ganv::Node* src,
+ Ganv::Node* dst)
+ : Ganv::Edge(canvas, src, dst)
+ , _arc_model(model)
+{
+ SPtr<const client::ObjectModel> tparent = model->tail()->parent();
+ SPtr<const client::BlockModel> tparent_block;
+ if ((tparent_block = dynamic_ptr_cast<const client::BlockModel>(tparent))) {
+ if (tparent_block->plugin_uri() == NS_INTERNALS "BlockDelay") {
+ g_object_set(_gobj, "dash-length", 4.0, nullptr);
+ set_constraining(false);
+ }
+ }
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/Arc.hpp b/src/gui/Arc.hpp
new file mode 100644
index 00000000..453985fc
--- /dev/null
+++ b/src/gui/Arc.hpp
@@ -0,0 +1,52 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_ARC_HPP
+#define INGEN_GUI_ARC_HPP
+
+#include "ganv/Edge.hpp"
+#include "ingen/types.hpp"
+
+#include <cassert>
+
+namespace ingen {
+
+namespace client { class ArcModel; }
+
+namespace gui {
+
+/** An Arc (directed edge) in a Graph.
+ *
+ * \ingroup GUI
+ */
+class Arc : public Ganv::Edge
+{
+public:
+ Arc(Ganv::Canvas& canvas,
+ SPtr<const client::ArcModel> model,
+ Ganv::Node* src,
+ Ganv::Node* dst);
+
+ SPtr<const client::ArcModel> model() const { return _arc_model; }
+
+private:
+ SPtr<const client::ArcModel> _arc_model;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_ARC_HPP
diff --git a/src/gui/BreadCrumbs.cpp b/src/gui/BreadCrumbs.cpp
new file mode 100644
index 00000000..33b2c4b3
--- /dev/null
+++ b/src/gui/BreadCrumbs.cpp
@@ -0,0 +1,229 @@
+/*
+ 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 "BreadCrumbs.hpp"
+
+#include "App.hpp"
+
+#include "ingen/client/SigClientInterface.hpp"
+
+#include <boost/variant/get.hpp>
+
+#include <string>
+
+namespace ingen {
+namespace gui {
+
+using std::string;
+
+BreadCrumbs::BreadCrumbs(App& app)
+ : Gtk::HBox()
+ , _active_path("/")
+ , _full_path("/")
+ , _enable_signal(true)
+{
+ app.sig_client()->signal_message().connect(
+ sigc::mem_fun(this, &BreadCrumbs::message));
+
+ set_can_focus(false);
+}
+
+SPtr<GraphView>
+BreadCrumbs::view(const Raul::Path& path)
+{
+ for (const auto& b : _breadcrumbs) {
+ if (b->path() == path) {
+ return b->view();
+ }
+ }
+
+ return SPtr<GraphView>();
+}
+
+/** Sets up the crumbs to display `path`.
+ *
+ * If `path` is already part of the shown path, it will be selected and the
+ * children preserved.
+ */
+void
+BreadCrumbs::build(Raul::Path path, SPtr<GraphView> view)
+{
+ bool old_enable_signal = _enable_signal;
+ _enable_signal = false;
+
+ if (!_breadcrumbs.empty() && (path.is_parent_of(_full_path) || path == _full_path)) {
+ // Moving to a path we already contain, just switch the active button
+ for (const auto& b : _breadcrumbs) {
+ if (b->path() == path) {
+ b->set_active(true);
+ if (!b->view()) {
+ b->set_view(view);
+ }
+
+ // views are expensive, having two around for the same graph is a bug
+ assert(b->view() == view);
+
+ } else {
+ b->set_active(false);
+ }
+ }
+
+ _active_path = path;
+ _enable_signal = old_enable_signal;
+
+ } else if (!_breadcrumbs.empty() && path.is_child_of(_full_path)) {
+ // Moving to a child of the full path, just append crumbs (preserve view cache)
+
+ string suffix = path.substr(_full_path.length());
+ while (suffix.length() > 0) {
+ if (suffix[0] == '/') {
+ suffix = suffix.substr(1);
+ }
+ const string name = suffix.substr(0, suffix.find("/"));
+ _full_path = _full_path.child(Raul::Symbol(name));
+ BreadCrumb* but = create_crumb(_full_path, view);
+ pack_start(*but, false, false, 1);
+ _breadcrumbs.push_back(but);
+ but->show();
+ if (suffix.find("/") == string::npos) {
+ break;
+ } else {
+ suffix = suffix.substr(suffix.find("/")+1);
+ }
+ }
+
+ for (const auto& b : _breadcrumbs) {
+ b->set_active(false);
+ }
+ _breadcrumbs.back()->set_active(true);
+
+ } else {
+ // Rebuild from scratch
+ // Getting here is bad unless absolutely necessary, since the GraphView cache is lost
+
+ _full_path = path;
+ _active_path = path;
+
+ // Empty existing breadcrumbs
+ for (const auto& b : _breadcrumbs) {
+ remove(*b);
+ }
+ _breadcrumbs.clear();
+
+ // Add root
+ BreadCrumb* root_but = create_crumb(Raul::Path("/"), view);
+ pack_start(*root_but, false, false, 1);
+ _breadcrumbs.push_front(root_but);
+ root_but->set_active(root_but->path() == _active_path);
+
+ Raul::Path working_path("/");
+ string suffix = path.substr(1);
+ while (suffix.length() > 0) {
+ if (suffix[0] == '/') {
+ suffix = suffix.substr(1);
+ }
+ const string name = suffix.substr(0, suffix.find("/"));
+ working_path = working_path.child(Raul::Symbol(name));
+ BreadCrumb* but = create_crumb(working_path, view);
+ pack_start(*but, false, false, 1);
+ _breadcrumbs.push_back(but);
+ but->set_active(working_path == _active_path);
+ but->show();
+ if (suffix.find("/") == string::npos) {
+ break;
+ } else {
+ suffix = suffix.substr(suffix.find("/")+1);
+ }
+ }
+ }
+
+ _enable_signal = old_enable_signal;
+}
+
+/** Create a new crumb, assigning it a reference to `view` if their paths
+ * match, otherwise ignoring `view`.
+ */
+BreadCrumbs::BreadCrumb*
+BreadCrumbs::create_crumb(const Raul::Path& path,
+ SPtr<GraphView> view)
+{
+ BreadCrumb* but = manage(
+ new BreadCrumb(path,
+ ((view && path == view->graph()->path())
+ ? view : SPtr<GraphView>())));
+
+ but->signal_toggled().connect(
+ sigc::bind(sigc::mem_fun(this, &BreadCrumbs::breadcrumb_clicked),
+ but));
+
+ return but;
+}
+
+void
+BreadCrumbs::breadcrumb_clicked(BreadCrumb* crumb)
+{
+ if (_enable_signal) {
+ _enable_signal = false;
+
+ if (!crumb->get_active()) {
+ // Tried to turn off the current active button, bad user, no cookie
+ crumb->set_active(true);
+ } else {
+ signal_graph_selected.emit(crumb->path(), crumb->view());
+ if (crumb->path() != _active_path) {
+ crumb->set_active(false);
+ }
+ }
+ _enable_signal = true;
+ }
+}
+
+void
+BreadCrumbs::message(const Message& msg)
+{
+ if (const Del* const del = boost::get<Del>(&msg)) {
+ object_destroyed(del->uri);
+ }
+}
+
+void
+BreadCrumbs::object_destroyed(const URI& uri)
+{
+ for (auto i = _breadcrumbs.begin(); i != _breadcrumbs.end(); ++i) {
+ if ((*i)->path() == uri.c_str()) {
+ // Remove all crumbs after the removed one (inclusive)
+ for (auto j = i; j != _breadcrumbs.end(); ) {
+ BreadCrumb* bc = *j;
+ j = _breadcrumbs.erase(j);
+ remove(*bc);
+ }
+ break;
+ }
+ }
+}
+
+void
+BreadCrumbs::object_moved(const Raul::Path& old_path, const Raul::Path& new_path)
+{
+ for (const auto& b : _breadcrumbs) {
+ if (b->path() == old_path) {
+ b->set_path(new_path);
+ }
+ }
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/BreadCrumbs.hpp b/src/gui/BreadCrumbs.hpp
new file mode 100644
index 00000000..63872a78
--- /dev/null
+++ b/src/gui/BreadCrumbs.hpp
@@ -0,0 +1,121 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_BREADCRUMBS_HPP
+#define INGEN_GUI_BREADCRUMBS_HPP
+
+#include "GraphView.hpp"
+
+#include "ingen/Message.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/types.hpp"
+#include "raul/Path.hpp"
+
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/togglebutton.h>
+
+#include <cassert>
+#include <list>
+#include <string>
+
+namespace ingen {
+namespace gui {
+
+/** Collection of breadcrumb buttons forming a path.
+ * This doubles as a cache for GraphViews.
+ *
+ * \ingroup GUI
+ */
+class BreadCrumbs : public Gtk::HBox
+{
+public:
+ explicit BreadCrumbs(App& app);
+
+ SPtr<GraphView> view(const Raul::Path& path);
+
+ void build(Raul::Path path, SPtr<GraphView> view);
+
+ sigc::signal<void, const Raul::Path&, SPtr<GraphView> > signal_graph_selected;
+
+private:
+ /** Breadcrumb button.
+ *
+ * Each Breadcrumb stores a reference to a GraphView for quick switching.
+ * So, the amount of allocated GraphViews at a given time is equal to the
+ * number of visible breadcrumbs (which is the perfect cache for GUI
+ * responsiveness balanced with mem consumption).
+ *
+ * \ingroup GUI
+ */
+ class BreadCrumb : public Gtk::ToggleButton
+ {
+ public:
+ BreadCrumb(const Raul::Path& path, SPtr<GraphView> view = SPtr<GraphView>())
+ : _path(path)
+ , _view(view)
+ {
+ assert(!view || view->graph()->path() == path);
+ set_border_width(0);
+ set_path(path);
+ set_can_focus(false);
+ show_all();
+ }
+
+ void set_view(SPtr<GraphView> view) {
+ assert(!view || view->graph()->path() == _path);
+ _view = view;
+ }
+
+ const Raul::Path& path() const { return _path; }
+ SPtr<GraphView> view() const { return _view; }
+
+ void set_path(const Raul::Path& path) {
+ remove();
+ const char* text = (path.is_root()) ? "/" : path.symbol();
+ Gtk::Label* lab = manage(new Gtk::Label(text));
+ lab->set_padding(0, 0);
+ lab->show();
+ add(*lab);
+
+ if (_view && _view->graph()->path() != path)
+ _view.reset();
+ }
+
+ private:
+ Raul::Path _path;
+ SPtr<GraphView> _view;
+ };
+
+ BreadCrumb* create_crumb(const Raul::Path& path,
+ SPtr<GraphView> view = SPtr<GraphView>());
+
+ void breadcrumb_clicked(BreadCrumb* crumb);
+
+ void message(const Message& msg);
+ void object_destroyed(const URI& uri);
+ void object_moved(const Raul::Path& old_path, const Raul::Path& new_path);
+
+ Raul::Path _active_path;
+ Raul::Path _full_path;
+ bool _enable_signal;
+ std::list<BreadCrumb*> _breadcrumbs;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_BREADCRUMBS_HPP
diff --git a/src/gui/ConnectWindow.cpp b/src/gui/ConnectWindow.cpp
new file mode 100644
index 00000000..232b5e20
--- /dev/null
+++ b/src/gui/ConnectWindow.cpp
@@ -0,0 +1,573 @@
+/*
+ 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 "ConnectWindow.hpp"
+
+#include "App.hpp"
+#include "WindowFactory.hpp"
+
+#include "ingen/Configuration.hpp"
+#include "ingen/EngineBase.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/Module.hpp"
+#include "ingen/QueuedInterface.hpp"
+#include "ingen/World.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/client/SigClientInterface.hpp"
+#include "ingen/client/SocketClient.hpp"
+#include "ingen_config.h"
+#include "raul/Process.hpp"
+
+#include <boost/variant/get.hpp>
+#include <glib.h>
+#include <gtkmm/stock.h>
+
+#include <limits>
+#include <string>
+#include <utility>
+
+using namespace ingen::client;
+
+namespace ingen {
+namespace gui {
+
+ConnectWindow::ConnectWindow(BaseObjectType* cobject,
+ Glib::RefPtr<Gtk::Builder> xml)
+ : Dialog(cobject)
+ , _xml(std::move(xml))
+ , _icon(nullptr)
+ , _progress_bar(nullptr)
+ , _progress_label(nullptr)
+ , _url_entry(nullptr)
+ , _server_radio(nullptr)
+ , _port_spinbutton(nullptr)
+ , _launch_radio(nullptr)
+ , _internal_radio(nullptr)
+ , _activate_button(nullptr)
+ , _deactivate_button(nullptr)
+ , _disconnect_button(nullptr)
+ , _connect_button(nullptr)
+ , _quit_button(nullptr)
+ , _mode(Mode::CONNECT_REMOTE)
+ , _connect_uri("unix:///tmp/ingen.sock")
+ , _ping_id(-1)
+ , _attached(false)
+ , _finished_connecting(false)
+ , _widgets_loaded(false)
+ , _connect_stage(0)
+ , _quit_flag(false)
+{
+}
+
+void
+ConnectWindow::message(const Message& msg)
+{
+ if (const Response* const r = boost::get<Response>(&msg)) {
+ ingen_response(r->id, r->status, r->subject);
+ } else if (const Error* const e = boost::get<Error>(&msg)) {
+ error(e->message);
+ }
+}
+
+void
+ConnectWindow::error(const std::string& msg)
+{
+ if (!is_visible()) {
+ present();
+ set_connecting_widget_states();
+ }
+
+ if (_progress_label) {
+ _progress_label->set_text(msg);
+ }
+
+ if (_app) {
+ _app->world().log().error(msg + "\n");
+ }
+}
+
+void
+ConnectWindow::start(App& app, ingen::World& world)
+{
+ _app = &app;
+
+ if (world.engine()) {
+ _mode = Mode::INTERNAL;
+ }
+
+ set_connected_to(world.interface());
+ connect(bool(world.interface()));
+}
+
+void
+ConnectWindow::ingen_response(int32_t id,
+ Status status,
+ const std::string& subject)
+{
+ if (id == _ping_id) {
+ if (status != Status::SUCCESS) {
+ error("Failed to get root patch");
+ } else {
+ _attached = true;
+ }
+ }
+}
+
+void
+ConnectWindow::set_connected_to(SPtr<ingen::Interface> engine)
+{
+ _app->world().set_interface(engine);
+
+ if (!_widgets_loaded) {
+ return;
+ }
+
+ if (engine) {
+ _icon->set(Gtk::Stock::CONNECT, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ _progress_bar->set_fraction(1.0);
+ _progress_label->set_text("Connected to engine");
+ _url_entry->set_sensitive(false);
+ _url_entry->set_text(engine->uri().string());
+ _connect_button->set_sensitive(false);
+ _disconnect_button->set_label("gtk-disconnect");
+ _disconnect_button->set_sensitive(true);
+ _port_spinbutton->set_sensitive(false);
+ _launch_radio->set_sensitive(false);
+ _internal_radio->set_sensitive(false);
+ _activate_button->set_sensitive(true);
+ _deactivate_button->set_sensitive(true);
+
+ } else {
+ _icon->set(Gtk::Stock::DISCONNECT, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ _progress_bar->set_fraction(0.0);
+ _connect_button->set_sensitive(true);
+ _disconnect_button->set_sensitive(false);
+ _internal_radio->set_sensitive(true);
+ _server_radio->set_sensitive(true);
+ _launch_radio->set_sensitive(true);
+ _activate_button->set_sensitive(false);
+ _deactivate_button->set_sensitive(false);
+
+ if (_mode == Mode::CONNECT_REMOTE) {
+ _url_entry->set_sensitive(true);
+ } else if (_mode == Mode::LAUNCH_REMOTE) {
+ _port_spinbutton->set_sensitive(true);
+ }
+
+ _progress_label->set_text(std::string("Disconnected"));
+ }
+}
+
+void
+ConnectWindow::set_connecting_widget_states()
+{
+ if (!_widgets_loaded) {
+ return;
+ }
+
+ _connect_button->set_sensitive(false);
+ _disconnect_button->set_label("gtk-cancel");
+ _disconnect_button->set_sensitive(true);
+ _server_radio->set_sensitive(false);
+ _launch_radio->set_sensitive(false);
+ _internal_radio->set_sensitive(false);
+ _url_entry->set_sensitive(false);
+ _port_spinbutton->set_sensitive(false);
+}
+
+bool
+ConnectWindow::connect_remote(const URI& uri)
+{
+ ingen::World& world = _app->world();
+
+ SPtr<SigClientInterface> sci(new SigClientInterface());
+ SPtr<QueuedInterface> qi(new QueuedInterface(sci));
+
+ SPtr<ingen::Interface> iface(world.new_interface(uri, qi));
+ if (iface) {
+ world.set_interface(iface);
+ _app->attach(qi);
+ _app->register_callbacks();
+ return true;
+ }
+
+ return false;
+}
+
+/** Set up initial connect stage and launch connect callback. */
+void
+ConnectWindow::connect(bool existing)
+{
+ if (_app->client()) {
+ error("Already connected");
+ return;
+ } else if (_attached) {
+ _attached = false;
+ }
+
+ set_connecting_widget_states();
+ _connect_stage = 0;
+
+ ingen::World& world = _app->world();
+
+ if (_mode == Mode::CONNECT_REMOTE) {
+ std::string uri_str = world.conf().option("connect").ptr<char>();
+ if (existing) {
+ uri_str = world.interface()->uri();
+ _connect_stage = 1;
+ SPtr<client::SocketClient> client = dynamic_ptr_cast<client::SocketClient>(
+ world.interface());
+ if (client) {
+ _app->attach(client->respondee());
+ _app->register_callbacks();
+ } else {
+ error("Connected with invalid client interface type");
+ return;
+ }
+ } else if (_widgets_loaded) {
+ uri_str = _url_entry->get_text();
+ }
+
+ if (!URI::is_valid(uri_str)) {
+ error(fmt("Invalid socket URI %1%", uri_str));
+ return;
+ }
+
+ _connect_uri = URI(uri_str);
+
+ } else if (_mode == Mode::LAUNCH_REMOTE) {
+ const std::string port = std::to_string(_port_spinbutton->get_value_as_int());
+ const char* cmd[] = { "ingen", "-e", "-E", port.c_str(), nullptr };
+
+ if (!Raul::Process::launch(cmd)) {
+ error("Failed to launch engine process");
+ return;
+ }
+
+ _connect_uri = URI(std::string("tcp://localhost:") + port);
+
+ } else if (_mode == Mode::INTERNAL) {
+ if (!world.engine()) {
+ if (!world.load_module("server")) {
+ error("Failed to load server module");
+ return;
+ } else if (!world.load_module("jack")) {
+ error("Failed to load jack module");
+ return;
+ } else if (!world.engine()->activate()) {
+ error("Failed to activate engine");
+ return;
+ }
+ }
+ }
+
+ set_connecting_widget_states();
+ if (_widgets_loaded) {
+ _progress_label->set_text("Connecting...");
+ }
+ Glib::signal_timeout().connect(
+ sigc::mem_fun(this, &ConnectWindow::gtk_callback), 33);
+}
+
+void
+ConnectWindow::disconnect()
+{
+ _connect_stage = -1;
+ _attached = false;
+
+ _app->detach();
+ set_connected_to(SPtr<ingen::Interface>());
+
+ if (!_widgets_loaded) {
+ return;
+ }
+
+ _activate_button->set_sensitive(false);
+ _deactivate_button->set_sensitive(false);
+
+ _progress_bar->set_fraction(0.0);
+ _connect_button->set_sensitive(true);
+ _disconnect_button->set_sensitive(false);
+}
+
+void
+ConnectWindow::activate()
+{
+ if (!_app->interface()) {
+ return;
+ }
+
+ _app->interface()->set_property(URI("ingen:/driver"),
+ _app->uris().ingen_enabled,
+ _app->forge().make(true));
+}
+
+void
+ConnectWindow::deactivate()
+{
+ if (!_app->interface()) {
+ return;
+ }
+
+ _app->interface()->set_property(URI("ingen:/driver"),
+ _app->uris().ingen_enabled,
+ _app->forge().make(false));
+}
+
+void
+ConnectWindow::on_show()
+{
+ if (!_widgets_loaded) {
+ load_widgets();
+ }
+
+ if (_attached) {
+ set_connected_to(_app->interface());
+ }
+
+ Gtk::Dialog::on_show();
+}
+
+void
+ConnectWindow::load_widgets()
+{
+ _xml->get_widget("connect_icon", _icon);
+ _xml->get_widget("connect_progress_bar", _progress_bar);
+ _xml->get_widget("connect_progress_label", _progress_label);
+ _xml->get_widget("connect_server_radiobutton", _server_radio);
+ _xml->get_widget("connect_url_entry", _url_entry);
+ _xml->get_widget("connect_launch_radiobutton", _launch_radio);
+ _xml->get_widget("connect_port_spinbutton", _port_spinbutton);
+ _xml->get_widget("connect_internal_radiobutton", _internal_radio);
+ _xml->get_widget("connect_activate_button", _activate_button);
+ _xml->get_widget("connect_deactivate_button", _deactivate_button);
+ _xml->get_widget("connect_disconnect_button", _disconnect_button);
+ _xml->get_widget("connect_connect_button", _connect_button);
+ _xml->get_widget("connect_quit_button", _quit_button);
+
+ _server_radio->signal_toggled().connect(
+ sigc::mem_fun(this, &ConnectWindow::server_toggled));
+ _launch_radio->signal_toggled().connect(
+ sigc::mem_fun(this, &ConnectWindow::launch_toggled));
+ _internal_radio->signal_clicked().connect(
+ sigc::mem_fun(this, &ConnectWindow::internal_toggled));
+ _activate_button->signal_clicked().connect(
+ sigc::mem_fun(this, &ConnectWindow::activate));
+ _deactivate_button->signal_clicked().connect(
+ sigc::mem_fun(this, &ConnectWindow::deactivate));
+ _disconnect_button->signal_clicked().connect(
+ sigc::mem_fun(this, &ConnectWindow::disconnect));
+ _connect_button->signal_clicked().connect(
+ sigc::bind(sigc::mem_fun(this, &ConnectWindow::connect), false));
+ _quit_button->signal_clicked().connect(
+ sigc::mem_fun(this, &ConnectWindow::quit_clicked));
+
+ _url_entry->set_text(_app->world().conf().option("connect").ptr<char>());
+ if (URI::is_valid(_url_entry->get_text())) {
+ _connect_uri = URI(_url_entry->get_text());
+ }
+
+ _port_spinbutton->set_range(1, std::numeric_limits<uint16_t>::max());
+ _port_spinbutton->set_increments(1, 100);
+ _port_spinbutton->set_value(
+ _app->world().conf().option("engine-port").get<int32_t>());
+
+ _progress_bar->set_pulse_step(0.01);
+ _widgets_loaded = true;
+
+ server_toggled();
+}
+
+void
+ConnectWindow::on_hide()
+{
+ Gtk::Dialog::on_hide();
+ if (_app->window_factory()->num_open_graph_windows() == 0) {
+ quit();
+ }
+}
+
+void
+ConnectWindow::quit_clicked()
+{
+ if (_app->quit(this)) {
+ _quit_flag = true;
+ }
+}
+
+void
+ConnectWindow::server_toggled()
+{
+ _url_entry->set_sensitive(true);
+ _port_spinbutton->set_sensitive(false);
+ _mode = Mode::CONNECT_REMOTE;
+}
+
+void
+ConnectWindow::launch_toggled()
+{
+ _url_entry->set_sensitive(false);
+ _port_spinbutton->set_sensitive(true);
+ _mode = Mode::LAUNCH_REMOTE;
+}
+
+void
+ConnectWindow::internal_toggled()
+{
+ _url_entry->set_sensitive(false);
+ _port_spinbutton->set_sensitive(false);
+ _mode = Mode::INTERNAL;
+}
+
+void
+ConnectWindow::next_stage()
+{
+ static const char* labels[] = {
+ "Connecting...",
+ "Pinging engine...",
+ "Attaching to engine...",
+ "Requesting root graph...",
+ "Waiting for root graph...",
+ "Connected"
+ };
+
+
+ ++_connect_stage;
+ if (_widgets_loaded) {
+ _progress_label->set_text(labels[_connect_stage]);
+ }
+}
+
+bool
+ConnectWindow::gtk_callback()
+{
+ /* If I call this a "state machine" it's not ugly code any more */
+
+ if (_quit_flag) {
+ return false; // deregister this callback
+ }
+
+ // Timing stuff for repeated attach attempts
+ timeval now;
+ gettimeofday(&now, nullptr);
+ static const timeval start = now;
+ static timeval last = now;
+ static unsigned attempts = 0;
+
+ // Show if attempted connection goes on for a noticeable amount of time
+ if (!is_visible()) {
+ const float ms_since_start = (now.tv_sec - start.tv_sec) * 1000.0f +
+ (now.tv_usec - start.tv_usec) * 0.001f;
+ if (ms_since_start > 500) {
+ present();
+ set_connecting_widget_states();
+ }
+ }
+
+ if (_connect_stage == 0) {
+ const float ms_since_last = (now.tv_sec - last.tv_sec) * 1000.0f +
+ (now.tv_usec - last.tv_usec) * 0.001f;
+ if (ms_since_last >= 250) {
+ last = now;
+ if (_mode == Mode::INTERNAL) {
+ SPtr<SigClientInterface> client(new SigClientInterface());
+ _app->world().interface()->set_respondee(client);
+ _app->attach(client);
+ _app->register_callbacks();
+ next_stage();
+ } else if (connect_remote(_connect_uri)) {
+ next_stage();
+ }
+ }
+ } else if (_connect_stage == 1) {
+ _attached = false;
+ _app->sig_client()->signal_message().connect(
+ sigc::mem_fun(this, &ConnectWindow::message));
+
+ _ping_id = g_random_int_range(1, std::numeric_limits<int32_t>::max());
+ _app->interface()->set_response_id(_ping_id);
+ _app->interface()->get(URI("ingen:/engine"));
+ last = now;
+ attempts = 0;
+ next_stage();
+
+ } else if (_connect_stage == 2) {
+ if (_attached) {
+ next_stage();
+ } else {
+ const float ms_since_last = (now.tv_sec - last.tv_sec) * 1000.0f +
+ (now.tv_usec - last.tv_usec) * 0.001f;
+ if (attempts > 10) {
+ error("Failed to ping engine");
+ _connect_stage = -1;
+ } else if (ms_since_last > 1000) {
+ _app->interface()->set_response_id(_ping_id);
+ _app->interface()->get(URI("ingen:/engine"));
+ last = now;
+ ++attempts;
+ }
+ }
+ } else if (_connect_stage == 3) {
+ _app->interface()->get(URI(main_uri().string() + "/"));
+ next_stage();
+ } else if (_connect_stage == 4) {
+ if (_app->store()->size() > 0) {
+ SPtr<const GraphModel> root = dynamic_ptr_cast<const GraphModel>(
+ _app->store()->object(Raul::Path("/")));
+ if (root) {
+ set_connected_to(_app->interface());
+ _app->window_factory()->present_graph(root);
+ next_stage();
+ }
+ }
+ } else if (_connect_stage == 5) {
+ hide();
+ _connect_stage = 0; // set ourselves up for next time (if there is one)
+ _finished_connecting = true;
+ _app->interface()->set_response_id(1);
+ return false; // deregister this callback
+ }
+
+ if (_widgets_loaded) {
+ _progress_bar->pulse();
+ }
+
+ if (_connect_stage == -1) { // we were cancelled
+ if (_widgets_loaded) {
+ _icon->set(Gtk::Stock::DISCONNECT, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ _progress_bar->set_fraction(0.0);
+ _connect_button->set_sensitive(true);
+ _disconnect_button->set_sensitive(false);
+ _disconnect_button->set_label("gtk-disconnect");
+ _progress_label->set_text(std::string("Disconnected"));
+ }
+ return false;
+ } else {
+ return true;
+ }
+}
+
+void
+ConnectWindow::quit()
+{
+ _quit_flag = true;
+ Gtk::Main::quit();
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/ConnectWindow.hpp b/src/gui/ConnectWindow.hpp
new file mode 100644
index 00000000..882e0142
--- /dev/null
+++ b/src/gui/ConnectWindow.hpp
@@ -0,0 +1,124 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_CONNECTWINDOW_HPP
+#define INGEN_GUI_CONNECTWINDOW_HPP
+
+#include "Window.hpp"
+
+#include "ingen/Message.hpp"
+#include "ingen/types.hpp"
+#include "lilv/lilv.h"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/progressbar.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/spinbutton.h>
+
+#include <cstdint>
+#include <string>
+
+namespace ingen {
+
+class Interface;
+class World;
+
+namespace gui {
+
+class App;
+
+/** The initially visible "Connect to engine" window.
+ *
+ * This handles actually connecting to the engine and making sure everything
+ * is ready before really launching the app.
+ *
+ * \ingroup GUI
+ */
+class ConnectWindow : public Dialog
+{
+public:
+ ConnectWindow(BaseObjectType* cobject,
+ Glib::RefPtr<Gtk::Builder> xml);
+
+ void set_connected_to(SPtr<ingen::Interface> engine);
+ void start(App& app, ingen::World& world);
+
+ bool attached() const { return _finished_connecting; }
+ bool quit_flag() const { return _quit_flag; }
+
+private:
+ enum class Mode { CONNECT_REMOTE, LAUNCH_REMOTE, INTERNAL };
+
+ void message(const Message& msg);
+
+ void error(const std::string& msg);
+
+ void ingen_response(int32_t id, Status status, const std::string& subject);
+
+ void server_toggled();
+ void launch_toggled();
+ void internal_toggled();
+
+ void disconnect();
+ void next_stage();
+ bool connect_remote(const URI& uri);
+ void connect(bool existing);
+ void activate();
+ void deactivate();
+ void quit_clicked();
+ void on_show() override;
+ void on_hide() override;
+
+ void load_widgets();
+ void set_connecting_widget_states();
+
+ bool gtk_callback();
+ void quit();
+
+ const Glib::RefPtr<Gtk::Builder> _xml;
+
+ Gtk::Image* _icon;
+ Gtk::ProgressBar* _progress_bar;
+ Gtk::Label* _progress_label;
+ Gtk::Entry* _url_entry;
+ Gtk::RadioButton* _server_radio;
+ Gtk::SpinButton* _port_spinbutton;
+ Gtk::RadioButton* _launch_radio;
+ Gtk::RadioButton* _internal_radio;
+ Gtk::Button* _activate_button;
+ Gtk::Button* _deactivate_button;
+ Gtk::Button* _disconnect_button;
+ Gtk::Button* _connect_button;
+ Gtk::Button* _quit_button;
+
+ Mode _mode;
+ URI _connect_uri;
+ int32_t _ping_id;
+ bool _attached;
+ bool _finished_connecting;
+ bool _widgets_loaded;
+ int _connect_stage;
+ bool _quit_flag;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_CONNECTWINDOW_HPP
diff --git a/src/gui/GraphBox.cpp b/src/gui/GraphBox.cpp
new file mode 100644
index 00000000..d248d0a7
--- /dev/null
+++ b/src/gui/GraphBox.cpp
@@ -0,0 +1,917 @@
+/*
+ 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 "App.hpp"
+#include "BreadCrumbs.hpp"
+#include "ConnectWindow.hpp"
+#include "GraphCanvas.hpp"
+#include "GraphTreeWindow.hpp"
+#include "GraphView.hpp"
+#include "GraphWindow.hpp"
+#include "LoadGraphWindow.hpp"
+#include "LoadPluginWindow.hpp"
+#include "MessagesWindow.hpp"
+#include "NewSubgraphWindow.hpp"
+#include "Style.hpp"
+#include "ThreadedLoader.hpp"
+#include "WidgetFactory.hpp"
+#include "WindowFactory.hpp"
+#include "ingen_config.h"
+
+#include "ingen/Configuration.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/fmt.hpp"
+
+#include <boost/format.hpp>
+#include <glib/gstdio.h>
+#include <glibmm/fileutils.h>
+#include <gtkmm/stock.h>
+#ifdef HAVE_WEBKIT
+#include <webkit/webkit.h>
+#endif
+
+#include <cassert>
+#include <sstream>
+#include <string>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+static const int STATUS_CONTEXT_ENGINE = 0;
+static const int STATUS_CONTEXT_GRAPH = 1;
+static const int STATUS_CONTEXT_HOVER = 2;
+
+GraphBox::GraphBox(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Gtk::VBox(cobject)
+ , _app(nullptr)
+ , _window(nullptr)
+ , _breadcrumbs(nullptr)
+ , _has_shown_documentation(false)
+ , _enable_signal(true)
+{
+ property_visible() = false;
+
+ xml->get_widget("graph_win_alignment", _alignment);
+ xml->get_widget("graph_win_status_bar", _status_bar);
+ xml->get_widget("graph_import_menuitem", _menu_import);
+ 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);
+ xml->get_widget("graph_delete_menuitem", _menu_delete);
+ xml->get_widget("graph_select_all_menuitem", _menu_select_all);
+ xml->get_widget("graph_close_menuitem", _menu_close);
+ xml->get_widget("graph_quit_menuitem", _menu_quit);
+ xml->get_widget("graph_view_control_window_menuitem", _menu_view_control_window);
+ xml->get_widget("graph_view_engine_window_menuitem", _menu_view_engine_window);
+ xml->get_widget("graph_properties_menuitem", _menu_view_graph_properties);
+ xml->get_widget("graph_parent_menuitem", _menu_parent);
+ xml->get_widget("graph_refresh_menuitem", _menu_refresh);
+ xml->get_widget("graph_fullscreen_menuitem", _menu_fullscreen);
+ xml->get_widget("graph_animate_signals_menuitem", _menu_animate_signals);
+ xml->get_widget("graph_sprung_layout_menuitem", _menu_sprung_layout);
+ xml->get_widget("graph_human_names_menuitem", _menu_human_names);
+ xml->get_widget("graph_show_port_names_menuitem", _menu_show_port_names);
+ xml->get_widget("graph_zoom_in_menuitem", _menu_zoom_in);
+ xml->get_widget("graph_zoom_out_menuitem", _menu_zoom_out);
+ xml->get_widget("graph_zoom_normal_menuitem", _menu_zoom_normal);
+ xml->get_widget("graph_zoom_full_menuitem", _menu_zoom_full);
+ xml->get_widget("graph_increase_font_size_menuitem", _menu_increase_font_size);
+ xml->get_widget("graph_decrease_font_size_menuitem", _menu_decrease_font_size);
+ xml->get_widget("graph_normal_font_size_menuitem", _menu_normal_font_size);
+ xml->get_widget("graph_doc_pane_menuitem", _menu_show_doc_pane);
+ xml->get_widget("graph_status_bar_menuitem", _menu_show_status_bar);
+ xml->get_widget("graph_arrange_menuitem", _menu_arrange);
+ xml->get_widget("graph_view_messages_window_menuitem", _menu_view_messages_window);
+ xml->get_widget("graph_view_graph_tree_window_menuitem", _menu_view_graph_tree_window);
+ xml->get_widget("graph_help_about_menuitem", _menu_help_about);
+ xml->get_widget("graph_documentation_paned", _doc_paned);
+ xml->get_widget("graph_documentation_scrolledwindow", _doc_scrolledwindow);
+
+ _menu_view_control_window->property_sensitive() = false;
+ _menu_import->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_import));
+ _menu_save->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_save));
+ _menu_save_as->signal_activate().connect(
+ 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(
+ sigc::mem_fun(this, &GraphBox::event_paste));
+ _menu_delete->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_delete));
+ _menu_select_all->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_select_all));
+ _menu_close->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_close));
+ _menu_quit->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_quit));
+ _menu_parent->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_parent_activated));
+ _menu_refresh->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_refresh_activated));
+ _menu_fullscreen->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_fullscreen_toggled));
+ _menu_animate_signals->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_animate_signals_toggled));
+ _menu_sprung_layout->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_sprung_layout_toggled));
+ _menu_human_names->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_human_names_toggled));
+ _menu_show_doc_pane->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_doc_pane_toggled));
+ _menu_show_status_bar->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_status_bar_toggled));
+ _menu_show_port_names->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_port_names_toggled));
+ _menu_arrange->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_arrange));
+ _menu_zoom_in->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_zoom_in));
+ _menu_zoom_out->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_zoom_out));
+ _menu_zoom_normal->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_zoom_normal));
+ _menu_zoom_full->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_zoom_full));
+ _menu_increase_font_size->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_increase_font_size));
+ _menu_decrease_font_size->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_decrease_font_size));
+ _menu_normal_font_size->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_normal_font_size));
+ _menu_view_engine_window->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_show_engine));
+ _menu_view_graph_properties->signal_activate().connect(
+ sigc::mem_fun(this, &GraphBox::event_show_properties));
+
+ Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get();
+ clipboard->signal_owner_change().connect(
+ sigc::mem_fun(this, &GraphBox::event_clipboard_changed));
+
+#ifdef __APPLE__
+ _menu_paste->set_sensitive(true);
+#endif
+
+ _status_label = Gtk::manage(new Gtk::Label("STATUS"));
+ _status_bar->pack_start(*_status_label, false, true, 0);
+ _status_label->show();
+}
+
+GraphBox::~GraphBox()
+{
+ delete _breadcrumbs;
+}
+
+SPtr<GraphBox>
+GraphBox::create(App& app, SPtr<const GraphModel> graph)
+{
+ GraphBox* result = nullptr;
+ Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create("graph_win");
+ xml->get_widget_derived("graph_win_vbox", result);
+ result->init_box(app);
+ result->set_graph(graph, SPtr<GraphView>());
+
+ if (app.is_plugin()) {
+ result->_menu_close->set_sensitive(false);
+ result->_menu_quit->set_sensitive(false);
+ }
+
+ return SPtr<GraphBox>(result);
+}
+
+void
+GraphBox::init_box(App& app)
+{
+ _app = &app;
+
+ const URI engine_uri(_app->interface()->uri());
+ if (engine_uri == "ingen:/clients/event_writer") {
+ _status_bar->push("Running internal engine", STATUS_CONTEXT_ENGINE);
+ } else {
+ _status_bar->push(fmt("Connected to %1%", engine_uri),
+ STATUS_CONTEXT_ENGINE);
+ }
+
+ _menu_view_messages_window->signal_activate().connect(
+ sigc::mem_fun<void>(_app->messages_dialog(), &MessagesWindow::present));
+ _menu_view_graph_tree_window->signal_activate().connect(
+ sigc::mem_fun<void>(_app->graph_tree(), &GraphTreeWindow::present));
+
+ _menu_help_about->signal_activate().connect(
+ sigc::hide_return(sigc::mem_fun(_app, &App::show_about)));
+
+ _breadcrumbs = new BreadCrumbs(*_app);
+ _breadcrumbs->signal_graph_selected.connect(
+ sigc::mem_fun(this, &GraphBox::set_graph_from_path));
+
+ _status_label->set_markup(app.status_text());
+ app.signal_status_text_changed.connect(
+ sigc::mem_fun(*this, &GraphBox::set_status_text));
+}
+
+void
+GraphBox::set_status_text(const std::string& text)
+{
+ _status_label->set_markup(text);
+}
+
+void
+GraphBox::set_graph_from_path(const Raul::Path& path, SPtr<GraphView> view)
+{
+ if (view) {
+ assert(view->graph()->path() == path);
+ _app->window_factory()->present_graph(view->graph(), _window, view);
+ } else {
+ SPtr<const GraphModel> model = dynamic_ptr_cast<const GraphModel>(
+ _app->store()->object(path));
+ if (model) {
+ _app->window_factory()->present_graph(model, _window);
+ }
+ }
+}
+
+/** Sets the graph for this box and initializes everything.
+ *
+ * If `view` is null, a new view will be created.
+ */
+void
+GraphBox::set_graph(SPtr<const GraphModel> graph,
+ SPtr<GraphView> view)
+{
+ if (!graph || graph == _graph) {
+ return;
+ }
+
+ _enable_signal = false;
+
+ new_port_connection.disconnect();
+ removed_port_connection.disconnect();
+ edit_mode_connection.disconnect();
+ _entered_connection.disconnect();
+ _left_connection.disconnect();
+
+ _status_bar->pop(STATUS_CONTEXT_GRAPH);
+
+ _graph = graph;
+ _view = view;
+
+ if (!_view) {
+ _view = _breadcrumbs->view(graph->path());
+ }
+
+ if (!_view) {
+ _view = GraphView::create(*_app, graph);
+ }
+
+ assert(_view);
+
+ graph->signal_property().connect(
+ sigc::mem_fun(this, &GraphBox::property_changed));
+
+ if (!_view->canvas()->supports_sprung_layout()) {
+ _menu_sprung_layout->set_active(false);
+ _menu_sprung_layout->set_sensitive(false);
+ }
+
+ // Add view to our alignment
+ if (_view->get_parent()) {
+ _view->get_parent()->remove(*_view.get());
+ }
+
+ _alignment->remove();
+ _alignment->add(*_view.get());
+
+ if (_breadcrumbs->get_parent()) {
+ _breadcrumbs->get_parent()->remove(*_breadcrumbs);
+ }
+
+ _view->breadcrumb_container()->remove();
+ _view->breadcrumb_container()->add(*_breadcrumbs);
+ _view->breadcrumb_container()->show();
+
+ _breadcrumbs->build(graph->path(), _view);
+ _breadcrumbs->show();
+
+ _menu_view_control_window->property_sensitive() = false;
+
+ for (const auto& p : graph->ports()) {
+ if (_app->can_control(p.get())) {
+ _menu_view_control_window->property_sensitive() = true;
+ break;
+ }
+ }
+
+ _menu_parent->property_sensitive() = bool(graph->parent());
+
+ new_port_connection = graph->signal_new_port().connect(
+ sigc::mem_fun(this, &GraphBox::graph_port_added));
+ removed_port_connection = graph->signal_removed_port().connect(
+ sigc::mem_fun(this, &GraphBox::graph_port_removed));
+
+ show();
+ _alignment->show_all();
+
+ _menu_human_names->set_active(
+ _app->world().conf().option("human-names").get<int32_t>());
+ _menu_show_port_names->set_active(
+ _app->world().conf().option("port-labels").get<int32_t>());
+
+ _doc_paned->set_position(std::numeric_limits<int>::max());
+ _doc_scrolledwindow->hide();
+
+ _enable_signal = true;
+}
+
+void
+GraphBox::graph_port_added(SPtr<const PortModel> port)
+{
+ if (port->is_input() && _app->can_control(port.get())) {
+ _menu_view_control_window->property_sensitive() = true;
+ }
+}
+
+void
+GraphBox::graph_port_removed(SPtr<const PortModel> port)
+{
+ if (!(port->is_input() && _app->can_control(port.get()))) {
+ return;
+ }
+
+ for (const auto& p : _graph->ports()) {
+ if (p->is_input() && _app->can_control(p.get())) {
+ _menu_view_control_window->property_sensitive() = true;
+ return;
+ }
+ }
+
+ _menu_view_control_window->property_sensitive() = false;
+}
+
+void
+GraphBox::property_changed(const URI& predicate, const Atom& value)
+{
+ if (predicate == _app->uris().ingen_sprungLayout) {
+ if (value.type() == _app->uris().forge.Bool) {
+ _menu_sprung_layout->set_active(value.get<int32_t>());
+ }
+ }
+}
+
+void
+GraphBox::set_documentation(const std::string& doc, bool html)
+{
+ _doc_scrolledwindow->remove();
+ if (doc.empty()) {
+ _doc_scrolledwindow->hide();
+ return;
+ }
+#ifdef HAVE_WEBKIT
+ WebKitWebView* view = WEBKIT_WEB_VIEW(webkit_web_view_new());
+ webkit_web_view_load_html_string(view, doc.c_str(), "");
+ Gtk::Widget* widget = Gtk::manage(Glib::wrap(GTK_WIDGET(view)));
+ _doc_scrolledwindow->add(*widget);
+ widget->show();
+#else
+ Gtk::TextView* view = Gtk::manage(new Gtk::TextView());
+ view->get_buffer()->set_text(doc);
+ view->set_wrap_mode(Gtk::WRAP_WORD);
+ _doc_scrolledwindow->add(*view);
+ view->show();
+#endif
+}
+
+void
+GraphBox::show_status(const ObjectModel* model)
+{
+ std::stringstream msg;
+ msg << model->path();
+
+ const PortModel* port = nullptr;
+ const BlockModel* block = nullptr;
+
+ if ((port = dynamic_cast<const PortModel*>(model))) {
+ show_port_status(port, port->value());
+
+ } else if ((block = dynamic_cast<const BlockModel*>(model))) {
+ const PluginModel* plugin = dynamic_cast<const PluginModel*>(block->plugin());
+ if (plugin) {
+ msg << fmt(" (%1%)", plugin->human_name());
+ }
+ _status_bar->push(msg.str(), STATUS_CONTEXT_HOVER);
+ }
+}
+
+void
+GraphBox::show_port_status(const PortModel* port, const Atom& value)
+{
+ std::stringstream msg;
+ msg << port->path();
+
+ const BlockModel* parent = dynamic_cast<const BlockModel*>(port->parent().get());
+ if (parent) {
+ const PluginModel* plugin = dynamic_cast<const PluginModel*>(parent->plugin());
+ if (plugin) {
+ const std::string& human_name = plugin->port_human_name(port->index());
+ if (!human_name.empty()) {
+ msg << " (" << human_name << ")";
+ }
+ }
+ }
+
+ if (value.is_valid()) {
+ msg << " = " << _app->forge().str(value, true);
+ }
+
+ _status_bar->pop(STATUS_CONTEXT_HOVER);
+ _status_bar->push(msg.str(), STATUS_CONTEXT_HOVER);
+}
+
+void
+GraphBox::object_entered(const ObjectModel* model)
+{
+ show_status(model);
+}
+
+void
+GraphBox::object_left(const ObjectModel* model)
+{
+ _status_bar->pop(STATUS_CONTEXT_GRAPH);
+ _status_bar->pop(STATUS_CONTEXT_HOVER);
+}
+
+void
+GraphBox::event_show_engine()
+{
+ if (_graph) {
+ _app->connect_window()->show();
+ }
+}
+
+void
+GraphBox::event_clipboard_changed(GdkEventOwnerChange* ev)
+{
+ Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get();
+ _menu_paste->set_sensitive(clipboard->wait_is_text_available());
+}
+
+void
+GraphBox::event_show_properties()
+{
+ _app->window_factory()->present_properties(_graph);
+}
+
+void
+GraphBox::event_import()
+{
+ _app->window_factory()->present_load_graph(_graph);
+}
+
+void
+GraphBox::event_save()
+{
+ const Atom& document = _graph->get_property(_app->uris().ingen_file);
+ if (!document.is_valid() || document.type() != _app->uris().forge.URI) {
+ event_save_as();
+ } else {
+ save_graph(URI(document.ptr<char>()));
+ }
+}
+
+void
+GraphBox::error(const Glib::ustring& message,
+ const Glib::ustring& secondary_text)
+{
+ Gtk::MessageDialog dialog(
+ message, true, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
+ dialog.set_secondary_text(secondary_text);
+ if (_window) {
+ dialog.set_transient_for(*_window);
+ }
+ dialog.run();
+}
+
+bool
+GraphBox::confirm(const Glib::ustring& message,
+ const Glib::ustring& secondary_text)
+{
+ Gtk::MessageDialog dialog(
+ message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true);
+ dialog.set_secondary_text(secondary_text);
+ if (_window) {
+ dialog.set_transient_for(*_window);
+ }
+ return dialog.run() == Gtk::RESPONSE_YES;
+}
+
+void
+GraphBox::save_graph(const URI& uri)
+{
+ if (_app->interface()->uri().string().substr(0, 3) == "tcp") {
+ _status_bar->push(
+ fmt("Saved %1% to %2% on client", _graph->path(), uri),
+ STATUS_CONTEXT_GRAPH);
+ _app->loader()->save_graph(_graph, uri);
+ } else {
+ _status_bar->push(
+ fmt("Saved %1% to %2% on server", _graph->path(), uri),
+ STATUS_CONTEXT_GRAPH);
+ _app->interface()->copy(_graph->uri(), uri);
+ }
+}
+
+void
+GraphBox::event_save_as()
+{
+ const URIs& uris = _app->uris();
+ while (true) {
+ Gtk::FileChooserDialog dialog(
+ "Save Graph", Gtk::FILE_CHOOSER_ACTION_SAVE);
+ if (_window) {
+ dialog.set_transient_for(*_window);
+ }
+
+ dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
+ Gtk::Button* save_button = dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK);
+ save_button->property_has_default() = true;
+
+ Gtk::FileFilter filt;
+ filt.add_pattern("*.ingen");
+ filt.set_name("Ingen bundles");
+ dialog.set_filter(filt);
+
+ // Set current folder to most sensible default
+ const Atom& document = _graph->get_property(uris.ingen_file);
+ const Atom& dir = _app->world().conf().option("graph-directory");
+ if (document.type() == uris.forge.URI) {
+ dialog.set_uri(document.ptr<char>());
+ } else if (dir.is_valid()) {
+ dialog.set_current_folder(dir.ptr<char>());
+ }
+
+ if (dialog.run() != Gtk::RESPONSE_OK) {
+ break;
+ }
+
+ std::string filename = dialog.get_filename();
+ std::string basename = Glib::path_get_basename(filename);
+
+ if (basename.find('.') == std::string::npos) {
+ filename += ".ingen";
+ basename += ".ingen";
+ } else if (filename.substr(filename.length() - 4) == ".ttl") {
+ const Glib::ustring dir = Glib::path_get_dirname(filename);
+ if (dir.substr(dir.length() - 6) != ".ingen") {
+ error("<b>File does not appear to be in an Ingen bundle.");
+ }
+ } else if (filename.substr(filename.length() - 6) != ".ingen") {
+ error("<b>Ingen bundles must end in \".ingen\"</b>");
+ continue;
+ }
+
+ const std::string symbol(basename.substr(0, basename.find('.')));
+
+ if (!Raul::Symbol::is_valid(symbol)) {
+ error(
+ "<b>Ingen bundle names must be valid symbols.</b>",
+ "All characters must be _, a-z, A-Z, or 0-9, but the first may not be 0-9.");
+ continue;
+ }
+
+ //_graph->set_property(uris.lv2_symbol, Atom(symbol.c_str()));
+
+ bool confirmed = true;
+ if (Glib::file_test(filename, Glib::FILE_TEST_IS_DIR)) {
+ if (Glib::file_test(Glib::build_filename(filename, "manifest.ttl"),
+ Glib::FILE_TEST_EXISTS)) {
+ confirmed = confirm(fmt("<b>The bundle \"%1%\" already exists."
+ " Replace it?</b>",
+ basename));
+ } else {
+ confirmed = confirm(
+ fmt("<b>A directory named \"%1%\" already exists,"
+ "but is not an Ingen bundle. "
+ "Save into it anyway?</b>", basename),
+ "This will create at least 2 .ttl files in this directory,"
+ "and possibly several more files and/or directories, recursively. "
+ "Existing files will be overwritten.");
+ }
+ } else if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) {
+ confirmed = confirm(fmt("<b>A file named \"%1%\" already exists. "
+ "Replace it with an Ingen bundle?</b>",
+ basename));
+ if (confirmed) {
+ ::g_remove(filename.c_str());
+ }
+ }
+
+ if (confirmed) {
+ const Glib::ustring uri = Glib::filename_to_uri(filename);
+ save_graph(URI(uri));
+
+ const_cast<GraphModel*>(_graph.get())->set_property(
+ uris.ingen_file,
+ _app->forge().alloc_uri(uri.c_str()));
+ }
+
+ _app->world().conf().set(
+ "graph-directory",
+ _app->world().forge().alloc(dialog.get_current_folder()));
+
+ break;
+ }
+}
+
+void
+GraphBox::event_export_image()
+{
+ Gtk::FileChooserDialog dialog("Export Image", Gtk::FILE_CHOOSER_ACTION_SAVE);
+ dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
+ dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK);
+ dialog.set_default_response(Gtk::RESPONSE_OK);
+ if (_window) {
+ dialog.set_transient_for(*_window);
+ }
+
+ typedef std::map<std::string, std::string> Types;
+ Types types;
+ types["*.dot"] = "Graphviz DOT";
+ types["*.pdf"] = "Portable Document Format";
+ types["*.ps"] = "PostScript";
+ types["*.svg"] = "Scalable Vector Graphics";
+ for (Types::const_iterator t = types.begin(); t != types.end(); ++t) {
+ Gtk::FileFilter filt;
+ filt.add_pattern(t->first);
+ filt.set_name(t->second);
+ dialog.add_filter(filt);
+ if (t->first == "*.pdf") {
+ dialog.set_filter(filt);
+ }
+ }
+
+ Gtk::CheckButton* bg_but = new Gtk::CheckButton("Draw _Background", true);
+ Gtk::Alignment* extra = new Gtk::Alignment(1.0, 0.5, 0.0, 0.0);
+ bg_but->set_active(true);
+ extra->add(*Gtk::manage(bg_but));
+ extra->show_all();
+ dialog.set_extra_widget(*Gtk::manage(extra));
+
+ if (dialog.run() == Gtk::RESPONSE_OK) {
+ const std::string filename = dialog.get_filename();
+ if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) {
+ Gtk::MessageDialog confirm(
+ std::string("File exists! Overwrite ") + filename + "?",
+ true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true);
+ confirm.set_transient_for(dialog);
+ if (confirm.run() != Gtk::RESPONSE_YES) {
+ return;
+ }
+ }
+ _view->canvas()->export_image(filename.c_str(), bg_but->get_active());
+ _status_bar->push(fmt("Rendered %1% to %2%", _graph->path(), filename),
+ STATUS_CONTEXT_GRAPH);
+ }
+}
+
+void
+GraphBox::event_copy()
+{
+ if (_view) {
+ _view->canvas()->copy_selection();
+ }
+}
+
+void
+GraphBox::event_redo()
+{
+ _app->interface()->redo();
+}
+
+void
+GraphBox::event_undo()
+{
+ _app->interface()->undo();
+}
+
+void
+GraphBox::event_paste()
+{
+ if (_view) {
+ _view->canvas()->paste();
+ }
+}
+
+void
+GraphBox::event_delete()
+{
+ if (_view) {
+ _view->canvas()->destroy_selection();
+ }
+}
+
+void
+GraphBox::event_select_all()
+{
+ if (_view) {
+ _view->canvas()->select_all();
+ }
+}
+
+void
+GraphBox::event_close()
+{
+ if (_window) {
+ _app->window_factory()->remove_graph_window(_window);
+ }
+}
+
+void
+GraphBox::event_quit()
+{
+ _app->quit(_window);
+}
+
+void
+GraphBox::event_zoom_in()
+{
+ _view->canvas()->set_font_size(_view->canvas()->get_font_size() + 1.0);
+}
+
+void
+GraphBox::event_zoom_out()
+{
+ _view->canvas()->set_font_size(_view->canvas()->get_font_size() - 1.0);
+}
+
+void
+GraphBox::event_zoom_normal()
+{
+ _view->canvas()->set_zoom(1.0);
+}
+
+void
+GraphBox::event_zoom_full()
+{
+ _view->canvas()->zoom_full();
+}
+
+void
+GraphBox::event_increase_font_size()
+{
+ _view->canvas()->set_font_size(_view->canvas()->get_font_size() + 1.0);
+}
+void
+GraphBox::event_decrease_font_size()
+{
+ _view->canvas()->set_font_size(_view->canvas()->get_font_size() - 1.0);
+}
+void
+GraphBox::event_normal_font_size()
+{
+ _view->canvas()->set_font_size(_view->canvas()->get_default_font_size());
+}
+
+void
+GraphBox::event_arrange()
+{
+ _app->interface()->bundle_begin();
+ _view->canvas()->arrange();
+ _app->interface()->bundle_end();
+}
+
+void
+GraphBox::event_parent_activated()
+{
+ SPtr<client::GraphModel> parent = dynamic_ptr_cast<client::GraphModel>(_graph->parent());
+ if (parent) {
+ _app->window_factory()->present_graph(parent, _window);
+ }
+}
+
+void
+GraphBox::event_refresh_activated()
+{
+ _app->interface()->get(_graph->uri());
+}
+
+void
+GraphBox::event_fullscreen_toggled()
+{
+ // FIXME: ugh, use GTK signals to track state and know for sure
+ static bool is_fullscreen = false;
+
+ if (_window) {
+ if (!is_fullscreen) {
+ _window->fullscreen();
+ is_fullscreen = true;
+ } else {
+ _window->unfullscreen();
+ is_fullscreen = false;
+ }
+ }
+}
+
+void
+GraphBox::event_doc_pane_toggled()
+{
+ if (_menu_show_doc_pane->get_active()) {
+ _doc_scrolledwindow->show_all();
+ if (!_has_shown_documentation) {
+ const Gtk::Allocation allocation = get_allocation();
+ _doc_paned->set_position(allocation.get_width() / 1.61803399);
+ _has_shown_documentation = true;
+ }
+ } else {
+ _doc_scrolledwindow->hide();
+ }
+}
+
+void
+GraphBox::event_status_bar_toggled()
+{
+ if (_menu_show_status_bar->get_active()) {
+ _status_bar->show();
+ } else {
+ _status_bar->hide();
+ }
+}
+
+void
+GraphBox::event_animate_signals_toggled()
+{
+ _app->interface()->set_property(
+ URI("ingen:/clients/this"),
+ _app->uris().ingen_broadcast,
+ _app->forge().make((bool)_menu_animate_signals->get_active()));
+}
+
+void
+GraphBox::event_sprung_layout_toggled()
+{
+ const bool sprung = _menu_sprung_layout->get_active();
+
+ _view->canvas()->set_sprung_layout(sprung);
+
+ Properties properties;
+ properties.emplace(_app->uris().ingen_sprungLayout,
+ _app->forge().make(sprung));
+ _app->interface()->put(_graph->uri(), properties);
+}
+
+void
+GraphBox::event_human_names_toggled()
+{
+ _view->canvas()->show_human_names(_menu_human_names->get_active());
+ _app->world().conf().set(
+ "human-names",
+ _app->world().forge().make(_menu_human_names->get_active()));
+}
+
+void
+GraphBox::event_port_names_toggled()
+{
+ _app->world().conf().set(
+ "port-labels",
+ _app->world().forge().make(_menu_show_port_names->get_active()));
+ if (_menu_show_port_names->get_active()) {
+ _view->canvas()->set_direction(GANV_DIRECTION_RIGHT);
+ _view->canvas()->show_port_names(true);
+ } else {
+ _view->canvas()->set_direction(GANV_DIRECTION_DOWN);
+ _view->canvas()->show_port_names(false);
+ }
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/GraphBox.hpp b/src/gui/GraphBox.hpp
new file mode 100644
index 00000000..93599e0b
--- /dev/null
+++ b/src/gui/GraphBox.hpp
@@ -0,0 +1,213 @@
+/*
+ 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_GUI_GRAPH_BOX_HPP
+#define INGEN_GUI_GRAPH_BOX_HPP
+
+#include <string>
+
+#include <gtkmm/alignment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/menushell.h>
+#include <gtkmm/messagedialog.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/statusbar.h>
+
+#include "ingen/ingen.h"
+#include "ingen/types.hpp"
+
+#include "Window.hpp"
+
+namespace Raul {
+class Atom;
+class Path;
+}
+
+namespace ingen {
+
+class URI;
+
+namespace client {
+class GraphModel;
+class PortModel;
+class ObjectModel;
+}
+
+namespace gui {
+
+class BreadCrumbs;
+class LoadGraphBox;
+class LoadPluginWindow;
+class NewSubgraphWindow;
+class GraphDescriptionWindow;
+class GraphView;
+class GraphWindow;
+class SubgraphModule;
+
+/** A window for a graph.
+ *
+ * \ingroup GUI
+ */
+class INGEN_API GraphBox : public Gtk::VBox
+{
+public:
+ GraphBox(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+ ~GraphBox();
+
+ static SPtr<GraphBox> create(
+ App& app, SPtr<const client::GraphModel> graph);
+
+ void init_box(App& app);
+
+ void set_status_text(const std::string& text);
+
+ void set_graph(SPtr<const client::GraphModel> graph,
+ SPtr<GraphView> view);
+
+ void set_window(GraphWindow* win) { _window = win; }
+
+ bool documentation_is_visible() { return _doc_scrolledwindow->is_visible(); }
+ void set_documentation(const std::string& doc, bool html);
+
+ SPtr<const client::GraphModel> graph() const { return _graph; }
+ SPtr<GraphView> view() const { return _view; }
+
+ void show_port_status(const client::PortModel* port,
+ const Atom& value);
+
+ void set_graph_from_path(const Raul::Path& path, SPtr<GraphView> view);
+
+ void object_entered(const client::ObjectModel* model);
+ void object_left(const client::ObjectModel* model);
+
+private:
+ void graph_port_added(SPtr<const client::PortModel> port);
+ void graph_port_removed(SPtr<const client::PortModel> port);
+ void property_changed(const URI& predicate, const Atom& value);
+ void show_status(const client::ObjectModel* model);
+
+ void error(const Glib::ustring& message,
+ const Glib::ustring& secondary_text="");
+
+ bool confirm(const Glib::ustring& message,
+ const Glib::ustring& secondary_text="");
+
+ void save_graph(const URI& uri);
+
+ void event_import();
+ 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();
+ void event_select_all();
+ void event_close();
+ void event_quit();
+ void event_parent_activated();
+ void event_refresh_activated();
+ void event_fullscreen_toggled();
+ void event_doc_pane_toggled();
+ void event_status_bar_toggled();
+ void event_animate_signals_toggled();
+ void event_sprung_layout_toggled();
+ void event_human_names_toggled();
+ void event_port_names_toggled();
+ void event_zoom_in();
+ void event_zoom_out();
+ void event_zoom_normal();
+ void event_zoom_full();
+ void event_increase_font_size();
+ void event_decrease_font_size();
+ void event_normal_font_size();
+ void event_arrange();
+ void event_show_properties();
+ void event_show_engine();
+ void event_clipboard_changed(GdkEventOwnerChange* ev);
+
+ App* _app;
+ SPtr<const client::GraphModel> _graph;
+ SPtr<GraphView> _view;
+ GraphWindow* _window;
+
+ sigc::connection new_port_connection;
+ sigc::connection removed_port_connection;
+ sigc::connection edit_mode_connection;
+
+ Gtk::MenuItem* _menu_import;
+ 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;
+ Gtk::MenuItem* _menu_delete;
+ Gtk::MenuItem* _menu_select_all;
+ Gtk::MenuItem* _menu_close;
+ Gtk::MenuItem* _menu_quit;
+ Gtk::CheckMenuItem* _menu_animate_signals;
+ Gtk::CheckMenuItem* _menu_sprung_layout;
+ Gtk::CheckMenuItem* _menu_human_names;
+ Gtk::CheckMenuItem* _menu_show_port_names;
+ Gtk::CheckMenuItem* _menu_show_doc_pane;
+ Gtk::CheckMenuItem* _menu_show_status_bar;
+ Gtk::MenuItem* _menu_zoom_in;
+ Gtk::MenuItem* _menu_zoom_out;
+ Gtk::MenuItem* _menu_zoom_normal;
+ Gtk::MenuItem* _menu_zoom_full;
+ Gtk::MenuItem* _menu_increase_font_size;
+ Gtk::MenuItem* _menu_decrease_font_size;
+ Gtk::MenuItem* _menu_normal_font_size;
+ Gtk::MenuItem* _menu_parent;
+ Gtk::MenuItem* _menu_refresh;
+ Gtk::MenuItem* _menu_fullscreen;
+ Gtk::MenuItem* _menu_arrange;
+ Gtk::MenuItem* _menu_view_engine_window;
+ Gtk::MenuItem* _menu_view_control_window;
+ Gtk::MenuItem* _menu_view_graph_properties;
+ Gtk::MenuItem* _menu_view_messages_window;
+ Gtk::MenuItem* _menu_view_graph_tree_window;
+ Gtk::MenuItem* _menu_help_about;
+
+ Gtk::Alignment* _alignment;
+ BreadCrumbs* _breadcrumbs;
+ Gtk::Statusbar* _status_bar;
+ Gtk::Label* _status_label;
+
+ Gtk::HPaned* _doc_paned;
+ Gtk::ScrolledWindow* _doc_scrolledwindow;
+
+ sigc::connection _entered_connection;
+ sigc::connection _left_connection;
+
+ /** Invisible bin used to store breadcrumbs when not shown by a view */
+ Gtk::Alignment _breadcrumb_bin;
+
+ bool _has_shown_documentation;
+ bool _enable_signal;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_GRAPH_BOX_HPP
diff --git a/src/gui/GraphCanvas.cpp b/src/gui/GraphCanvas.cpp
new file mode 100644
index 00000000..d10d569b
--- /dev/null
+++ b/src/gui/GraphCanvas.cpp
@@ -0,0 +1,912 @@
+/*
+ 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/>.
+*/
+
+#include "App.hpp"
+#include "Arc.hpp"
+#include "GraphCanvas.hpp"
+#include "GraphPortModule.hpp"
+#include "GraphWindow.hpp"
+#include "LoadPluginWindow.hpp"
+#include "NewSubgraphWindow.hpp"
+#include "NodeModule.hpp"
+#include "PluginMenu.hpp"
+#include "Port.hpp"
+#include "SubgraphModule.hpp"
+#include "ThreadedLoader.hpp"
+#include "WidgetFactory.hpp"
+#include "WindowFactory.hpp"
+
+#include "ganv/Canvas.hpp"
+#include "ganv/Circle.hpp"
+#include "ingen/ClashAvoider.hpp"
+#include "ingen/Configuration.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/Serialiser.hpp"
+#include "ingen/World.hpp"
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/client/PluginModel.hpp"
+#include "ingen/ingen.h"
+#include "lv2/atom/atom.h"
+
+#include <boost/optional/optional.hpp>
+#include <gtkmm/stock.h>
+
+#include <algorithm>
+#include <cassert>
+#include <map>
+#include <set>
+#include <string>
+
+using std::string;
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+static int
+port_order(const GanvPort* a, const GanvPort* b, void* data)
+{
+ const Port* pa = dynamic_cast<const Port*>(Glib::wrap(a));
+ const Port* pb = dynamic_cast<const Port*>(Glib::wrap(b));
+ if (pa && pb) {
+ return ((int)pa->model()->index() - (int)pb->model()->index());
+ }
+ return 0;
+}
+
+GraphCanvas::GraphCanvas(App& app,
+ SPtr<const GraphModel> graph,
+ int width,
+ int height)
+ : Canvas(width, height)
+ , _app(app)
+ , _graph(std::move(graph))
+ , _auto_position_count(0)
+ , _menu_x(0)
+ , _menu_y(0)
+ , _paste_count(0)
+ , _menu(nullptr)
+ , _internal_menu(nullptr)
+ , _plugin_menu(nullptr)
+ , _human_names(true)
+ , _show_port_names(true)
+ , _menu_dirty(false)
+{
+ Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create("canvas_menu");
+ xml->get_widget("canvas_menu", _menu);
+
+ xml->get_widget("canvas_menu_add_audio_input", _menu_add_audio_input);
+ xml->get_widget("canvas_menu_add_audio_output", _menu_add_audio_output);
+ xml->get_widget("canvas_menu_add_cv_input", _menu_add_cv_input);
+ xml->get_widget("canvas_menu_add_cv_output", _menu_add_cv_output);
+ xml->get_widget("canvas_menu_add_control_input", _menu_add_control_input);
+ xml->get_widget("canvas_menu_add_control_output", _menu_add_control_output);
+ xml->get_widget("canvas_menu_add_event_input", _menu_add_event_input);
+ xml->get_widget("canvas_menu_add_event_output", _menu_add_event_output);
+ xml->get_widget("canvas_menu_load_plugin", _menu_load_plugin);
+ xml->get_widget("canvas_menu_load_graph", _menu_load_graph);
+ xml->get_widget("canvas_menu_new_graph", _menu_new_graph);
+ xml->get_widget("canvas_menu_edit", _menu_edit);
+ xml->get_widget("canvas_menu_properties", _menu_properties);
+
+ const URIs& uris = _app.uris();
+
+ // Add port menu items
+ _menu_add_audio_input->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port),
+ "audio_in", "Audio In", uris.lv2_AudioPort, false));
+ _menu_add_audio_output->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port),
+ "audio_out", "Audio Out", uris.lv2_AudioPort, true));
+ _menu_add_cv_input->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port),
+ "cv_in", "CV In", uris.lv2_CVPort, false));
+ _menu_add_cv_output->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port),
+ "cv_out", "CV Out", uris.lv2_CVPort, true));
+ _menu_add_control_input->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port),
+ "control_in", "Control In", uris.lv2_ControlPort, false));
+ _menu_add_control_output->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port),
+ "control_out", "Control Out", uris.lv2_ControlPort, true));
+ _menu_add_event_input->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port),
+ "event_in", "Event In", uris.atom_AtomPort, false));
+ _menu_add_event_output->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port),
+ "event_out", "Event Out", uris.atom_AtomPort, true));
+
+ signal_event.connect(
+ sigc::mem_fun(this, &GraphCanvas::on_event));
+ signal_connect.connect(
+ sigc::mem_fun(this, &GraphCanvas::connect));
+ signal_disconnect.connect(
+ sigc::mem_fun(this, &GraphCanvas::disconnect));
+
+ // Connect to model signals to track state
+ _graph->signal_new_block().connect(
+ sigc::mem_fun(this, &GraphCanvas::add_block));
+ _graph->signal_removed_block().connect(
+ sigc::mem_fun(this, &GraphCanvas::remove_block));
+ _graph->signal_new_port().connect(
+ sigc::mem_fun(this, &GraphCanvas::add_port));
+ _graph->signal_removed_port().connect(
+ sigc::mem_fun(this, &GraphCanvas::remove_port));
+ _graph->signal_new_arc().connect(
+ sigc::mem_fun(this, &GraphCanvas::connection));
+ _graph->signal_removed_arc().connect(
+ sigc::mem_fun(this, &GraphCanvas::disconnection));
+
+ _app.store()->signal_new_plugin().connect(
+ sigc::mem_fun(this, &GraphCanvas::add_plugin));
+ _app.store()->signal_plugin_deleted().connect(
+ sigc::mem_fun(this, &GraphCanvas::remove_plugin));
+
+ // Connect widget signals to do things
+ _menu_load_plugin->signal_activate().connect(
+ sigc::mem_fun(this, &GraphCanvas::menu_load_plugin));
+ _menu_load_graph->signal_activate().connect(
+ sigc::mem_fun(this, &GraphCanvas::menu_load_graph));
+ _menu_new_graph->signal_activate().connect(
+ sigc::mem_fun(this, &GraphCanvas::menu_new_graph));
+ _menu_properties->signal_activate().connect(
+ sigc::mem_fun(this, &GraphCanvas::menu_properties));
+
+ show_human_names(app.world().conf().option("human-names").get<int32_t>());
+ show_port_names(app.world().conf().option("port-labels").get<int32_t>());
+ set_port_order(port_order, nullptr);
+}
+
+void
+GraphCanvas::show_menu(bool position, unsigned button, uint32_t time)
+{
+ _app.request_plugins_if_necessary();
+
+ if (!_internal_menu || _menu_dirty) {
+ build_menus();
+ }
+
+ if (position) {
+ _menu->popup(sigc::mem_fun(this, &GraphCanvas::auto_menu_position), button, time);
+ } else {
+ _menu->popup(button, time);
+ }
+}
+
+void
+GraphCanvas::build_menus()
+{
+ // Build (or clear existing) internal plugin menu
+ if (_internal_menu) {
+ _internal_menu->items().clear();
+ } else {
+ _menu->items().push_back(
+ Gtk::Menu_Helpers::ImageMenuElem(
+ "In_ternal",
+ *(manage(new Gtk::Image(Gtk::Stock::EXECUTE, Gtk::ICON_SIZE_MENU)))));
+ Gtk::MenuItem* internal_menu_item = &(_menu->items().back());
+ _internal_menu = Gtk::manage(new Gtk::Menu());
+ internal_menu_item->set_submenu(*_internal_menu);
+ _menu->reorder_child(*internal_menu_item, 4);
+ }
+
+ // Build skeleton LV2 plugin class heirarchy for 'Plugin' menu
+ if (_plugin_menu) {
+ _plugin_menu->clear();
+ } else {
+ _plugin_menu = Gtk::manage(new PluginMenu(_app.world()));
+ _menu->items().push_back(
+ Gtk::Menu_Helpers::ImageMenuElem(
+ "_Plugin",
+ *(manage(new Gtk::Image(Gtk::Stock::EXECUTE, Gtk::ICON_SIZE_MENU)))));
+ Gtk::MenuItem* plugin_menu_item = &(_menu->items().back());
+ plugin_menu_item->set_submenu(*_plugin_menu);
+ _menu->reorder_child(*plugin_menu_item, 5);
+ _plugin_menu->signal_load_plugin.connect(
+ sigc::mem_fun(this, &GraphCanvas::load_plugin));
+ }
+
+ // Add known plugins to menu heirarchy
+ SPtr<const ClientStore::Plugins> plugins = _app.store()->plugins();
+ for (const auto& p : *plugins.get()) {
+ add_plugin(p.second);
+ }
+
+ _menu_dirty = false;
+}
+
+void
+GraphCanvas::build()
+{
+ const Store::const_range kids = _app.store()->children_range(_graph);
+
+ // Create modules for blocks
+ for (Store::const_iterator i = kids.first; i != kids.second; ++i) {
+ SPtr<BlockModel> block = dynamic_ptr_cast<BlockModel>(i->second);
+ if (block && block->parent() == _graph) {
+ add_block(block);
+ }
+ }
+
+ // Create pseudo modules for ports (ports on this canvas, not on our module)
+ for (const auto& p : _graph->ports()) {
+ add_port(p);
+ }
+
+ // Create arcs
+ for (const auto& a : _graph->arcs()) {
+ connection(dynamic_ptr_cast<ArcModel>(a.second));
+ }
+}
+
+static void
+show_module_human_names(GanvNode* node, void* data)
+{
+ bool b = *(bool*)data;
+ if (GANV_IS_MODULE(node)) {
+ Ganv::Module* module = Glib::wrap(GANV_MODULE(node));
+ NodeModule* nmod = dynamic_cast<NodeModule*>(module);
+ if (nmod) {
+ nmod->show_human_names(b);
+ }
+
+ GraphPortModule* pmod = dynamic_cast<GraphPortModule*>(module);
+ if (pmod) {
+ pmod->show_human_names(b);
+ }
+ }
+}
+
+void
+GraphCanvas::show_human_names(bool b)
+{
+ _human_names = b;
+ _app.world().conf().set("human-names", _app.forge().make(b));
+
+ for_each_node(show_module_human_names, &b);
+}
+
+static void
+ensure_port_labels(GanvNode* node, void* data)
+{
+ if (GANV_IS_MODULE(node)) {
+ Ganv::Module* module = Glib::wrap(GANV_MODULE(node));
+ for (Ganv::Port* p : *module) {
+ ingen::gui::Port* port = dynamic_cast<ingen::gui::Port*>(p);
+ if (port) {
+ port->ensure_label();
+ }
+ }
+ }
+}
+
+void
+GraphCanvas::show_port_names(bool b)
+{
+ ganv_canvas_set_direction(gobj(), b ? GANV_DIRECTION_RIGHT : GANV_DIRECTION_DOWN);
+ for_each_node(ensure_port_labels, &b);
+}
+
+void
+GraphCanvas::add_plugin(SPtr<PluginModel> p)
+{
+ if (_internal_menu && _app.uris().ingen_Internal == p->type()) {
+ _internal_menu->items().push_back(
+ Gtk::Menu_Helpers::MenuElem(
+ std::string("_") + p->human_name(),
+ sigc::bind(sigc::mem_fun(this, &GraphCanvas::load_plugin), p)));
+ } else if (_plugin_menu) {
+ _plugin_menu->add_plugin(p);
+ }
+}
+
+void
+GraphCanvas::remove_plugin(const URI& uri)
+{
+ // Flag menus as dirty so they will be rebuilt when needed next
+ _menu_dirty = true;
+}
+
+void
+GraphCanvas::add_block(SPtr<const BlockModel> bm)
+{
+ SPtr<const GraphModel> pm = dynamic_ptr_cast<const GraphModel>(bm);
+ NodeModule* module;
+ if (pm) {
+ module = SubgraphModule::create(*this, pm, _human_names);
+ } else {
+ module = NodeModule::create(*this, bm, _human_names);
+ }
+
+ module->show();
+ _views.emplace(bm, module);
+ if (_pastees.find(bm->path()) != _pastees.end()) {
+ module->set_selected(true);
+ }
+}
+
+void
+GraphCanvas::remove_block(SPtr<const BlockModel> bm)
+{
+ auto i = _views.find(bm);
+
+ if (i != _views.end()) {
+ const guint n_ports = i->second->num_ports();
+ for (gint p = n_ports - 1; p >= 0; --p) {
+ delete i->second->get_port(p);
+ }
+ delete i->second;
+ _views.erase(i);
+ }
+}
+
+void
+GraphCanvas::add_port(SPtr<const PortModel> pm)
+{
+ GraphPortModule* view = GraphPortModule::create(*this, pm);
+ _views.emplace(pm, view);
+ view->show();
+}
+
+void
+GraphCanvas::remove_port(SPtr<const PortModel> pm)
+{
+ auto i = _views.find(pm);
+
+ // Port on this graph
+ if (i != _views.end()) {
+ delete i->second;
+ _views.erase(i);
+
+ } else {
+ NodeModule* module = dynamic_cast<NodeModule*>(_views[pm->parent()]);
+ module->delete_port_view(pm);
+ }
+
+ assert(_views.find(pm) == _views.end());
+}
+
+Ganv::Port*
+GraphCanvas::get_port_view(SPtr<PortModel> port)
+{
+ Ganv::Module* module = _views[port];
+
+ // Port on this graph
+ if (module) {
+ GraphPortModule* ppm = dynamic_cast<GraphPortModule*>(module);
+ return ppm
+ ? *ppm->begin()
+ : dynamic_cast<Ganv::Port*>(module);
+ } else {
+ module = dynamic_cast<NodeModule*>(_views[port->parent()]);
+ if (module) {
+ for (const auto& p : *module) {
+ gui::Port* pv = dynamic_cast<gui::Port*>(p);
+ if (pv && pv->model() == port) {
+ return pv;
+ }
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+/** Called when a connection is added to the model. */
+void
+GraphCanvas::connection(SPtr<const ArcModel> arc)
+{
+ Ganv::Port* const tail = get_port_view(arc->tail());
+ Ganv::Port* const head = get_port_view(arc->head());
+
+ if (tail && head) {
+ new gui::Arc(*this, arc, tail, head);
+ } else {
+ _app.log().error("Unable to find ports to connect %1% => %2%\n",
+ arc->tail_path(), arc->head_path());
+ }
+}
+
+/** Called when a connection is removed from the model. */
+void
+GraphCanvas::disconnection(SPtr<const ArcModel> arc)
+{
+ Ganv::Port* const tail = get_port_view(arc->tail());
+ Ganv::Port* const head = get_port_view(arc->head());
+
+ if (tail && head) {
+ remove_edge_between(tail, head);
+ if (arc->head()->is_a(_app.uris().lv2_AudioPort)) {
+ gui::Port* const h = dynamic_cast<gui::Port*>(head);
+ if (h) {
+ h->activity(_app.forge().make(0.0f)); // Reset peaks
+ }
+ }
+ } else {
+ _app.log().error("Unable to find ports to disconnect %1% => %2%\n",
+ arc->tail_path(), arc->head_path());
+ }
+}
+
+/** Called when the user connects ports on the canvas. */
+void
+GraphCanvas::connect(Ganv::Node* tail, Ganv::Node* head)
+{
+ const gui::Port* const t = dynamic_cast<gui::Port*>(tail);
+ const gui::Port* const h = dynamic_cast<gui::Port*>(head);
+
+ if (t && h) {
+ _app.interface()->connect(t->model()->path(), h->model()->path());
+ }
+}
+
+/** Called when the user disconnects ports on the canvas. */
+void
+GraphCanvas::disconnect(Ganv::Node* tail, Ganv::Node* head)
+{
+ const gui::Port* const t = dynamic_cast<gui::Port*>(tail);
+ const gui::Port* const h = dynamic_cast<gui::Port*>(head);
+
+ if (t && h) {
+ _app.interface()->disconnect(t->model()->path(), h->model()->path());
+ }
+}
+
+void
+GraphCanvas::auto_menu_position(int& x, int& y, bool& push_in)
+{
+ std::pair<int, int> scroll_offsets;
+ get_scroll_offsets(scroll_offsets.first, scroll_offsets.second);
+
+ if (_auto_position_count > 1 && scroll_offsets != _auto_position_scroll_offsets) {
+ // Scroll changed since last auto position, reset
+ _menu_x = 0;
+ _menu_y = 0;
+ _auto_position_count = 0;
+ }
+
+ if (_menu_x == 0 && _menu_y == 0) {
+ // No menu position set, start near top left of canvas
+ widget().translate_coordinates(
+ *_app.window_factory()->graph_window(_graph),
+ 64, 64, _menu_x, _menu_y);
+
+ int origin_x;
+ int origin_y;
+ widget().get_window()->get_origin(origin_x, origin_y);
+ _menu_x += origin_x;
+ _menu_y += origin_y;
+ }
+
+ const int cascade = _auto_position_count * 32;
+
+ x = _menu_x + cascade;
+ y = _menu_y + cascade;
+ push_in = true;
+
+ ++_auto_position_count;
+ _auto_position_scroll_offsets = scroll_offsets;
+}
+
+bool
+GraphCanvas::on_event(GdkEvent* event)
+{
+ assert(event);
+
+ bool ret = false;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 3) {
+ _auto_position_count = 1;
+ _menu_x = (int)event->button.x_root;
+ _menu_y = (int)event->button.y_root;
+ show_menu(false, event->button.button, event->button.time);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_PRESS:
+ switch (event->key.keyval) {
+ case GDK_Delete:
+ destroy_selection();
+ ret = true;
+ break;
+ case GDK_Home:
+ scroll_to(0, 0);
+ break;
+ case GDK_space:
+ case GDK_Menu:
+ show_menu(true, 3, event->key.time);
+ default: break;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ _paste_count = 0;
+ break;
+
+ default: break;
+ }
+
+ return ret;
+}
+
+void
+GraphCanvas::clear_selection()
+{
+ GraphWindow* win = _app.window_factory()->graph_window(_graph);
+ if (win) {
+ win->set_documentation("", false);
+ }
+
+ Ganv::Canvas::clear_selection();
+}
+
+static void
+destroy_node(GanvNode* node, void* data)
+{
+ if (!GANV_IS_MODULE(node)) {
+ return;
+ }
+
+ App* app = (App*)data;
+ Ganv::Module* module = Glib::wrap(GANV_MODULE(node));
+ NodeModule* node_module = dynamic_cast<NodeModule*>(module);
+
+ if (node_module) {
+ app->interface()->del(node_module->block()->uri());
+ } else {
+ GraphPortModule* port_module = dynamic_cast<GraphPortModule*>(module);
+ if (port_module &&
+ strcmp(port_module->port()->path().symbol(), "control") &&
+ strcmp(port_module->port()->path().symbol(), "notify")) {
+ app->interface()->del(port_module->port()->uri());
+ }
+ }
+}
+
+static void
+destroy_arc(GanvEdge* arc, void* data)
+{
+ App* app = (App*)data;
+ Ganv::Edge* arcmm = Glib::wrap(arc);
+
+ Port* tail = dynamic_cast<Port*>(arcmm->get_tail());
+ Port* head = dynamic_cast<Port*>(arcmm->get_head());
+ app->interface()->disconnect(tail->model()->path(), head->model()->path());
+}
+
+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
+serialise_node(GanvNode* node, void* data)
+{
+ Serialiser* serialiser = (Serialiser*)data;
+ if (!GANV_IS_MODULE(node)) {
+ return;
+ }
+
+ Ganv::Module* module = Glib::wrap(GANV_MODULE(node));
+ NodeModule* node_module = dynamic_cast<NodeModule*>(module);
+
+ if (node_module) {
+ serialiser->serialise(node_module->block());
+ } else {
+ GraphPortModule* port_module = dynamic_cast<GraphPortModule*>(module);
+ if (port_module) {
+ serialiser->serialise(port_module->port());
+ }
+ }
+}
+
+static void
+serialise_arc(GanvEdge* arc, void* data)
+{
+ Serialiser* serialiser = (Serialiser*)data;
+ if (!GANV_IS_EDGE(arc)) {
+ return;
+ }
+
+ gui::Arc* garc = dynamic_cast<gui::Arc*>(Glib::wrap(GANV_EDGE(arc)));
+ if (garc) {
+ serialiser->serialise_arc(Sord::Node(), garc->model());
+ }
+}
+
+void
+GraphCanvas::copy_selection()
+{
+ std::lock_guard<std::mutex> lock(_app.world().rdf_mutex());
+
+ Serialiser serialiser(_app.world());
+ serialiser.start_to_string(_graph->path(), _graph->base_uri());
+
+ for_each_selected_node(serialise_node, &serialiser);
+ for_each_selected_edge(serialise_arc, &serialiser);
+
+ Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get();
+ clipboard->set_text(serialiser.finish());
+ _paste_count = 0;
+}
+
+void
+GraphCanvas::paste()
+{
+ typedef Properties::const_iterator PropIter;
+
+ std::lock_guard<std::mutex> lock(_app.world().rdf_mutex());
+
+ const Glib::ustring str = Gtk::Clipboard::get()->wait_for_text();
+ SPtr<Parser> parser = _app.loader()->parser();
+ const URIs& uris = _app.uris();
+ const Raul::Path& parent = _graph->path();
+ if (!parser) {
+ _app.log().error("Unable to load parser, paste unavailable\n");
+ return;
+ }
+
+ // Prepare for paste
+ clear_selection();
+ _pastees.clear();
+ ++_paste_count;
+
+ // Make a client store to serve as clipboard
+ ClientStore clipboard(_app.world().uris(), _app.log());
+ clipboard.set_plugins(_app.store()->plugins());
+ clipboard.put(main_uri(),
+ {{uris.rdf_type, Property(uris.ingen_Graph)}});
+
+ // Parse clipboard text into clipboard store
+ boost::optional<URI> base_uri = parser->parse_string(
+ _app.world(), clipboard, str, main_uri());
+
+ // Figure out the copy graph base path
+ Raul::Path copy_root("/");
+ if (base_uri) {
+ std::string base = *base_uri;
+ if (base[base.size() - 1] == '/') {
+ base = base.substr(0, base.size() - 1);
+ }
+ copy_root = uri_to_path(URI(base));
+ }
+
+ // Find the minimum x and y coordinate of objects to be pasted
+ float min_x = std::numeric_limits<float>::max();
+ float min_y = std::numeric_limits<float>::max();
+ for (const auto& c : clipboard) {
+ if (c.first.parent() == Raul::Path("/")) {
+ const Atom& x = c.second->get_property(uris.ingen_canvasX);
+ const Atom& y = c.second->get_property(uris.ingen_canvasY);
+ if (x.type() == uris.atom_Float) {
+ min_x = std::min(min_x, x.get<float>());
+ }
+ if (y.type() == uris.atom_Float) {
+ min_y = std::min(min_y, y.get<float>());
+ }
+ }
+ }
+
+ // Find canvas paste origin based on pointer position
+ int widget_point_x, widget_point_y, scroll_x, scroll_y;
+ widget().get_pointer(widget_point_x, widget_point_y);
+ get_scroll_offsets(scroll_x, scroll_y);
+ const int paste_x = widget_point_x + scroll_x + (20.0f * _paste_count);
+ const int paste_y = widget_point_y + scroll_y + (20.0f * _paste_count);
+
+ _app.interface()->bundle_begin();
+
+ // Put each top level object in the clipboard store
+ ClashAvoider avoider(*_app.store().get());
+ for (const auto& c : clipboard) {
+ if (c.first.is_root() || c.first.parent() != Raul::Path("/")) {
+ continue;
+ }
+
+ const SPtr<Node> node = c.second;
+ const Raul::Path& old_path = copy_root.child(node->path());
+ const URI& old_uri = path_to_uri(old_path);
+ const Raul::Path& new_path = avoider.map_path(parent.child(node->path()));
+
+ // Copy properties, except those that should not be inherited in copies
+ Properties props = node->properties();
+ for (const auto& prop : {uris.lv2_prototype,
+ uris.ingen_canvasX,
+ uris.ingen_canvasY,
+ uris.lv2_index,
+ uris.lv2_symbol}) {
+ props.erase(prop);
+ }
+
+ // Store the old URI as a prototype (keeps provenance around)
+ props.emplace(uris.lv2_prototype, _app.forge().make_urid(old_uri));
+
+ // Adjust numeric suffix on name if appropriate
+ auto n = props.find(uris.lv2_name);
+ if (n != props.end()) {
+ n->second = _app.forge().alloc(ClashAvoider::adjust_name(
+ old_path, new_path, n->second.ptr<char>()));
+ }
+
+ // Set coordinates so paste origin is at the mouse pointer
+ PropIter xi = node->properties().find(uris.ingen_canvasX);
+ PropIter yi = node->properties().find(uris.ingen_canvasY);
+ if (xi != node->properties().end()) {
+ const float x = xi->second.get<float>() - min_x + paste_x;
+ props.insert({xi->first, Property(_app.forge().make(x),
+ xi->second.context())});
+ }
+ if (yi != node->properties().end()) {
+ const float y = yi->second.get<float>() - min_y + paste_y;
+ props.insert({yi->first, Property(_app.forge().make(y),
+ yi->second.context())});
+ }
+
+ _app.interface()->put(path_to_uri(new_path), props);
+ _pastees.insert(new_path);
+ }
+
+ // Connect objects
+ for (auto a : clipboard.object(Raul::Path("/"))->arcs()) {
+ _app.interface()->connect(
+ avoider.map_path(parent.child(a.second->tail_path())),
+ avoider.map_path(parent.child(a.second->head_path())));
+ }
+
+ _app.interface()->bundle_end();
+}
+
+void
+GraphCanvas::generate_port_name(
+ const string& sym_base, string& symbol,
+ const string& name_base, string& name)
+{
+ symbol = sym_base;
+ name = name_base;
+
+ char num_buf[5];
+ uint32_t i = 1;
+ for ( ; i < 9999; ++i) {
+ snprintf(num_buf, sizeof(num_buf), "%u", i);
+ symbol = sym_base + "_";
+ symbol += num_buf;
+ if (!_graph->get_port(Raul::Symbol::symbolify(symbol))) {
+ break;
+ }
+ }
+
+ assert(Raul::Path::is_valid(string("/") + symbol));
+
+ name.append(" ").append(num_buf);
+}
+
+void
+GraphCanvas::menu_add_port(const string& sym_base,
+ const string& name_base,
+ const URI& type,
+ bool is_output)
+{
+ string sym, name;
+ generate_port_name(sym_base, sym, name_base, name);
+ const Raul::Path& path = _graph->path().child(Raul::Symbol(sym));
+
+ const URIs& uris = _app.uris();
+
+ Properties props = get_initial_data(Resource::Graph::INTERNAL);
+ props.emplace(uris.rdf_type, _app.forge().make_urid(type));
+ if (type == uris.atom_AtomPort) {
+ props.emplace(uris.atom_bufferType, Property(uris.atom_Sequence));
+ }
+ props.emplace(
+ uris.rdf_type,
+ Property(is_output ? uris.lv2_OutputPort : uris.lv2_InputPort));
+ props.emplace(uris.lv2_index,
+ _app.forge().make(int32_t(_graph->num_ports())));
+ props.emplace(uris.lv2_name, _app.forge().alloc(name.c_str()));
+ _app.interface()->put(path_to_uri(path), props);
+}
+
+void
+GraphCanvas::load_plugin(WPtr<PluginModel> weak_plugin)
+{
+ SPtr<PluginModel> plugin = weak_plugin.lock();
+ if (!plugin) {
+ return;
+ }
+
+ Raul::Symbol symbol = plugin->default_block_symbol();
+ unsigned offset = _app.store()->child_name_offset(_graph->path(), symbol);
+ if (offset != 0) {
+ std::stringstream ss;
+ ss << symbol << "_" << offset;
+ symbol = Raul::Symbol(ss.str());
+ }
+
+ const URIs& uris = _app.uris();
+ const Raul::Path path = _graph->path().child(symbol);
+
+ // FIXME: polyphony?
+ Properties props = get_initial_data();
+ props.emplace(uris.rdf_type, Property(uris.ingen_Block));
+ props.emplace(uris.lv2_prototype, uris.forge.make_urid(plugin->uri()));
+ _app.interface()->put(path_to_uri(path), props);
+}
+
+/** Try to guess a suitable location for a new module.
+ */
+void
+GraphCanvas::get_new_module_location(double& x, double& y)
+{
+ int scroll_x;
+ int scroll_y;
+ get_scroll_offsets(scroll_x, scroll_y);
+ x = scroll_x + 20;
+ y = scroll_y + 20;
+}
+
+Properties
+GraphCanvas::get_initial_data(Resource::Graph ctx)
+{
+ Properties result;
+ const URIs& uris = _app.uris();
+ result.emplace(uris.ingen_canvasX,
+ Property(_app.forge().make((float)_menu_x), ctx));
+ result.emplace(uris.ingen_canvasY,
+ Property(_app.forge().make((float)_menu_y), ctx));
+ return result;
+}
+
+void
+GraphCanvas::menu_load_plugin()
+{
+ _app.window_factory()->present_load_plugin(_graph, get_initial_data());
+}
+
+void
+GraphCanvas::menu_load_graph()
+{
+ _app.window_factory()->present_load_subgraph(
+ _graph, get_initial_data(Resource::Graph::EXTERNAL));
+}
+
+void
+GraphCanvas::menu_new_graph()
+{
+ _app.window_factory()->present_new_subgraph(
+ _graph, get_initial_data(Resource::Graph::EXTERNAL));
+}
+
+void
+GraphCanvas::menu_properties()
+{
+ _app.window_factory()->present_properties(_graph);
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/GraphCanvas.hpp b/src/gui/GraphCanvas.hpp
new file mode 100644
index 00000000..613c03da
--- /dev/null
+++ b/src/gui/GraphCanvas.hpp
@@ -0,0 +1,158 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_GRAPHCANVAS_HPP
+#define INGEN_GUI_GRAPHCANVAS_HPP
+
+#include "NodeModule.hpp"
+
+#include "ganv/Canvas.hpp"
+#include "ganv/Module.hpp"
+#include "ingen/Node.hpp"
+#include "ingen/client/ArcModel.hpp"
+#include "ingen/types.hpp"
+#include "lilv/lilv.h"
+#include "raul/Path.hpp"
+
+#include <string>
+#include <map>
+#include <set>
+
+namespace ingen {
+
+namespace client { class GraphModel; }
+
+namespace gui {
+
+class NodeModule;
+class PluginMenu;
+
+/** Graph canvas widget.
+ *
+ * \ingroup GUI
+ */
+class GraphCanvas : public Ganv::Canvas
+{
+public:
+ GraphCanvas(App& app,
+ SPtr<const client::GraphModel> graph,
+ int width,
+ int height);
+
+ virtual ~GraphCanvas() {}
+
+ App& app() { return _app; }
+
+ void build();
+ void show_human_names(bool b);
+ void show_port_names(bool b);
+ bool show_port_names() const { return _show_port_names; }
+
+ void add_plugin(SPtr<client::PluginModel> p);
+ void remove_plugin(const URI& uri);
+ void add_block(SPtr<const client::BlockModel> bm);
+ void remove_block(SPtr<const client::BlockModel> bm);
+ void add_port(SPtr<const client::PortModel> pm);
+ void remove_port(SPtr<const client::PortModel> pm);
+ void connection(SPtr<const client::ArcModel> arc);
+ void disconnection(SPtr<const client::ArcModel> arc);
+
+ void get_new_module_location(double& x, double& y);
+
+ void clear_selection() override;
+ void destroy_selection();
+ void copy_selection();
+ void paste();
+
+ void show_menu(bool position, unsigned button, uint32_t time);
+
+ bool on_event(GdkEvent* event);
+
+private:
+ enum class ControlType { NUMBER, BUTTON };
+ void generate_port_name(
+ const std::string& sym_base, std::string& symbol,
+ const std::string& name_base, std::string& name);
+
+ void menu_add_port(const std::string& sym_base,
+ const std::string& name_base,
+ const URI& type,
+ bool is_output);
+
+ void menu_load_plugin();
+ void menu_new_graph();
+ void menu_load_graph();
+ void menu_properties();
+ void load_plugin(WPtr<client::PluginModel> weak_plugin);
+
+ void build_menus();
+
+ void auto_menu_position(int& x, int& y, bool& push_in);
+
+ typedef std::multimap<const std::string, const LilvPluginClass*> LV2Children;
+
+ Properties get_initial_data(Resource::Graph ctx=Resource::Graph::DEFAULT);
+
+ Ganv::Port* get_port_view(SPtr<client::PortModel> port);
+
+ void connect(Ganv::Node* tail,
+ Ganv::Node* head);
+
+ void disconnect(Ganv::Node* tail,
+ Ganv::Node* head);
+
+ App& _app;
+ SPtr<const client::GraphModel> _graph;
+
+ typedef std::map<SPtr<const client::ObjectModel>, Ganv::Module*> Views;
+ Views _views;
+
+ int _auto_position_count;
+ std::pair<int, int> _auto_position_scroll_offsets;
+
+ int _menu_x;
+ int _menu_y;
+ int _paste_count;
+
+ // Track pasted objects so they can be selected when they arrive
+ std::set<Raul::Path> _pastees;
+
+ Gtk::Menu* _menu;
+ Gtk::Menu* _internal_menu;
+ PluginMenu* _plugin_menu;
+ Gtk::MenuItem* _menu_add_audio_input;
+ Gtk::MenuItem* _menu_add_audio_output;
+ Gtk::MenuItem* _menu_add_control_input;
+ Gtk::MenuItem* _menu_add_control_output;
+ Gtk::MenuItem* _menu_add_cv_input;
+ Gtk::MenuItem* _menu_add_cv_output;
+ Gtk::MenuItem* _menu_add_event_input;
+ Gtk::MenuItem* _menu_add_event_output;
+ Gtk::MenuItem* _menu_load_plugin;
+ Gtk::MenuItem* _menu_load_graph;
+ Gtk::MenuItem* _menu_new_graph;
+ Gtk::MenuItem* _menu_properties;
+ Gtk::CheckMenuItem* _menu_edit;
+
+ bool _human_names;
+ bool _show_port_names;
+ bool _menu_dirty;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_GRAPHCANVAS_HPP
diff --git a/src/gui/GraphPortModule.cpp b/src/gui/GraphPortModule.cpp
new file mode 100644
index 00000000..ffca1834
--- /dev/null
+++ b/src/gui/GraphPortModule.cpp
@@ -0,0 +1,166 @@
+/*
+ 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 "App.hpp"
+#include "Style.hpp"
+#include "GraphCanvas.hpp"
+#include "GraphPortModule.hpp"
+#include "GraphWindow.hpp"
+#include "Port.hpp"
+#include "PortMenu.hpp"
+#include "RenameWindow.hpp"
+#include "WidgetFactory.hpp"
+#include "WindowFactory.hpp"
+
+#include "ingen/Configuration.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/client/GraphModel.hpp"
+
+#include <cassert>
+#include <string>
+#include <utility>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+GraphPortModule::GraphPortModule(GraphCanvas& canvas,
+ SPtr<const client::PortModel> model)
+ : Ganv::Module(canvas, "", 0, 0, false) // FIXME: coords?
+ , _model(model)
+ , _port(nullptr)
+{
+ assert(model);
+
+ assert(dynamic_ptr_cast<const GraphModel>(model->parent()));
+
+ set_stacked(model->polyphonic());
+ if (model->is_input() && !model->is_numeric()) {
+ set_is_source(true);
+ }
+
+ model->signal_property().connect(
+ sigc::mem_fun(this, &GraphPortModule::property_changed));
+
+ signal_moved().connect(
+ sigc::mem_fun(this, &GraphPortModule::store_location));
+}
+
+GraphPortModule*
+GraphPortModule::create(GraphCanvas& canvas,
+ SPtr<const PortModel> model)
+{
+ GraphPortModule* ret = new GraphPortModule(canvas, model);
+ Port* port = Port::create(canvas.app(), *ret, model, true);
+
+ ret->set_port(port);
+ if (model->is_numeric()) {
+ port->show_control();
+ }
+
+ for (const auto& p : model->properties()) {
+ ret->property_changed(p.first, p.second);
+ }
+
+ return ret;
+}
+
+App&
+GraphPortModule::app() const
+{
+ return ((GraphCanvas*)canvas())->app();
+}
+
+bool
+GraphPortModule::show_menu(GdkEventButton* ev)
+{
+ return _port->show_menu(ev);
+}
+
+void
+GraphPortModule::store_location(double ax, double ay)
+{
+ const URIs& uris = app().uris();
+
+ const Atom x(app().forge().make(static_cast<float>(ax)));
+ const Atom y(app().forge().make(static_cast<float>(ay)));
+
+ if (x != _model->get_property(uris.ingen_canvasX) ||
+ y != _model->get_property(uris.ingen_canvasY))
+ {
+ app().interface()->put(
+ _model->uri(),
+ {{uris.ingen_canvasX, Property(x, Property::Graph::INTERNAL)},
+ {uris.ingen_canvasY, Property(y, Property::Graph::INTERNAL)}});
+ }
+}
+
+void
+GraphPortModule::show_human_names(bool b)
+{
+ const URIs& uris = app().uris();
+ const Atom& name = _model->get_property(uris.lv2_name);
+ if (b && name.type() == uris.forge.String) {
+ set_name(name.ptr<char>());
+ } else {
+ set_name(_model->symbol().c_str());
+ }
+}
+
+void
+GraphPortModule::set_name(const std::string& n)
+{
+ _port->set_label(n.c_str());
+}
+
+void
+GraphPortModule::property_changed(const URI& key, const Atom& value)
+{
+ const URIs& uris = app().uris();
+ if (value.type() == uris.forge.Float) {
+ if (key == uris.ingen_canvasX) {
+ move_to(value.get<float>(), get_y());
+ } else if (key == uris.ingen_canvasY) {
+ move_to(get_x(), value.get<float>());
+ }
+ } else if (value.type() == uris.forge.String) {
+ if (key == uris.lv2_name &&
+ app().world().conf().option("human-names").get<int32_t>()) {
+ set_name(value.ptr<char>());
+ } else if (key == uris.lv2_symbol &&
+ !app().world().conf().option("human-names").get<int32_t>()) {
+ set_name(value.ptr<char>());
+ }
+ } else if (value.type() == uris.forge.Bool) {
+ if (key == uris.ingen_polyphonic) {
+ set_stacked(value.get<int32_t>());
+ }
+ }
+}
+
+void
+GraphPortModule::set_selected(gboolean b)
+{
+ if (b != get_selected()) {
+ Module::set_selected(b);
+ }
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/GraphPortModule.hpp b/src/gui/GraphPortModule.hpp
new file mode 100644
index 00000000..6400b327
--- /dev/null
+++ b/src/gui/GraphPortModule.hpp
@@ -0,0 +1,79 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_GRAPHPORTMODULE_HPP
+#define INGEN_GUI_GRAPHPORTMODULE_HPP
+
+#include "Port.hpp"
+
+#include "ganv/Module.hpp"
+
+#include <string>
+
+namespace Raul { class Atom; }
+
+namespace ingen { namespace client {
+class PortModel;
+} }
+
+namespace ingen {
+namespace gui {
+
+class GraphCanvas;
+class Port;
+class PortMenu;
+
+/** A "module" to represent a graph's port on its own canvas.
+ *
+ * Translation: This is the nameless single port pseudo module thingy.
+ *
+ * \ingroup GUI
+ */
+class GraphPortModule : public Ganv::Module
+{
+public:
+ static GraphPortModule* create(
+ GraphCanvas& canvas,
+ SPtr<const client::PortModel> model);
+
+ App& app() const;
+
+ virtual void store_location(double ax, double ay);
+ void show_human_names(bool b);
+
+ void set_name(const std::string& n);
+
+ SPtr<const client::PortModel> port() const { return _model; }
+
+protected:
+ GraphPortModule(GraphCanvas& canvas,
+ SPtr<const client::PortModel> model);
+
+ bool show_menu(GdkEventButton* ev);
+ void set_selected(gboolean b) override;
+
+ void set_port(Port* port) { _port = port; }
+
+ void property_changed(const URI& key, const Atom& value);
+
+ SPtr<const client::PortModel> _model;
+ Port* _port;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_GRAPHPORTMODULE_HPP
diff --git a/src/gui/GraphTreeWindow.cpp b/src/gui/GraphTreeWindow.cpp
new file mode 100644
index 00000000..7d00b0f5
--- /dev/null
+++ b/src/gui/GraphTreeWindow.cpp
@@ -0,0 +1,233 @@
+/*
+ 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 "App.hpp"
+#include "GraphTreeWindow.hpp"
+#include "SubgraphModule.hpp"
+#include "WindowFactory.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "raul/Path.hpp"
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+GraphTreeWindow::GraphTreeWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Window(cobject)
+ , _app(nullptr)
+ , _enable_signal(true)
+{
+ xml->get_widget_derived("graphs_treeview", _graphs_treeview);
+
+ _graph_treestore = Gtk::TreeStore::create(_graph_tree_columns);
+ _graphs_treeview->set_window(this);
+ _graphs_treeview->set_model(_graph_treestore);
+ Gtk::TreeViewColumn* name_col = Gtk::manage(
+ new Gtk::TreeViewColumn("Graph", _graph_tree_columns.name_col));
+ Gtk::TreeViewColumn* enabled_col = Gtk::manage(
+ new Gtk::TreeViewColumn("Run", _graph_tree_columns.enabled_col));
+ name_col->set_resizable(true);
+ name_col->set_expand(true);
+
+ _graphs_treeview->append_column(*name_col);
+ _graphs_treeview->append_column(*enabled_col);
+ Gtk::CellRendererToggle* enabled_renderer = dynamic_cast<Gtk::CellRendererToggle*>(
+ _graphs_treeview->get_column_cell_renderer(1));
+ enabled_renderer->property_activatable() = true;
+
+ _graph_tree_selection = _graphs_treeview->get_selection();
+
+ _graphs_treeview->signal_row_activated().connect(
+ sigc::mem_fun(this, &GraphTreeWindow::event_graph_activated));
+ enabled_renderer->signal_toggled().connect(
+ sigc::mem_fun(this, &GraphTreeWindow::event_graph_enabled_toggled));
+
+ _graphs_treeview->columns_autosize();
+}
+
+void
+GraphTreeWindow::init(App& app, ClientStore& store)
+{
+ _app = &app;
+ store.signal_new_object().connect(
+ sigc::mem_fun(this, &GraphTreeWindow::new_object));
+}
+
+void
+GraphTreeWindow::new_object(SPtr<ObjectModel> object)
+{
+ SPtr<GraphModel> graph = dynamic_ptr_cast<GraphModel>(object);
+ if (graph) {
+ add_graph(graph);
+ }
+}
+
+void
+GraphTreeWindow::add_graph(SPtr<GraphModel> pm)
+{
+ if (!pm->parent()) {
+ Gtk::TreeModel::iterator iter = _graph_treestore->append();
+ Gtk::TreeModel::Row row = *iter;
+ if (pm->path().is_root()) {
+ row[_graph_tree_columns.name_col] = _app->interface()->uri().string();
+ } else {
+ row[_graph_tree_columns.name_col] = pm->symbol().c_str();
+ }
+ row[_graph_tree_columns.enabled_col] = pm->enabled();
+ row[_graph_tree_columns.graph_model_col] = pm;
+ _graphs_treeview->expand_row(_graph_treestore->get_path(iter), true);
+ } else {
+ Gtk::TreeModel::Children children = _graph_treestore->children();
+ Gtk::TreeModel::iterator c = find_graph(children, pm->parent());
+
+ if (c != children.end()) {
+ Gtk::TreeModel::iterator iter = _graph_treestore->append(c->children());
+ Gtk::TreeModel::Row row = *iter;
+ row[_graph_tree_columns.name_col] = pm->symbol().c_str();
+ row[_graph_tree_columns.enabled_col] = pm->enabled();
+ row[_graph_tree_columns.graph_model_col] = pm;
+ _graphs_treeview->expand_row(_graph_treestore->get_path(iter), true);
+ }
+ }
+
+ pm->signal_property().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphTreeWindow::graph_property_changed),
+ pm));
+
+ pm->signal_moved().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphTreeWindow::graph_moved),
+ pm));
+
+ pm->signal_destroyed().connect(
+ sigc::bind(sigc::mem_fun(this, &GraphTreeWindow::remove_graph),
+ pm));
+}
+
+void
+GraphTreeWindow::remove_graph(SPtr<GraphModel> pm)
+{
+ Gtk::TreeModel::iterator i = find_graph(_graph_treestore->children(), pm);
+ if (i != _graph_treestore->children().end()) {
+ _graph_treestore->erase(i);
+ }
+}
+
+Gtk::TreeModel::iterator
+GraphTreeWindow::find_graph(Gtk::TreeModel::Children root,
+ SPtr<client::ObjectModel> graph)
+{
+ for (Gtk::TreeModel::iterator c = root.begin(); c != root.end(); ++c) {
+ SPtr<GraphModel> pm = (*c)[_graph_tree_columns.graph_model_col];
+ if (graph == pm) {
+ return c;
+ } else if ((*c)->children().size() > 0) {
+ Gtk::TreeModel::iterator ret = find_graph(c->children(), graph);
+ if (ret != c->children().end()) {
+ return ret;
+ }
+ }
+ }
+ return root.end();
+}
+
+/** Show the context menu for the selected graph in the graphs treeview.
+ */
+void
+GraphTreeWindow::show_graph_menu(GdkEventButton* ev)
+{
+ Gtk::TreeModel::iterator active = _graph_tree_selection->get_selected();
+ if (active) {
+ Gtk::TreeModel::Row row = *active;
+ SPtr<GraphModel> pm = row[_graph_tree_columns.graph_model_col];
+ if (pm) {
+ _app->log().warn("TODO: graph menu from tree window");
+ }
+ }
+}
+
+void
+GraphTreeWindow::event_graph_activated(const Gtk::TreeModel::Path& path,
+ Gtk::TreeView::Column* col)
+{
+ Gtk::TreeModel::iterator active = _graph_treestore->get_iter(path);
+ Gtk::TreeModel::Row row = *active;
+ SPtr<GraphModel> pm = row[_graph_tree_columns.graph_model_col];
+
+ _app->window_factory()->present_graph(pm);
+}
+
+void
+GraphTreeWindow::event_graph_enabled_toggled(const Glib::ustring& path_str)
+{
+ Gtk::TreeModel::Path path(path_str);
+ Gtk::TreeModel::iterator active = _graph_treestore->get_iter(path);
+ Gtk::TreeModel::Row row = *active;
+
+ SPtr<GraphModel> pm = row[_graph_tree_columns.graph_model_col];
+ assert(pm);
+
+ if (_enable_signal) {
+ _app->set_property(pm->uri(),
+ _app->uris().ingen_enabled,
+ _app->forge().make((bool)!pm->enabled()));
+ }
+}
+
+void
+GraphTreeWindow::graph_property_changed(const URI& key,
+ const Atom& value,
+ SPtr<GraphModel> graph)
+{
+ const URIs& uris = _app->uris();
+ _enable_signal = false;
+ if (key == uris.ingen_enabled && value.type() == uris.forge.Bool) {
+ Gtk::TreeModel::iterator i = find_graph(_graph_treestore->children(), graph);
+ if (i != _graph_treestore->children().end()) {
+ Gtk::TreeModel::Row row = *i;
+ row[_graph_tree_columns.enabled_col] = value.get<int32_t>();
+ } else {
+ _app->log().error("Unable to find graph %1%\n", graph->path());
+ }
+ }
+ _enable_signal = true;
+}
+
+void
+GraphTreeWindow::graph_moved(SPtr<GraphModel> graph)
+{
+ _enable_signal = false;
+
+ Gtk::TreeModel::iterator i
+ = find_graph(_graph_treestore->children(), graph);
+
+ if (i != _graph_treestore->children().end()) {
+ Gtk::TreeModel::Row row = *i;
+ row[_graph_tree_columns.name_col] = graph->symbol().c_str();
+ } else {
+ _app->log().error("Unable to find graph %1%\n", graph->path());
+ }
+
+ _enable_signal = true;
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/GraphTreeWindow.hpp b/src/gui/GraphTreeWindow.hpp
new file mode 100644
index 00000000..eb5a5f78
--- /dev/null
+++ b/src/gui/GraphTreeWindow.hpp
@@ -0,0 +1,123 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_GRAPHTREEWINDOW_HPP
+#define INGEN_GUI_GRAPHTREEWINDOW_HPP
+
+#include "Window.hpp"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/treeview.h>
+
+namespace Raul { class Path; }
+
+namespace ingen {
+
+namespace client { class ClientStore; class ObjectModel; }
+
+namespace gui {
+
+class GraphWindow;
+class GraphTreeView;
+
+/** Window with a TreeView of all loaded graphs.
+ *
+ * \ingroup GUI
+ */
+class GraphTreeWindow : public Window
+{
+public:
+ GraphTreeWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ void init(App& app, client::ClientStore& store);
+
+ void new_object(SPtr<client::ObjectModel> object);
+
+ void graph_property_changed(const URI& key,
+ const Atom& value,
+ SPtr<client::GraphModel> graph);
+
+ void graph_moved(SPtr<client::GraphModel> graph);
+
+ void add_graph(SPtr<client::GraphModel> pm);
+ void remove_graph(SPtr<client::GraphModel> pm);
+ void show_graph_menu(GdkEventButton* ev);
+
+protected:
+ void event_graph_activated(const Gtk::TreeModel::Path& path,
+ Gtk::TreeView::Column* col);
+
+ void event_graph_enabled_toggled(const Glib::ustring& path_str);
+
+ Gtk::TreeModel::iterator find_graph(
+ Gtk::TreeModel::Children root,
+ SPtr<client::ObjectModel> graph);
+
+ GraphTreeView* _graphs_treeview;
+
+ struct GraphTreeModelColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ GraphTreeModelColumns() {
+ add(name_col);
+ add(enabled_col);
+ add(graph_model_col);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> name_col;
+ Gtk::TreeModelColumn<bool> enabled_col;
+ Gtk::TreeModelColumn<SPtr<client::GraphModel> > graph_model_col;
+ };
+
+ App* _app;
+ GraphTreeModelColumns _graph_tree_columns;
+ Glib::RefPtr<Gtk::TreeStore> _graph_treestore;
+ Glib::RefPtr<Gtk::TreeSelection> _graph_tree_selection;
+ bool _enable_signal;
+};
+
+/** Derived TreeView class to support context menus for graphs */
+class GraphTreeView : public Gtk::TreeView
+{
+public:
+ GraphTreeView(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Gtk::TreeView(cobject)
+ , _window(nullptr)
+ {}
+
+ void set_window(GraphTreeWindow* win) { _window = win; }
+
+ bool on_button_press_event(GdkEventButton* ev) override {
+ bool ret = Gtk::TreeView::on_button_press_event(ev);
+
+ if ((ev->type == GDK_BUTTON_PRESS) && (ev->button == 3))
+ _window->show_graph_menu(ev);
+
+ return ret;
+ }
+
+private:
+ GraphTreeWindow* _window;
+
+}; // struct GraphTreeView
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_GRAPHTREEWINDOW_HPP
diff --git a/src/gui/GraphView.cpp b/src/gui/GraphView.cpp
new file mode 100644
index 00000000..9e39ef7c
--- /dev/null
+++ b/src/gui/GraphView.cpp
@@ -0,0 +1,154 @@
+/*
+ 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 "App.hpp"
+#include "LoadPluginWindow.hpp"
+#include "NewSubgraphWindow.hpp"
+#include "GraphCanvas.hpp"
+#include "GraphTreeWindow.hpp"
+#include "GraphView.hpp"
+#include "WidgetFactory.hpp"
+
+#include "ingen/Interface.hpp"
+#include "ingen/client/GraphModel.hpp"
+
+#include <cassert>
+#include <fstream>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+GraphView::GraphView(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Gtk::Box(cobject)
+ , _app(nullptr)
+ , _breadcrumb_container(nullptr)
+ , _enable_signal(true)
+{
+ property_visible() = false;
+
+ xml->get_widget("graph_view_breadcrumb_container", _breadcrumb_container);
+ xml->get_widget("graph_view_toolbar", _toolbar);
+ xml->get_widget("graph_view_process_but", _process_but);
+ xml->get_widget("graph_view_poly_spin", _poly_spin);
+ xml->get_widget("graph_view_scrolledwindow", _canvas_scrolledwindow);
+
+ _toolbar->set_toolbar_style(Gtk::TOOLBAR_ICONS);
+ _canvas_scrolledwindow->property_hadjustment().get_value()->set_step_increment(10);
+ _canvas_scrolledwindow->property_vadjustment().get_value()->set_step_increment(10);
+}
+
+GraphView::~GraphView()
+{
+ _canvas_scrolledwindow->remove();
+}
+
+void
+GraphView::init(App& app)
+{
+ _app = &app;
+}
+
+void
+GraphView::set_graph(SPtr<const GraphModel> graph)
+{
+ assert(!_canvas); // FIXME: remove
+
+ assert(_breadcrumb_container); // ensure created
+
+ _graph = graph;
+ _canvas = SPtr<GraphCanvas>(new GraphCanvas(*_app, graph, 1600*2, 1200*2));
+ _canvas->build();
+
+ _canvas_scrolledwindow->add(_canvas->widget());
+
+ _poly_spin->set_range(1, 128);
+ _poly_spin->set_increments(1, 4);
+ _poly_spin->set_value(graph->internal_poly());
+
+ for (const auto& p : graph->properties()) {
+ property_changed(p.first, p.second);
+ }
+
+ // Connect model signals to track state
+ graph->signal_property().connect(
+ sigc::mem_fun(this, &GraphView::property_changed));
+
+ // Connect widget signals to do things
+ _process_but->signal_toggled().connect(
+ sigc::mem_fun(this, &GraphView::process_toggled));
+
+ _poly_spin->signal_value_changed().connect(
+ sigc::mem_fun(*this, &GraphView::poly_changed));
+
+ _canvas->widget().grab_focus();
+}
+
+SPtr<GraphView>
+GraphView::create(App& app, SPtr<const GraphModel> graph)
+{
+ GraphView* result = nullptr;
+ Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create("warehouse_win");
+ xml->get_widget_derived("graph_view_box", result);
+ result->init(app);
+ result->set_graph(graph);
+ return SPtr<GraphView>(result);
+}
+
+void
+GraphView::process_toggled()
+{
+ if (!_enable_signal) {
+ return;
+ }
+
+ _app->set_property(_graph->uri(),
+ _app->uris().ingen_enabled,
+ _app->forge().make((bool)_process_but->get_active()));
+}
+
+void
+GraphView::poly_changed()
+{
+ const int poly = _poly_spin->get_value_as_int();
+ if (_enable_signal && poly != (int)_graph->internal_poly()) {
+ _app->set_property(_graph->uri(),
+ _app->uris().ingen_polyphony,
+ _app->forge().make(poly));
+ }
+}
+
+void
+GraphView::property_changed(const URI& predicate, const Atom& value)
+{
+ _enable_signal = false;
+ if (predicate == _app->uris().ingen_enabled) {
+ if (value.type() == _app->uris().forge.Bool) {
+ _process_but->set_active(value.get<int32_t>());
+ }
+ } else if (predicate == _app->uris().ingen_polyphony) {
+ if (value.type() == _app->uris().forge.Int) {
+ _poly_spin->set_value(value.get<int32_t>());
+ }
+ }
+ _enable_signal = true;
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/GraphView.hpp b/src/gui/GraphView.hpp
new file mode 100644
index 00000000..8c62ba3d
--- /dev/null
+++ b/src/gui/GraphView.hpp
@@ -0,0 +1,101 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_GRAPHVIEW_HPP
+#define INGEN_GUI_GRAPHVIEW_HPP
+
+#include "ingen/types.hpp"
+
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/toggletoolbutton.h>
+#include <gtkmm/toolbar.h>
+#include <gtkmm/toolitem.h>
+#include <gtkmm/toolitem.h>
+
+namespace Raul { class Atom; }
+
+namespace ingen {
+
+class Atom;
+class URI;
+
+namespace client {
+class PortModel;
+class MetadataModel;
+class GraphModel;
+class ObjectModel;
+}
+
+namespace gui {
+
+class App;
+class LoadPluginWindow;
+class NewSubgraphWindow;
+class GraphCanvas;
+class GraphDescriptionWindow;
+class SubgraphModule;
+
+/** The graph specific contents of a GraphWindow (ie the canvas and whatever else).
+ *
+ * \ingroup GUI
+ */
+class GraphView : public Gtk::Box
+{
+public:
+ GraphView(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ ~GraphView();
+
+ void init(App& app);
+
+ SPtr<GraphCanvas> canvas() const { return _canvas; }
+ SPtr<const client::GraphModel> graph() const { return _graph; }
+ Gtk::ToolItem* breadcrumb_container() const { return _breadcrumb_container; }
+
+ static SPtr<GraphView> create(App& app,
+ SPtr<const client::GraphModel> graph);
+
+private:
+ void set_graph(SPtr<const client::GraphModel> graph);
+
+ void process_toggled();
+ void poly_changed();
+ void clear_clicked();
+
+ void property_changed(const URI& predicate, const Atom& value);
+
+ App* _app;
+
+ SPtr<const client::GraphModel> _graph;
+ SPtr<GraphCanvas> _canvas;
+
+ Gtk::ScrolledWindow* _canvas_scrolledwindow;
+ Gtk::Toolbar* _toolbar;
+ Gtk::ToggleToolButton* _process_but;
+ Gtk::SpinButton* _poly_spin;
+ Gtk::ToolItem* _breadcrumb_container;
+
+ bool _enable_signal;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_GRAPHVIEW_HPP
diff --git a/src/gui/GraphWindow.cpp b/src/gui/GraphWindow.cpp
new file mode 100644
index 00000000..086886ef
--- /dev/null
+++ b/src/gui/GraphWindow.cpp
@@ -0,0 +1,85 @@
+/*
+ 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/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+
+#include "App.hpp"
+#include "GraphCanvas.hpp"
+#include "GraphView.hpp"
+#include "GraphWindow.hpp"
+#include "WindowFactory.hpp"
+
+namespace ingen {
+namespace gui {
+
+GraphWindow::GraphWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Window(cobject)
+ , _box(nullptr)
+ , _position_stored(false)
+ , _x(0)
+ , _y(0)
+{
+ property_visible() = false;
+
+ xml->get_widget_derived("graph_win_vbox", _box);
+
+ set_title("Ingen");
+}
+
+GraphWindow::~GraphWindow()
+{
+ delete _box;
+}
+
+void
+GraphWindow::init_window(App& app)
+{
+ Window::init_window(app);
+ _box->init_box(app);
+ _box->set_window(this);
+}
+
+void
+GraphWindow::on_show()
+{
+ if (_position_stored) {
+ move(_x, _y);
+ }
+
+ Gtk::Window::on_show();
+
+ _box->view()->canvas()->widget().grab_focus();
+}
+
+void
+GraphWindow::on_hide()
+{
+ _position_stored = true;
+ get_position(_x, _y);
+ Gtk::Window::on_hide();
+}
+
+bool
+GraphWindow::on_key_press_event(GdkEventKey* event)
+{
+ // Disable Window C-w handling so quit works correctly
+ return Gtk::Window::on_key_press_event(event);
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/GraphWindow.hpp b/src/gui/GraphWindow.hpp
new file mode 100644
index 00000000..e3d30d4c
--- /dev/null
+++ b/src/gui/GraphWindow.hpp
@@ -0,0 +1,80 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_GRAPH_WINDOW_HPP
+#define INGEN_GUI_GRAPH_WINDOW_HPP
+
+#include "GraphBox.hpp"
+#include "Window.hpp"
+
+#include "ingen/types.hpp"
+
+#include <gtkmm/builder.h>
+
+#include <string>
+
+namespace ingen {
+
+namespace client {
+class GraphModel;
+}
+
+namespace gui {
+
+/** A window for a graph.
+ *
+ * \ingroup GUI
+ */
+class GraphWindow : public Window
+{
+public:
+ GraphWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ ~GraphWindow();
+
+ void init_window(App& app) override;
+
+ SPtr<const client::GraphModel> graph() const { return _box->graph(); }
+ GraphBox* box() const { return _box; }
+
+ bool documentation_is_visible() { return _box->documentation_is_visible(); }
+
+ void set_documentation(const std::string& doc, bool html) {
+ _box->set_documentation(doc, html);
+ }
+
+ void show_port_status(const client::PortModel* model,
+ const Atom& value) {
+ _box->show_port_status(model, value);
+ }
+
+protected:
+ void on_hide() override;
+ void on_show() override;
+ bool on_key_press_event(GdkEventKey* event) override;
+
+private:
+ GraphBox* _box;
+ bool _position_stored;
+ int _x;
+ int _y;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_GRAPH_WINDOW_HPP
diff --git a/src/gui/LoadGraphWindow.cpp b/src/gui/LoadGraphWindow.cpp
new file mode 100644
index 00000000..fb1dc9ff
--- /dev/null
+++ b/src/gui/LoadGraphWindow.cpp
@@ -0,0 +1,259 @@
+/*
+ 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 "LoadGraphWindow.hpp"
+
+#include "App.hpp"
+#include "GraphView.hpp"
+#include "Style.hpp"
+#include "ThreadedLoader.hpp"
+
+#include "ingen/Configuration.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/runtime_paths.hpp"
+
+#include <boost/optional/optional.hpp>
+#include <glibmm/miscutils.h>
+
+#include <list>
+#include <ostream>
+#include <string>
+#include <utility>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+LoadGraphWindow::LoadGraphWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Gtk::FileChooserDialog(cobject)
+ , _app(nullptr)
+ , _merge_ports(false)
+{
+ xml->get_widget("load_graph_symbol_label", _symbol_label);
+ xml->get_widget("load_graph_symbol_entry", _symbol_entry);
+ xml->get_widget("load_graph_ports_label", _ports_label);
+ xml->get_widget("load_graph_merge_ports_radio", _merge_ports_radio);
+ xml->get_widget("load_graph_insert_ports_radio", _insert_ports_radio);
+ xml->get_widget("load_graph_poly_voices_radio", _poly_voices_radio);
+ xml->get_widget("load_graph_poly_from_file_radio", _poly_from_file_radio);
+ xml->get_widget("load_graph_poly_spinbutton", _poly_spinbutton);
+ xml->get_widget("load_graph_ok_button", _ok_button);
+ xml->get_widget("load_graph_cancel_button", _cancel_button);
+
+ _cancel_button->signal_clicked().connect(
+ sigc::mem_fun(this, &LoadGraphWindow::cancel_clicked));
+ _ok_button->signal_clicked().connect(
+ sigc::mem_fun(this, &LoadGraphWindow::ok_clicked));
+ _merge_ports_radio->signal_toggled().connect(
+ sigc::mem_fun(this, &LoadGraphWindow::merge_ports_selected));
+ _insert_ports_radio->signal_toggled().connect(
+ sigc::mem_fun(this, &LoadGraphWindow::insert_ports_selected));
+ _poly_from_file_radio->signal_toggled().connect(
+ sigc::bind(sigc::mem_fun(_poly_spinbutton, &Gtk::SpinButton::set_sensitive),
+ false));
+ _poly_voices_radio->signal_toggled().connect(
+ sigc::bind(sigc::mem_fun(_poly_spinbutton, &Gtk::SpinButton::set_sensitive),
+ true));
+
+ signal_selection_changed().connect(
+ sigc::mem_fun(this, &LoadGraphWindow::selection_changed));
+
+ Gtk::FileFilter file_filter;
+ file_filter.add_pattern("*.ttl");
+ file_filter.set_name("Ingen graph files (*.ttl)");
+ add_filter(file_filter);
+
+ Gtk::FileFilter bundle_filter;
+ bundle_filter.add_pattern("*.ingen");
+ bundle_filter.set_name("Ingen bundles (*.ingen)");
+ add_filter(bundle_filter);
+
+ property_select_multiple() = true;
+
+ // Add global examples directory to "shortcut folders" (bookmarks)
+ const FilePath examples_dir = ingen::data_file_path("graphs");
+ if (Glib::file_test(examples_dir, Glib::FILE_TEST_IS_DIR)) {
+ add_shortcut_folder(examples_dir.string());
+ }
+}
+
+void
+LoadGraphWindow::present(SPtr<const GraphModel> graph,
+ bool import,
+ Properties data)
+{
+ _import = import;
+ set_graph(graph);
+ _symbol_label->property_visible() = !import;
+ _symbol_entry->property_visible() = !import;
+ _ports_label->property_visible() = _import;
+ _merge_ports_radio->property_visible() = _import;
+ _insert_ports_radio->property_visible() = _import;
+ _initial_data = data;
+ Gtk::Window::present();
+}
+
+/** Sets the graph model for this window and initializes everything.
+ *
+ * This function MUST be called before using the window in any way!
+ */
+void
+LoadGraphWindow::set_graph(SPtr<const GraphModel> graph)
+{
+ _graph = graph;
+ _symbol_entry->set_text("");
+ _symbol_entry->set_sensitive(!_import);
+ _poly_spinbutton->set_value(graph->internal_poly());
+}
+
+void
+LoadGraphWindow::on_show()
+{
+ const Atom& dir = _app->world().conf().option("graph-directory");
+ if (dir.is_valid()) {
+ set_current_folder(dir.ptr<char>());
+ }
+ Gtk::FileChooserDialog::on_show();
+}
+
+void
+LoadGraphWindow::merge_ports_selected()
+{
+ _merge_ports = true;
+}
+
+void
+LoadGraphWindow::insert_ports_selected()
+{
+ _merge_ports = false;
+}
+
+void
+LoadGraphWindow::ok_clicked()
+{
+ if (!_graph) {
+ hide();
+ return;
+ }
+
+ const URIs& uris = _app->uris();
+
+ if (_poly_voices_radio->get_active()) {
+ _initial_data.emplace(
+ uris.ingen_polyphony,
+ _app->forge().make(_poly_spinbutton->get_value_as_int()));
+ }
+
+ if (get_uri() == "") {
+ return;
+ }
+
+ if (_import) {
+ // If unset load_graph will load value
+ boost::optional<Raul::Path> parent;
+ boost::optional<Raul::Symbol> symbol;
+ if (!_graph->path().is_root()) {
+ parent = _graph->path().parent();
+ symbol = _graph->symbol();
+ }
+
+ _app->loader()->load_graph(
+ true, FilePath(get_filename()), parent, symbol, _initial_data);
+
+ } else {
+ std::list<Glib::ustring> uri_list = get_filenames();
+ for (auto u : uri_list) {
+ // Cascade
+ Atom& x = _initial_data.find(uris.ingen_canvasX)->second;
+ x = _app->forge().make(x.get<float>() + 20.0f);
+ Atom& y = _initial_data.find(uris.ingen_canvasY)->second;
+ y = _app->forge().make(y.get<float>() + 20.0f);
+
+ Raul::Symbol symbol(symbol_from_filename(u));
+ if (uri_list.size() == 1 && _symbol_entry->get_text() != "") {
+ symbol = Raul::Symbol::symbolify(_symbol_entry->get_text());
+ }
+
+ symbol = avoid_symbol_clash(symbol);
+
+ _app->loader()->load_graph(
+ false, FilePath(URI(u).path()), _graph->path(), symbol, _initial_data);
+ }
+ }
+
+ _graph.reset();
+ hide();
+
+ _app->world().conf().set(
+ "graph-directory",
+ _app->world().forge().alloc(get_current_folder()));
+}
+
+void
+LoadGraphWindow::cancel_clicked()
+{
+ _graph.reset();
+ hide();
+}
+
+Raul::Symbol
+LoadGraphWindow::symbol_from_filename(const Glib::ustring& filename)
+{
+ std::string symbol_str = Glib::path_get_basename(get_filename());
+ symbol_str = symbol_str.substr(0, symbol_str.find('.'));
+ return Raul::Symbol::symbolify(symbol_str);
+}
+
+Raul::Symbol
+LoadGraphWindow::avoid_symbol_clash(const Raul::Symbol& symbol)
+{
+ unsigned offset = _app->store()->child_name_offset(
+ _graph->path(), symbol);
+
+ if (offset != 0) {
+ std::stringstream ss;
+ ss << symbol << "_" << offset;
+ return Raul::Symbol(ss.str());
+ } else {
+ return symbol;
+ }
+}
+
+void
+LoadGraphWindow::selection_changed()
+{
+ if (_import) {
+ return;
+ }
+
+ if (get_filenames().size() != 1) {
+ _symbol_entry->set_text("");
+ _symbol_entry->set_sensitive(false);
+ } else {
+ _symbol_entry->set_text(
+ avoid_symbol_clash(symbol_from_filename(get_filename())).c_str());
+ _symbol_entry->set_sensitive(true);
+ }
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/LoadGraphWindow.hpp b/src/gui/LoadGraphWindow.hpp
new file mode 100644
index 00000000..32d435ad
--- /dev/null
+++ b/src/gui/LoadGraphWindow.hpp
@@ -0,0 +1,97 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_LOADGRAPHWINDOW_HPP
+#define INGEN_GUI_LOADGRAPHWINDOW_HPP
+
+#include "ingen/Node.hpp"
+#include "ingen/types.hpp"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/filechooserdialog.h>
+#include <gtkmm/label.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/spinbutton.h>
+
+namespace ingen {
+
+namespace client { class GraphModel; }
+
+namespace gui {
+
+class App;
+
+/** 'Load Graph' Window.
+ *
+ * Loaded from XML as a derived object.
+ *
+ * \ingroup GUI
+ */
+class LoadGraphWindow : public Gtk::FileChooserDialog
+{
+public:
+ LoadGraphWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ void init(App& app) { _app = &app; }
+
+ void set_graph(SPtr<const client::GraphModel> graph);
+
+ void present(SPtr<const client::GraphModel> graph,
+ bool import,
+ Properties data);
+
+protected:
+ void on_show() override;
+
+private:
+ void merge_ports_selected();
+ void insert_ports_selected();
+
+ void selection_changed();
+ void cancel_clicked();
+ void ok_clicked();
+
+ Raul::Symbol symbol_from_filename(const Glib::ustring& filename);
+ Raul::Symbol avoid_symbol_clash(const Raul::Symbol& symbol);
+
+ App* _app;
+
+ Properties _initial_data;
+
+ SPtr<const client::GraphModel> _graph;
+
+ Gtk::Label* _symbol_label;
+ Gtk::Entry* _symbol_entry;
+ Gtk::Label* _ports_label;
+ Gtk::RadioButton* _merge_ports_radio;
+ Gtk::RadioButton* _insert_ports_radio;
+ Gtk::RadioButton* _poly_voices_radio;
+ Gtk::RadioButton* _poly_from_file_radio;
+ Gtk::SpinButton* _poly_spinbutton;
+ Gtk::Button* _ok_button;
+ Gtk::Button* _cancel_button;
+
+ bool _import;
+ bool _merge_ports;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_LOADGRAPHWINDOW_HPP
diff --git a/src/gui/LoadPluginWindow.cpp b/src/gui/LoadPluginWindow.cpp
new file mode 100644
index 00000000..bb84f96f
--- /dev/null
+++ b/src/gui/LoadPluginWindow.cpp
@@ -0,0 +1,508 @@
+/*
+ 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 "App.hpp"
+#include "GraphCanvas.hpp"
+#include "GraphView.hpp"
+#include "GraphWindow.hpp"
+#include "LoadPluginWindow.hpp"
+#include "ingen_config.h"
+
+#include "ingen/Interface.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+
+#include <string>
+#include <cstddef>
+#include <cassert>
+#include <algorithm>
+
+using std::string;
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+LoadPluginWindow::LoadPluginWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Window(cobject)
+ , _name_offset(0)
+ , _has_shown(false)
+ , _refresh_list(true)
+{
+ xml->get_widget("load_plugin_plugins_treeview", _plugins_treeview);
+ xml->get_widget("load_plugin_polyphonic_checkbutton", _polyphonic_checkbutton);
+ xml->get_widget("load_plugin_name_entry", _name_entry);
+ xml->get_widget("load_plugin_add_button", _add_button);
+ xml->get_widget("load_plugin_close_button", _close_button);
+
+ xml->get_widget("load_plugin_filter_combo", _filter_combo);
+ xml->get_widget("load_plugin_search_entry", _search_entry);
+
+ // Set up the plugins list
+ _plugins_liststore = Gtk::ListStore::create(_plugins_columns);
+ _plugins_treeview->set_model(_plugins_liststore);
+ _plugins_treeview->append_column("_Name", _plugins_columns._col_name);
+ _plugins_treeview->append_column("_Type", _plugins_columns._col_type);
+ _plugins_treeview->append_column("_Project", _plugins_columns._col_project);
+ _plugins_treeview->append_column("_Author", _plugins_columns._col_author);
+ _plugins_treeview->append_column("_URI", _plugins_columns._col_uri);
+
+ // This could be nicer.. store the TreeViewColumns locally maybe?
+ _plugins_treeview->get_column(0)->set_sort_column(_plugins_columns._col_name);
+ _plugins_treeview->get_column(1)->set_sort_column(_plugins_columns._col_type);
+ _plugins_treeview->get_column(2)->set_sort_column(_plugins_columns._col_project);
+ _plugins_treeview->get_column(2)->set_sort_column(_plugins_columns._col_author);
+ _plugins_treeview->get_column(3)->set_sort_column(_plugins_columns._col_uri);
+ for (int i = 0; i < 5; ++i) {
+ _plugins_treeview->get_column(i)->set_resizable(true);
+ }
+
+ // Set up the search criteria combobox
+ _criteria_liststore = Gtk::ListStore::create(_criteria_columns);
+ _filter_combo->set_model(_criteria_liststore);
+
+ Gtk::TreeModel::iterator iter = _criteria_liststore->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[_criteria_columns._col_label] = "Name contains";
+ row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::NAME;
+ _filter_combo->set_active(iter);
+
+ row = *(iter = _criteria_liststore->append());
+ row[_criteria_columns._col_label] = "Type contains";
+ row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::TYPE;
+
+ row = *(iter = _criteria_liststore->append());
+ row[_criteria_columns._col_label] = "Project contains";
+ row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::PROJECT;
+
+ row = *(iter = _criteria_liststore->append());
+ row[_criteria_columns._col_label] = "Author contains";
+ row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::AUTHOR;
+
+ row = *(iter = _criteria_liststore->append());
+ row[_criteria_columns._col_label] = "URI contains";
+ row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::URI;
+ _filter_combo->pack_start(_criteria_columns._col_label);
+
+ _add_button->signal_clicked().connect(
+ sigc::mem_fun(this, &LoadPluginWindow::add_clicked));
+ _close_button->signal_clicked().connect(
+ sigc::mem_fun(this, &Window::hide));
+ _plugins_treeview->signal_row_activated().connect(
+ sigc::mem_fun(this, &LoadPluginWindow::plugin_activated));
+ _search_entry->signal_activate().connect(
+ sigc::mem_fun(this, &LoadPluginWindow::add_clicked));
+ _search_entry->signal_changed().connect(
+ sigc::mem_fun(this, &LoadPluginWindow::filter_changed));
+ _name_entry->signal_changed().connect(
+ sigc::mem_fun(this, &LoadPluginWindow::name_changed));
+
+ _search_entry->signal_icon_release().connect(
+ sigc::mem_fun(this, &LoadPluginWindow::name_cleared));
+
+ _selection = _plugins_treeview->get_selection();
+ _selection->set_mode(Gtk::SELECTION_MULTIPLE);
+ _selection->signal_changed().connect(
+ sigc::mem_fun(this, &LoadPluginWindow::plugin_selection_changed));
+
+ //m_add_button->grab_default();
+}
+
+void
+LoadPluginWindow::present(SPtr<const GraphModel> graph,
+ Properties data)
+{
+ set_graph(graph);
+ _initial_data = data;
+ Gtk::Window::present();
+}
+
+/** Called every time the user types into the name input box.
+ * Used to display warning messages, and enable/disable the OK button.
+ */
+void
+LoadPluginWindow::name_changed()
+{
+ // Toggle add button sensitivity according name legality
+ if (_selection->get_selected_rows().size() == 1) {
+ const string sym = _name_entry->get_text();
+ if (!Raul::Symbol::is_valid(sym)) {
+ _add_button->property_sensitive() = false;
+ } else if (_app->store()->find(_graph->path().child(Raul::Symbol(sym)))
+ != _app->store()->end()) {
+ _add_button->property_sensitive() = false;
+ } else {
+ _add_button->property_sensitive() = true;
+ }
+ }
+}
+
+void
+LoadPluginWindow::name_cleared(Gtk::EntryIconPosition pos, const GdkEventButton* event)
+{
+ _search_entry->set_text("");
+}
+
+/** Sets the graph controller for this window and initializes everything.
+ *
+ * This function MUST be called before using the window in any way!
+ */
+void
+LoadPluginWindow::set_graph(SPtr<const GraphModel> graph)
+{
+ if (_graph) {
+ _graph = graph;
+ plugin_selection_changed();
+ } else {
+ _graph = graph;
+ }
+}
+
+/** Populates the plugin list on the first show.
+ *
+ * This is done here instead of construction time as the list population is
+ * really expensive and bogs down creation of a graph. This is especially
+ * important when many graph notifications are sent at one time from the
+ * engine.
+ */
+void
+LoadPluginWindow::on_show()
+{
+ if (!_has_shown) {
+ _app->store()->signal_new_plugin().connect(
+ sigc::mem_fun(this, &LoadPluginWindow::add_plugin));
+ _has_shown = true;
+ }
+
+ if (_refresh_list) {
+ set_plugins(_app->store()->plugins());
+ _refresh_list = false;
+ }
+
+ Gtk::Window::on_show();
+}
+
+void
+LoadPluginWindow::set_plugins(SPtr<const ClientStore::Plugins> plugins)
+{
+ _rows.clear();
+ _plugins_liststore->clear();
+
+ for (const auto& p : *plugins.get()) {
+ add_plugin(p.second);
+ }
+
+ _plugins_liststore->set_sort_column(1, Gtk::SORT_ASCENDING);
+ _plugins_treeview->columns_autosize();
+}
+
+void
+LoadPluginWindow::new_plugin(SPtr<const PluginModel> pm)
+{
+ if (is_visible()) {
+ add_plugin(pm);
+ } else {
+ _refresh_list = true;
+ }
+}
+
+static std::string
+get_project_name(SPtr<const PluginModel> plugin)
+{
+ std::string name;
+ if (plugin->lilv_plugin()) {
+ LilvNode* project = lilv_plugin_get_project(plugin->lilv_plugin());
+ if (!project) {
+ return "";
+ }
+
+ LilvNode* doap_name = lilv_new_uri(
+ plugin->lilv_world(), "http://usefulinc.com/ns/doap#name");
+ LilvNodes* names = lilv_world_find_nodes(
+ plugin->lilv_world(), project, doap_name, nullptr);
+
+ if (names) {
+ name = lilv_node_as_string(lilv_nodes_get_first(names));
+ }
+
+ lilv_nodes_free(names);
+ lilv_node_free(doap_name);
+ lilv_node_free(project);
+ }
+ return name;
+}
+
+static std::string
+get_author_name(SPtr<const PluginModel> plugin)
+{
+ std::string name;
+ if (plugin->lilv_plugin()) {
+ LilvNode* author = lilv_plugin_get_author_name(plugin->lilv_plugin());
+ if (author) {
+ name = lilv_node_as_string(author);
+ }
+ lilv_node_free(author);
+ }
+ return name;
+}
+
+void
+LoadPluginWindow::set_row(Gtk::TreeModel::Row& row,
+ SPtr<const PluginModel> plugin)
+{
+ const URIs& uris = _app->uris();
+ const Atom& name = plugin->get_property(uris.doap_name);
+ if (name.is_valid() && name.type() == uris.forge.String) {
+ row[_plugins_columns._col_name] = name.ptr<char>();
+ }
+
+ if (uris.lv2_Plugin == plugin->type()) {
+ row[_plugins_columns._col_type] = lilv_node_as_string(
+ lilv_plugin_class_get_label(
+ lilv_plugin_get_class(plugin->lilv_plugin())));
+
+ row[_plugins_columns._col_project] = get_project_name(plugin);
+ row[_plugins_columns._col_author] = get_author_name(plugin);
+ } else if (uris.ingen_Internal == plugin->type()) {
+ row[_plugins_columns._col_type] = "Internal";
+ row[_plugins_columns._col_project] = "Ingen";
+ row[_plugins_columns._col_author] = "David Robillard";
+ } else if (uris.ingen_Graph == plugin->type()) {
+ row[_plugins_columns._col_type] = "Graph";
+ } else {
+ row[_plugins_columns._col_type] = "";
+ }
+
+ row[_plugins_columns._col_uri] = plugin->uri().string();
+ row[_plugins_columns._col_plugin] = plugin;
+}
+
+void
+LoadPluginWindow::add_plugin(SPtr<const PluginModel> plugin)
+{
+ if (plugin->lilv_plugin() && lilv_plugin_is_replaced(plugin->lilv_plugin())) {
+ return;
+ }
+
+ Gtk::TreeModel::iterator iter = _plugins_liststore->append();
+ Gtk::TreeModel::Row row = *iter;
+ _rows.emplace(plugin->uri(), iter);
+
+ set_row(row, plugin);
+
+ plugin->signal_property().connect(
+ sigc::bind<0>(sigc::mem_fun(this, &LoadPluginWindow::plugin_property_changed),
+ plugin->uri()));
+}
+
+///// Event Handlers //////
+
+void
+LoadPluginWindow::plugin_activated(const Gtk::TreeModel::Path& path,
+ Gtk::TreeViewColumn* col)
+{
+ add_clicked();
+}
+
+void
+LoadPluginWindow::plugin_selection_changed()
+{
+ size_t n_selected = _selection->get_selected_rows().size();
+ if (n_selected == 0) {
+ _name_offset = 0;
+ _name_entry->set_text("");
+ _name_entry->set_sensitive(false);
+ } else if (n_selected == 1) {
+ Gtk::TreeModel::iterator iter = _plugins_liststore->get_iter(
+ *_selection->get_selected_rows().begin());
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ SPtr<const PluginModel> p = row.get_value(
+ _plugins_columns._col_plugin);
+ _name_offset = _app->store()->child_name_offset(
+ _graph->path(), p->default_block_symbol());
+ _name_entry->set_text(generate_module_name(p, _name_offset));
+ _name_entry->set_sensitive(true);
+ } else {
+ _name_offset = 0;
+ _name_entry->set_text("");
+ _name_entry->set_sensitive(false);
+ }
+ } else {
+ _name_entry->set_text("");
+ _name_entry->set_sensitive(false);
+ }
+}
+
+/** Generate an automatic name for this Node.
+ *
+ * Offset is an offset of the number that will be appended to the plugin's
+ * label, needed if the user adds multiple plugins faster than the engine
+ * sends the notification back.
+ */
+string
+LoadPluginWindow::generate_module_name(SPtr<const PluginModel> plugin,
+ int offset)
+{
+ std::stringstream ss;
+ ss << plugin->default_block_symbol();
+ if (offset != 0) {
+ ss << "_" << offset;
+ }
+ return ss.str();
+}
+
+void
+LoadPluginWindow::load_plugin(const Gtk::TreeModel::iterator& iter)
+{
+ const URIs& uris = _app->uris();
+ Gtk::TreeModel::Row row = *iter;
+ SPtr<const PluginModel> plugin = row.get_value(_plugins_columns._col_plugin);
+ bool polyphonic = _polyphonic_checkbutton->get_active();
+ string name = _name_entry->get_text();
+
+ if (name.empty()) {
+ name = generate_module_name(plugin, _name_offset);
+ }
+
+ if (name.empty() || !Raul::Symbol::is_valid(name)) {
+ Gtk::MessageDialog dialog(
+ *this,
+ "Unable to choose a default name, please provide one",
+ false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
+
+ dialog.run();
+ } else {
+ Raul::Path path = _graph->path().child(Raul::Symbol::symbolify(name));
+ Properties props = _initial_data;
+ props.emplace(uris.rdf_type, Property(uris.ingen_Block));
+ props.emplace(uris.lv2_prototype, _app->forge().make_urid(plugin->uri()));
+ props.emplace(uris.ingen_polyphonic, _app->forge().make(polyphonic));
+ _app->interface()->put(path_to_uri(path), props);
+
+ if (_selection->get_selected_rows().size() == 1) {
+ _name_offset = (_name_offset == 0) ? 2 : _name_offset + 1;
+ _name_entry->set_text(generate_module_name(plugin, _name_offset));
+ }
+
+ // Cascade next block
+ Atom& x = _initial_data.find(uris.ingen_canvasX)->second;
+ x = _app->forge().make(x.get<float>() + 20.0f);
+ Atom& y = _initial_data.find(uris.ingen_canvasY)->second;
+ y = _app->forge().make(y.get<float>() + 20.0f);
+ }
+}
+
+void
+LoadPluginWindow::add_clicked()
+{
+ _selection->selected_foreach_iter(
+ sigc::mem_fun(*this, &LoadPluginWindow::load_plugin));
+}
+
+void
+LoadPluginWindow::filter_changed()
+{
+ _rows.clear();
+ _plugins_liststore->clear();
+ string search = _search_entry->get_text();
+ transform(search.begin(), search.end(), search.begin(), ::toupper);
+
+ // Get selected criteria
+ const Gtk::TreeModel::Row row = *(_filter_combo->get_active());
+ CriteriaColumns::Criteria criteria = row[_criteria_columns._col_criteria];
+
+ string field;
+
+ Gtk::TreeModel::Row model_row;
+ Gtk::TreeModel::iterator model_iter;
+ size_t num_visible = 0;
+ const URIs& uris = _app->uris();
+
+ for (const auto& p : *_app->store()->plugins().get()) {
+ const SPtr<PluginModel> plugin = p.second;
+ const Atom& name = plugin->get_property(uris.doap_name);
+
+ switch (criteria) {
+ case CriteriaColumns::Criteria::NAME:
+ if (name.is_valid() && name.type() == uris.forge.String) {
+ field = name.ptr<char>();
+ }
+ break;
+ case CriteriaColumns::Criteria::TYPE:
+ if (plugin->lilv_plugin()) {
+ field = lilv_node_as_string(
+ lilv_plugin_class_get_label(
+ lilv_plugin_get_class(plugin->lilv_plugin())));
+ }
+ break;
+ case CriteriaColumns::Criteria::PROJECT:
+ field = get_project_name(plugin);
+ break;
+ case CriteriaColumns::Criteria::AUTHOR:
+ field = get_author_name(plugin);
+ break;
+ case CriteriaColumns::Criteria::URI:
+ field = plugin->uri();
+ break;
+ }
+
+ transform(field.begin(), field.end(), field.begin(), ::toupper);
+
+ if (field.find(search) != string::npos) {
+ model_iter = _plugins_liststore->append();
+ model_row = *model_iter;
+ set_row(model_row, plugin);
+ ++num_visible;
+ }
+ }
+
+ if (num_visible == 1) {
+ _selection->unselect_all();
+ _selection->select(model_iter);
+ }
+}
+
+bool
+LoadPluginWindow::on_key_press_event(GdkEventKey* event)
+{
+ if (event->keyval == GDK_w && event->state & GDK_CONTROL_MASK) {
+ hide();
+ return true;
+ } else {
+ return Gtk::Window::on_key_press_event(event);
+ }
+}
+
+void
+LoadPluginWindow::plugin_property_changed(const URI& plugin,
+ const URI& predicate,
+ const Atom& value)
+{
+ const URIs& uris = _app->uris();
+ if (predicate == uris.doap_name) {
+ Rows::const_iterator i = _rows.find(plugin);
+ if (i != _rows.end() && value.type() == uris.forge.String) {
+ (*i->second)[_plugins_columns._col_name] = value.ptr<char>();
+ }
+ }
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/LoadPluginWindow.hpp b/src/gui/LoadPluginWindow.hpp
new file mode 100644
index 00000000..ddd0b427
--- /dev/null
+++ b/src/gui/LoadPluginWindow.hpp
@@ -0,0 +1,160 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_LOADPLUGINWINDOW_HPP
+#define INGEN_GUI_LOADPLUGINWINDOW_HPP
+
+#include "Window.hpp"
+
+#include "ingen/Node.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/types.hpp"
+#include "ingen_config.h"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treeview.h>
+
+#include <map>
+#include <string>
+
+namespace ingen {
+
+namespace client {
+class GraphModel;
+class PluginModel;
+}
+
+namespace gui {
+
+/** 'Load Plugin' window.
+ *
+ * Loaded from XML as a derived object.
+ *
+ * \ingroup GUI
+ */
+class LoadPluginWindow : public Window
+{
+public:
+ LoadPluginWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ void set_graph(SPtr<const client::GraphModel> graph);
+ void set_plugins(SPtr<const client::ClientStore::Plugins> plugins);
+
+ void add_plugin(SPtr<const client::PluginModel> plugin);
+
+ void present(SPtr<const client::GraphModel> graph,
+ Properties data);
+
+protected:
+ void on_show() override;
+ bool on_key_press_event(GdkEventKey* event) override;
+
+private:
+ /** Columns for the plugin list */
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ ModelColumns() {
+ add(_col_name);
+ add(_col_type);
+ add(_col_project);
+ add(_col_author);
+ add(_col_uri);
+ add(_col_plugin);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> _col_name;
+ Gtk::TreeModelColumn<Glib::ustring> _col_type;
+ Gtk::TreeModelColumn<Glib::ustring> _col_project;
+ Gtk::TreeModelColumn<Glib::ustring> _col_author;
+ Gtk::TreeModelColumn<Glib::ustring> _col_uri;
+
+ // Not displayed:
+ Gtk::TreeModelColumn< SPtr<const client::PluginModel> > _col_plugin;
+ };
+
+ /** Column for the filter criteria combo box. */
+ class CriteriaColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ enum class Criteria { NAME, TYPE, PROJECT, AUTHOR, URI, };
+
+ CriteriaColumns() {
+ add(_col_label);
+ add(_col_criteria);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> _col_label;
+ Gtk::TreeModelColumn<Criteria> _col_criteria;
+ };
+
+ void add_clicked();
+ void filter_changed();
+ void clear_clicked();
+ void name_changed();
+ void name_cleared(Gtk::EntryIconPosition pos, const GdkEventButton* event);
+
+ void set_row(Gtk::TreeModel::Row& row,
+ SPtr<const client::PluginModel> plugin);
+
+ void new_plugin(SPtr<const client::PluginModel> pm);
+
+ void plugin_property_changed(const URI& plugin,
+ const URI& predicate,
+ const Atom& value);
+
+ void plugin_activated(const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn* col);
+ void plugin_selection_changed();
+
+ std::string generate_module_name(SPtr<const client::PluginModel> plugin,
+ int offset=0);
+
+ void load_plugin(const Gtk::TreeModel::iterator& iter);
+
+ Properties _initial_data;
+
+ SPtr<const client::GraphModel> _graph;
+
+ typedef std::map<URI, Gtk::TreeModel::iterator> Rows;
+ Rows _rows;
+
+ Glib::RefPtr<Gtk::ListStore> _plugins_liststore;
+ ModelColumns _plugins_columns;
+
+ Glib::RefPtr<Gtk::ListStore> _criteria_liststore;
+ CriteriaColumns _criteria_columns;
+
+ Glib::RefPtr<Gtk::TreeSelection> _selection;
+
+ int _name_offset; // see comments for generate_plugin_name
+
+ bool _has_shown;
+ bool _refresh_list;
+ Gtk::TreeView* _plugins_treeview;
+ Gtk::CheckButton* _polyphonic_checkbutton;
+ Gtk::Entry* _name_entry;
+ Gtk::Button* _close_button;
+ Gtk::Button* _add_button;
+ Gtk::ComboBox* _filter_combo;
+ Gtk::Entry* _search_entry;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_LOADPLUGINWINDOW_HPP
diff --git a/src/gui/MessagesWindow.cpp b/src/gui/MessagesWindow.cpp
new file mode 100644
index 00000000..84d29679
--- /dev/null
+++ b/src/gui/MessagesWindow.cpp
@@ -0,0 +1,145 @@
+/*
+ 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/>.
+*/
+
+#include "MessagesWindow.hpp"
+
+#include "App.hpp"
+
+#include "ingen/URIs.hpp"
+
+#include <cstdio>
+#include <cstdlib>
+#include <string>
+#include <utility>
+
+namespace ingen {
+namespace gui {
+using std::string;
+
+MessagesWindow::MessagesWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Window(cobject)
+{
+ xml->get_widget("messages_textview", _textview);
+ xml->get_widget("messages_clear_button", _clear_button);
+ xml->get_widget("messages_close_button", _close_button);
+
+ _clear_button->signal_clicked().connect(sigc::mem_fun(this, &MessagesWindow::clear_clicked));
+ _close_button->signal_clicked().connect(sigc::mem_fun(this, &Window::hide));
+
+ for (int s = Gtk::STATE_NORMAL; s <= Gtk::STATE_INSENSITIVE; ++s) {
+ _textview->modify_base((Gtk::StateType)s, Gdk::Color("#000000"));
+ _textview->modify_text((Gtk::StateType)s, Gdk::Color("#EEEEEC"));
+ }
+}
+
+void
+MessagesWindow::init_window(App& app)
+{
+ Glib::RefPtr<Gtk::TextTag> tag = Gtk::TextTag::create();
+ tag->property_foreground() = "#EF2929";
+ _tags.emplace(app.uris().log_Error, tag);
+ _error_tag = tag;
+
+ tag = Gtk::TextTag::create();
+ tag->property_foreground() = "#FCAF3E";
+ _tags.emplace(app.uris().log_Warning, tag);
+
+ tag = Gtk::TextTag::create();
+ tag->property_foreground() = "#8AE234";
+ _tags.emplace(app.uris().log_Trace, tag);
+
+ for (const auto& t : _tags) {
+ _textview->get_buffer()->get_tag_table()->add(t.second);
+ }
+}
+
+void
+MessagesWindow::post_error(const string& msg)
+{
+ Glib::RefPtr<Gtk::TextBuffer> text_buf = _textview->get_buffer();
+ text_buf->insert_with_tag(text_buf->end(), msg, _error_tag);
+ text_buf->insert(text_buf->end(), "\n");
+
+ if (!_clear_button->is_sensitive()) {
+ _clear_button->set_sensitive(true);
+ }
+
+ set_urgency_hint(true);
+ if (!is_visible()) {
+ present();
+ }
+}
+
+int
+MessagesWindow::log(LV2_URID type, const char* fmt, va_list args)
+{
+ std::lock_guard<std::mutex> lock(_mutex);
+
+#ifdef HAVE_VASPRINTF
+ char* buf = nullptr;
+ const int len = vasprintf(&buf, fmt, args);
+#else
+ char* buf = g_strdup_vprintf(fmt, args);
+ const int len = strlen(buf);
+#endif
+
+ _stream << type << ' ' << buf << '\0';
+ free(buf);
+
+ return len;
+}
+
+void
+MessagesWindow::flush()
+{
+ while (true) {
+ LV2_URID type;
+ std::string line;
+ {
+ std::lock_guard<std::mutex> lock(_mutex);
+ if (!_stream.rdbuf()->in_avail()) {
+ return;
+ }
+ _stream >> type;
+ std::getline(_stream, line, '\0');
+ }
+
+ Glib::RefPtr<Gtk::TextBuffer> text_buf = _textview->get_buffer();
+
+ auto t = _tags.find(type);
+ if (t != _tags.end()) {
+ text_buf->insert_with_tag(text_buf->end(), line, t->second);
+ } else {
+ text_buf->insert(text_buf->end(), line);
+ }
+ }
+
+ if (!_clear_button->is_sensitive()) {
+ _clear_button->set_sensitive(true);
+ }
+}
+
+void
+MessagesWindow::clear_clicked()
+{
+ Glib::RefPtr<Gtk::TextBuffer> text_buf = _textview->get_buffer();
+ text_buf->erase(text_buf->begin(), text_buf->end());
+ _clear_button->set_sensitive(false);
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/MessagesWindow.hpp b/src/gui/MessagesWindow.hpp
new file mode 100644
index 00000000..add87455
--- /dev/null
+++ b/src/gui/MessagesWindow.hpp
@@ -0,0 +1,73 @@
+/*
+ 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_GUI_MESSAGESWINDOW_HPP
+#define INGEN_GUI_MESSAGESWINDOW_HPP
+
+#include "Window.hpp"
+
+#include "lv2/log/log.h"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/textview.h>
+
+#include <cstdarg>
+#include <map>
+#include <mutex>
+#include <sstream>
+#include <string>
+
+namespace ingen {
+namespace gui {
+
+/** Messages Window.
+ *
+ * Loaded from XML as a derived object.
+ * This is shown when errors occur (e.g. during graph loading).
+ *
+ * \ingroup GUI
+ */
+class MessagesWindow : public Window
+{
+public:
+ MessagesWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ void init_window(App& app) override;
+
+ int log(LV2_URID type, const char* fmt, va_list args);
+ void flush();
+
+ void post_error(const std::string& msg);
+
+private:
+ void clear_clicked();
+
+ std::mutex _mutex;
+ std::stringstream _stream;
+ Gtk::TextView* _textview;
+ Gtk::Button* _clear_button;
+ Gtk::Button* _close_button;
+
+ Glib::RefPtr<Gtk::TextTag> _error_tag;
+ std::map< LV2_URID, Glib::RefPtr<Gtk::TextTag> > _tags;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_MESSAGESWINDOW_HPP
diff --git a/src/gui/NewSubgraphWindow.cpp b/src/gui/NewSubgraphWindow.cpp
new file mode 100644
index 00000000..228bd91c
--- /dev/null
+++ b/src/gui/NewSubgraphWindow.cpp
@@ -0,0 +1,121 @@
+/*
+ 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 "NewSubgraphWindow.hpp"
+
+#include "App.hpp"
+#include "GraphView.hpp"
+
+#include "ingen/Interface.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+
+#include <cstdint>
+#include <string>
+
+namespace ingen {
+namespace gui {
+
+NewSubgraphWindow::NewSubgraphWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Window(cobject)
+{
+ xml->get_widget("new_subgraph_name_entry", _name_entry);
+ xml->get_widget("new_subgraph_message_label", _message_label);
+ xml->get_widget("new_subgraph_polyphony_spinbutton", _poly_spinbutton);
+ xml->get_widget("new_subgraph_ok_button", _ok_button);
+ xml->get_widget("new_subgraph_cancel_button", _cancel_button);
+
+ _name_entry->signal_changed().connect(sigc::mem_fun(this, &NewSubgraphWindow::name_changed));
+ _ok_button->signal_clicked().connect(sigc::mem_fun(this, &NewSubgraphWindow::ok_clicked));
+ _cancel_button->signal_clicked().connect(sigc::mem_fun(this, &NewSubgraphWindow::cancel_clicked));
+
+ _ok_button->property_sensitive() = false;
+
+ _poly_spinbutton->get_adjustment()->configure(1.0, 1.0, 128, 1.0, 10.0, 0);
+}
+
+void
+NewSubgraphWindow::present(SPtr<const client::GraphModel> graph,
+ Properties data)
+{
+ set_graph(graph);
+ _initial_data = data;
+ Gtk::Window::present();
+}
+
+/** Sets the graph controller for this window and initializes everything.
+ *
+ * This function MUST be called before using the window in any way!
+ */
+void
+NewSubgraphWindow::set_graph(SPtr<const client::GraphModel> graph)
+{
+ _graph = graph;
+}
+
+/** Called every time the user types into the name input box.
+ * Used to display warning messages, and enable/disable the OK button.
+ */
+void
+NewSubgraphWindow::name_changed()
+{
+ std::string name = _name_entry->get_text();
+ if (!Raul::Symbol::is_valid(name)) {
+ _message_label->set_text("Name contains invalid characters.");
+ _ok_button->property_sensitive() = false;
+ } else if (_app->store()->find(_graph->path().child(Raul::Symbol(name)))
+ != _app->store()->end()) {
+ _message_label->set_text("An object already exists with that name.");
+ _ok_button->property_sensitive() = false;
+ } else {
+ _message_label->set_text("");
+ _ok_button->property_sensitive() = true;
+ }
+}
+
+void
+NewSubgraphWindow::ok_clicked()
+{
+ const uint32_t poly = _poly_spinbutton->get_value_as_int();
+ const Raul::Path path = _graph->path().child(
+ Raul::Symbol::symbolify(_name_entry->get_text()));
+
+ // Create graph
+ Properties props;
+ props.emplace(_app->uris().rdf_type, Property(_app->uris().ingen_Graph));
+ props.emplace(_app->uris().ingen_polyphony, _app->forge().make(int32_t(poly)));
+ props.emplace(_app->uris().ingen_enabled, _app->forge().make(bool(true)));
+ _app->interface()->put(
+ path_to_uri(path), props, Resource::Graph::INTERNAL);
+
+ // Set external (block perspective) properties
+ props = _initial_data;
+ props.emplace(_app->uris().rdf_type, Property(_app->uris().ingen_Graph));
+ _app->interface()->put(
+ path_to_uri(path), _initial_data, Resource::Graph::EXTERNAL);
+
+ hide();
+}
+
+void
+NewSubgraphWindow::cancel_clicked()
+{
+ hide();
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/NewSubgraphWindow.hpp b/src/gui/NewSubgraphWindow.hpp
new file mode 100644
index 00000000..2d249cf3
--- /dev/null
+++ b/src/gui/NewSubgraphWindow.hpp
@@ -0,0 +1,72 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_NEWSUBGRAPHWINDOW_HPP
+#define INGEN_GUI_NEWSUBGRAPHWINDOW_HPP
+
+#include "Window.hpp"
+
+#include "ingen/Node.hpp"
+#include "ingen/types.hpp"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/label.h>
+#include <gtkmm/spinbutton.h>
+
+namespace ingen {
+
+namespace client { class GraphModel; }
+
+namespace gui {
+
+/** 'New Subgraph' window.
+ *
+ * Loaded from XML as a derived object.
+ *
+ * \ingroup GUI
+ */
+class NewSubgraphWindow : public Window
+{
+public:
+ NewSubgraphWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ void set_graph(SPtr<const client::GraphModel> graph);
+
+ void present(SPtr<const client::GraphModel> graph,
+ Properties data);
+
+private:
+ void name_changed();
+ void ok_clicked();
+ void cancel_clicked();
+
+ Properties _initial_data;
+ SPtr<const client::GraphModel> _graph;
+
+ Gtk::Entry* _name_entry;
+ Gtk::Label* _message_label;
+ Gtk::SpinButton* _poly_spinbutton;
+ Gtk::Button* _ok_button;
+ Gtk::Button* _cancel_button;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_NEWSUBGRAPHWINDOW_HPP
diff --git a/src/gui/NodeMenu.cpp b/src/gui/NodeMenu.cpp
new file mode 100644
index 00000000..e2478592
--- /dev/null
+++ b/src/gui/NodeMenu.cpp
@@ -0,0 +1,258 @@
+/*
+ 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 "NodeMenu.hpp"
+
+#include "App.hpp"
+#include "WidgetFactory.hpp"
+#include "WindowFactory.hpp"
+
+#include "ingen/Interface.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/URIMap.hpp"
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/client/PluginModel.hpp"
+#include "ingen/client/PortModel.hpp"
+#include "lv2/presets/presets.h"
+
+#include <glib.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/filechooserdialog.h>
+#include <gtkmm/image.h>
+#include <gtkmm/stock.h>
+
+#include <cstdint>
+#include <string>
+#include <utility>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+NodeMenu::NodeMenu(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : ObjectMenu(cobject, xml)
+ , _presets_menu(nullptr)
+{
+ xml->get_widget("node_popup_gui_menuitem", _popup_gui_menuitem);
+ xml->get_widget("node_embed_gui_menuitem", _embed_gui_menuitem);
+ xml->get_widget("node_enabled_menuitem", _enabled_menuitem);
+ xml->get_widget("node_randomize_menuitem", _randomize_menuitem);
+}
+
+void
+NodeMenu::init(App& app, SPtr<const client::BlockModel> block)
+{
+ ObjectMenu::init(app, block);
+
+ _learn_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &NodeMenu::on_menu_learn));
+ _popup_gui_menuitem->signal_activate().connect(
+ sigc::mem_fun(signal_popup_gui, &sigc::signal<void>::emit));
+ _embed_gui_menuitem->signal_toggled().connect(
+ sigc::mem_fun(this, &NodeMenu::on_menu_embed_gui));
+ _enabled_menuitem->signal_toggled().connect(
+ sigc::mem_fun(this, &NodeMenu::on_menu_enabled));
+ _randomize_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &NodeMenu::on_menu_randomize));
+
+ SPtr<PluginModel> plugin = block->plugin_model();
+ if (plugin) {
+ // Get the plugin to receive related presets
+ _preset_connection = plugin->signal_preset().connect(
+ sigc::mem_fun(this, &NodeMenu::add_preset));
+
+ if (!plugin->fetched()) {
+ _app->interface()->get(plugin->uri());
+ plugin->set_fetched(true);
+ }
+ }
+
+ if (plugin && plugin->has_ui()) {
+ _popup_gui_menuitem->show();
+ _embed_gui_menuitem->show();
+ const Atom& ui_embedded = block->get_property(
+ _app->uris().ingen_uiEmbedded);
+ _embed_gui_menuitem->set_active(
+ ui_embedded.is_valid() && ui_embedded.get<int32_t>());
+ } else {
+ _popup_gui_menuitem->hide();
+ _embed_gui_menuitem->hide();
+ }
+
+ const Atom& enabled = block->get_property(_app->uris().ingen_enabled);
+ _enabled_menuitem->set_active(!enabled.is_valid() || enabled.get<int32_t>());
+
+ if (plugin && _app->uris().lv2_Plugin == plugin->type()) {
+ _presets_menu = Gtk::manage(new Gtk::Menu());
+ _presets_menu->items().push_back(
+ Gtk::Menu_Helpers::MenuElem(
+ "_Save Preset...",
+ sigc::mem_fun(this, &NodeMenu::on_save_preset_activated)));
+ _presets_menu->items().push_back(Gtk::Menu_Helpers::SeparatorElem());
+
+ for (const auto& p : plugin->presets()) {
+ add_preset(p.first, p.second);
+ }
+
+ items().push_front(
+ Gtk::Menu_Helpers::ImageMenuElem(
+ "_Presets",
+ *(manage(new Gtk::Image(Gtk::Stock::INDEX, Gtk::ICON_SIZE_MENU)))));
+
+ Gtk::MenuItem* presets_menu_item = &(items().front());
+ presets_menu_item->set_submenu(*_presets_menu);
+ }
+
+ if (has_control_inputs()) {
+ _randomize_menuitem->show();
+ } else {
+ _randomize_menuitem->hide();
+ }
+
+ if (plugin && (plugin->uri() == "http://drobilla.net/ns/ingen-internals#Controller"
+ || plugin->uri() == "http://drobilla.net/ns/ingen-internals#Trigger")) {
+ _learn_menuitem->show();
+ } else {
+ _learn_menuitem->hide();
+ }
+
+ if (!_popup_gui_menuitem->is_visible() &&
+ !_embed_gui_menuitem->is_visible() &&
+ !_randomize_menuitem->is_visible()) {
+ _separator_menuitem->hide();
+ }
+
+ _enable_signal = true;
+}
+
+void
+NodeMenu::add_preset(const URI& uri, const std::string& label)
+{
+ if (_presets_menu) {
+ _presets_menu->items().push_back(
+ Gtk::Menu_Helpers::MenuElem(
+ label,
+ sigc::bind(sigc::mem_fun(this, &NodeMenu::on_preset_activated),
+ uri)));
+ }
+}
+
+void
+NodeMenu::on_menu_embed_gui()
+{
+ signal_embed_gui.emit(_embed_gui_menuitem->get_active());
+}
+
+void
+NodeMenu::on_menu_enabled()
+{
+ _app->set_property(_object->uri(),
+ _app->uris().ingen_enabled,
+ _app->forge().make(bool(_enabled_menuitem->get_active())));
+}
+
+void
+NodeMenu::on_menu_randomize()
+{
+ _app->interface()->bundle_begin();
+
+ const SPtr<const BlockModel> bm = block();
+ for (const auto& p : bm->ports()) {
+ if (p->is_input() && _app->can_control(p.get())) {
+ float min = 0.0f, max = 1.0f;
+ bm->port_value_range(p, min, max, _app->sample_rate());
+ const float val = g_random_double_range(0.0, 1.0) * (max - min) + min;
+ _app->set_property(p->uri(),
+ _app->uris().ingen_value,
+ _app->forge().make(val));
+ }
+ }
+
+ _app->interface()->bundle_end();
+}
+
+void
+NodeMenu::on_menu_disconnect()
+{
+ _app->interface()->disconnect_all(_object->parent()->path(), _object->path());
+}
+
+void
+NodeMenu::on_save_preset_activated()
+{
+ Gtk::FileChooserDialog dialog("Save Preset", Gtk::FILE_CHOOSER_ACTION_SAVE);
+ dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
+ dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK);
+ dialog.set_default_response(Gtk::RESPONSE_OK);
+ dialog.set_current_folder(Glib::build_filename(Glib::get_home_dir(), ".lv2"));
+
+ Gtk::HBox* extra = Gtk::manage(new Gtk::HBox());
+ Gtk::Label* label = Gtk::manage(new Gtk::Label("URI (Optional): "));
+ Gtk::Entry* entry = Gtk::manage(new Gtk::Entry());
+ extra->pack_start(*label, false, true, 4);
+ extra->pack_start(*entry, true, true, 4);
+ extra->show_all();
+ dialog.set_extra_widget(*Gtk::manage(extra));
+
+ if (dialog.run() == Gtk::RESPONSE_OK) {
+ const std::string user_uri = dialog.get_uri();
+ const std::string user_path = Glib::filename_from_uri(user_uri);
+ const std::string dirname = Glib::path_get_dirname(user_path);
+ const std::string basename = Glib::path_get_basename(user_path);
+ const std::string sym = Raul::Symbol::symbolify(basename);
+ const std::string plugname = block()->plugin_model()->human_name();
+ const std::string prefix = Raul::Symbol::symbolify(plugname);
+ const std::string bundle = prefix + "_" + sym + ".preset.lv2/";
+ const std::string file = sym + ".ttl";
+ const std::string real_path = Glib::build_filename(dirname, bundle, file);
+ const std::string real_uri = Glib::filename_to_uri(real_path);
+
+ Properties props{
+ { _app->uris().rdf_type,
+ _app->uris().pset_Preset },
+ { _app->uris().rdfs_label,
+ _app->forge().alloc(basename) },
+ { _app->uris().lv2_prototype,
+ _app->forge().make_urid(block()->uri()) }};
+ _app->interface()->put(URI(real_uri), props);
+ }
+}
+
+void
+NodeMenu::on_preset_activated(const std::string& uri)
+{
+ _app->set_property(block()->uri(),
+ _app->uris().pset_preset,
+ _app->forge().make_urid(URI(uri)));
+}
+
+bool
+NodeMenu::has_control_inputs()
+{
+ for (const auto& p : block()->ports()) {
+ if (p->is_input() && p->is_numeric()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/NodeMenu.hpp b/src/gui/NodeMenu.hpp
new file mode 100644
index 00000000..2a3268b4
--- /dev/null
+++ b/src/gui/NodeMenu.hpp
@@ -0,0 +1,76 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_NODEMENU_HPP
+#define INGEN_GUI_NODEMENU_HPP
+
+#include "ObjectMenu.hpp"
+
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/types.hpp"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menushell.h>
+
+#include <string>
+
+namespace ingen {
+namespace gui {
+
+/** Menu for a Node.
+ *
+ * \ingroup GUI
+ */
+class NodeMenu : public ObjectMenu
+{
+public:
+ NodeMenu(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ void init(App& app, SPtr<const client::BlockModel> block);
+
+ bool has_control_inputs();
+
+ sigc::signal<void> signal_popup_gui;
+ sigc::signal<void, bool> signal_embed_gui;
+
+protected:
+ SPtr<const client::BlockModel> block() const {
+ return dynamic_ptr_cast<const client::BlockModel>(_object);
+ }
+
+ void add_preset(const URI& uri, const std::string& label);
+
+ void on_menu_disconnect() override;
+ void on_menu_embed_gui();
+ void on_menu_enabled();
+ void on_menu_randomize();
+ void on_save_preset_activated();
+ void on_preset_activated(const std::string& uri);
+
+ Gtk::MenuItem* _popup_gui_menuitem;
+ Gtk::CheckMenuItem* _embed_gui_menuitem;
+ Gtk::CheckMenuItem* _enabled_menuitem;
+ Gtk::MenuItem* _randomize_menuitem;
+ Gtk::Menu* _presets_menu;
+ sigc::connection _preset_connection;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_NODEMENU_HPP
diff --git a/src/gui/NodeModule.cpp b/src/gui/NodeModule.cpp
new file mode 100644
index 00000000..b514b6ae
--- /dev/null
+++ b/src/gui/NodeModule.cpp
@@ -0,0 +1,518 @@
+/*
+ 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/>.
+*/
+
+#include "NodeModule.hpp"
+
+#include "App.hpp"
+#include "GraphCanvas.hpp"
+#include "GraphWindow.hpp"
+#include "NodeMenu.hpp"
+#include "Port.hpp"
+#include "RenameWindow.hpp"
+#include "Style.hpp"
+#include "SubgraphModule.hpp"
+#include "WidgetFactory.hpp"
+#include "WindowFactory.hpp"
+#include "ingen_config.h"
+
+#include "ingen/Atom.hpp"
+#include "ingen/Configuration.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/client/PluginModel.hpp"
+#include "ingen/client/PluginUI.hpp"
+#include "lv2/atom/util.h"
+
+#include <gtkmm/eventbox.h>
+
+#include <cassert>
+#include <string>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+NodeModule::NodeModule(GraphCanvas& canvas,
+ SPtr<const BlockModel> block)
+ : Ganv::Module(canvas, block->path().symbol(), 0, 0, true)
+ , _block(block)
+ , _gui_widget(nullptr)
+ , _gui_window(nullptr)
+ , _initialised(false)
+{
+ block->signal_new_port().connect(
+ sigc::mem_fun(this, &NodeModule::new_port_view));
+ block->signal_removed_port().connect(
+ sigc::hide_return(sigc::mem_fun(this, &NodeModule::delete_port_view)));
+ block->signal_property().connect(
+ sigc::mem_fun(this, &NodeModule::property_changed));
+ block->signal_moved().connect(
+ sigc::mem_fun(this, &NodeModule::rename));
+
+ signal_event().connect(
+ sigc::mem_fun(this, &NodeModule::on_event));
+
+ signal_moved().connect(
+ sigc::mem_fun(this, &NodeModule::store_location));
+
+ signal_selected().connect(
+ sigc::mem_fun(this, &NodeModule::on_selected));
+
+ const PluginModel* plugin = dynamic_cast<const PluginModel*>(block->plugin());
+ if (plugin) {
+ plugin->signal_changed().connect(
+ sigc::mem_fun(this, &NodeModule::plugin_changed));
+ }
+
+ for (const auto& p : block->properties()) {
+ property_changed(p.first, p.second);
+ }
+
+ if (_block->has_property(app().uris().ingen_uiEmbedded,
+ app().uris().forge.make(true))) {
+ // Schedule idle callback to embed GUI once ports arrive
+ Glib::signal_timeout().connect(
+ sigc::mem_fun(*this, &NodeModule::idle_init), 25, G_PRIORITY_DEFAULT_IDLE);
+ } else {
+ _initialised = true;
+ }
+}
+
+NodeModule::~NodeModule()
+{
+ delete _gui_widget;
+ delete _gui_window;
+}
+
+bool
+NodeModule::idle_init()
+{
+ if (_block->ports().size() == 0) {
+ return true; // Need to embed GUI, but ports haven't shown up yet
+ }
+
+ // Ports have arrived, embed GUI and deregister this callback
+ embed_gui(true);
+ _initialised = true;
+ return false;
+}
+
+bool
+NodeModule::show_menu(GdkEventButton* ev)
+{
+ WidgetFactory::get_widget_derived("object_menu", _menu);
+ if (!_menu) {
+ app().log().error("Failed to load object menu widget\n");
+ return false;
+ }
+
+ _menu->init(app(), _block);
+ _menu->signal_embed_gui.connect(
+ sigc::mem_fun(this, &NodeModule::on_embed_gui_toggled));
+ _menu->signal_popup_gui.connect(
+ sigc::hide_return(sigc::mem_fun(this, &NodeModule::popup_gui)));
+ _menu->popup(ev->button, ev->time);
+ return true;
+}
+
+NodeModule*
+NodeModule::create(GraphCanvas& canvas,
+ SPtr<const BlockModel> block,
+ bool human)
+{
+ SPtr<const GraphModel> graph = dynamic_ptr_cast<const GraphModel>(block);
+
+ NodeModule* ret = (graph)
+ ? new SubgraphModule(canvas, graph)
+ : new NodeModule(canvas, block);
+
+ for (const auto& p : block->properties()) {
+ ret->property_changed(p.first, p.second);
+ }
+
+ for (const auto& p : block->ports()) {
+ ret->new_port_view(p);
+ }
+
+ ret->set_stacked(block->polyphonic());
+
+ if (human) {
+ ret->show_human_names(human); // FIXME: double port iteration
+ }
+
+ return ret;
+}
+
+App&
+NodeModule::app() const
+{
+ return ((GraphCanvas*)canvas())->app();
+}
+
+void
+NodeModule::show_human_names(bool b)
+{
+ const URIs& uris = app().uris();
+
+ if (b) {
+ set_label(block()->label().c_str());
+ } else {
+ set_label(block()->symbol().c_str());
+ }
+
+ for (iterator i = begin(); i != end(); ++i) {
+ ingen::gui::Port* const port = dynamic_cast<ingen::gui::Port*>(*i);
+ Glib::ustring label(port->model()->symbol().c_str());
+ if (b) {
+ const Atom& name_property = port->model()->get_property(uris.lv2_name);
+ if (name_property.type() == uris.forge.String) {
+ label = name_property.ptr<char>();
+ } else {
+ Glib::ustring hn = block()->plugin_model()->port_human_name(
+ port->model()->index());
+ if (!hn.empty()) {
+ label = hn;
+ }
+ }
+ }
+ (*i)->set_label(label.c_str());
+ }
+}
+
+void
+NodeModule::port_activity(uint32_t index, const Atom& value)
+{
+ const URIs& uris = app().uris();
+ if (!_plugin_ui) {
+ return;
+ }
+
+ if (_block->get_port(index)->is_a(uris.atom_AtomPort)) {
+ _plugin_ui->port_event(index,
+ lv2_atom_total_size(value.atom()),
+ uris.atom_eventTransfer,
+ value.atom());
+ }
+}
+
+void
+NodeModule::port_value_changed(uint32_t index, const Atom& value)
+{
+ const URIs& uris = app().uris();
+ if (!_plugin_ui) {
+ return;
+ }
+
+ if (value.type() == uris.atom_Float &&
+ _block->get_port(index)->is_numeric()) {
+ _plugin_ui->port_event(index, sizeof(float), 0, value.ptr<float>());
+ } else {
+ _plugin_ui->port_event(index,
+ lv2_atom_total_size(value.atom()),
+ uris.atom_eventTransfer,
+ value.atom());
+ }
+}
+
+void
+NodeModule::plugin_changed()
+{
+ for (iterator p = begin(); p != end(); ++p) {
+ dynamic_cast<ingen::gui::Port*>(*p)->update_metadata();
+ }
+}
+
+void
+NodeModule::on_embed_gui_toggled(bool embed)
+{
+ embed_gui(embed);
+ app().set_property(_block->uri(),
+ app().uris().ingen_uiEmbedded,
+ app().forge().make(embed));
+}
+
+void
+NodeModule::embed_gui(bool embed)
+{
+ if (embed) {
+ if (_gui_window) {
+ app().log().warn("LV2 GUI already popped up, cannot embed\n");
+ return;
+ }
+
+ if (!_plugin_ui) {
+ _plugin_ui = _block->plugin_model()->ui(app().world(), _block);
+ }
+
+ if (_plugin_ui) {
+ _plugin_ui->signal_property_changed().connect(
+ sigc::mem_fun(app(), &App::set_property));
+
+ if (!_plugin_ui->instantiate()) {
+ app().log().error("Failed to instantiate LV2 UI\n");
+ } else {
+ GtkWidget* c_widget = (GtkWidget*)_plugin_ui->get_widget();
+ _gui_widget = Glib::wrap(c_widget);
+
+ Gtk::Container* container = new Gtk::EventBox();
+ container->set_name("IngenEmbeddedUI");
+ container->set_border_width(4.0);
+ container->add(*_gui_widget);
+ Ganv::Module::embed(container);
+ }
+ } else {
+ app().log().error("Failed to create LV2 UI\n");
+ }
+
+ if (_gui_widget) {
+ _gui_widget->show_all();
+ set_control_values();
+ }
+
+ } else { // un-embed
+ Ganv::Module::embed(nullptr);
+ _plugin_ui.reset();
+ }
+}
+
+void
+NodeModule::rename()
+{
+ if (app().world().conf().option("port-labels").get<int32_t>() &&
+ !app().world().conf().option("human-names").get<int32_t>()) {
+ set_label(_block->path().symbol());
+ }
+}
+
+void
+NodeModule::new_port_view(SPtr<const PortModel> port)
+{
+ Port::create(app(), *this, port);
+
+ port->signal_value_changed().connect(
+ sigc::bind<0>(sigc::mem_fun(this, &NodeModule::port_value_changed),
+ port->index()));
+
+ port->signal_activity().connect(
+ sigc::bind<0>(sigc::mem_fun(this, &NodeModule::port_activity),
+ port->index()));
+}
+
+Port*
+NodeModule::port(SPtr<const PortModel> model)
+{
+ for (iterator p = begin(); p != end(); ++p) {
+ Port* const port = dynamic_cast<Port*>(*p);
+ if (port->model() == model) {
+ return port;
+ }
+ }
+ return nullptr;
+}
+
+void
+NodeModule::delete_port_view(SPtr<const PortModel> model)
+{
+ Port* p = port(model);
+ if (p) {
+ delete p;
+ } else {
+ app().log().warn("Failed to find port %1% on module %2%\n",
+ model->path(), _block->path());
+ }
+}
+
+bool
+NodeModule::popup_gui()
+{
+ if (_block->plugin() && app().uris().lv2_Plugin == _block->plugin_model()->type()) {
+ if (_plugin_ui) {
+ app().log().warn("LV2 GUI already embedded, cannot pop up\n");
+ return false;
+ }
+
+ const PluginModel* const plugin = dynamic_cast<const PluginModel*>(_block->plugin());
+ assert(plugin);
+
+ _plugin_ui = plugin->ui(app().world(), _block);
+
+ if (_plugin_ui) {
+ _plugin_ui->signal_property_changed().connect(
+ sigc::mem_fun(app(), &App::set_property));
+
+ if (!_plugin_ui->instantiated() && !_plugin_ui->instantiate()) {
+ app().log().error("Failed to instantiate LV2 UI\n");
+ return false;
+ }
+
+ GtkWidget* c_widget = (GtkWidget*)_plugin_ui->get_widget();
+ _gui_widget = Glib::wrap(c_widget);
+
+ _gui_window = new Gtk::Window();
+ if (!_plugin_ui->is_resizable()) {
+ _gui_window->set_resizable(false);
+ }
+ _gui_window->set_title(_block->path() + " UI - Ingen");
+ _gui_window->set_role("plugin_ui");
+ _gui_window->add(*_gui_widget);
+ _gui_widget->show_all();
+ set_control_values();
+
+ _gui_window->signal_unmap().connect(
+ sigc::mem_fun(this, &NodeModule::on_gui_window_close));
+ _gui_window->present();
+
+ return true;
+ } else {
+ app().log().warn("No LV2 GUI for %1%\n", _block->path());
+ }
+ }
+
+ return false;
+}
+
+void
+NodeModule::on_gui_window_close()
+{
+ delete _gui_window;
+ _gui_window = nullptr;
+ _plugin_ui.reset();
+ _gui_widget = nullptr;
+}
+
+void
+NodeModule::set_control_values()
+{
+ uint32_t index = 0;
+ for (const auto& p : _block->ports()) {
+ if (app().can_control(p.get()) && p->value().is_valid()) {
+ port_value_changed(index, p->value());
+ }
+ ++index;
+ }
+}
+
+bool
+NodeModule::on_double_click(GdkEventButton* event)
+{
+ popup_gui();
+ return true;
+}
+
+bool
+NodeModule::on_event(GdkEvent* ev)
+{
+ if (ev->type == GDK_BUTTON_PRESS && ev->button.button == 3) {
+ return show_menu(&ev->button);
+ } else if (ev->type == GDK_2BUTTON_PRESS) {
+ return on_double_click(&ev->button);
+ } else if (ev->type == GDK_ENTER_NOTIFY) {
+ GraphBox* const box = app().window_factory()->graph_box(
+ dynamic_ptr_cast<const GraphModel>(_block->parent()));
+ if (box) {
+ box->object_entered(_block.get());
+ }
+ } else if (ev->type == GDK_LEAVE_NOTIFY) {
+ GraphBox* const box = app().window_factory()->graph_box(
+ dynamic_ptr_cast<const GraphModel>(_block->parent()));
+ if (box) {
+ box->object_left(_block.get());
+ }
+ }
+
+ return false;
+}
+
+void
+NodeModule::store_location(double ax, double ay)
+{
+ const URIs& uris = app().uris();
+
+ const Atom x(app().forge().make(static_cast<float>(ax)));
+ const Atom y(app().forge().make(static_cast<float>(ay)));
+
+ if (x != _block->get_property(uris.ingen_canvasX) ||
+ y != _block->get_property(uris.ingen_canvasY))
+ {
+ app().interface()->put(_block->uri(), {{uris.ingen_canvasX, x},
+ {uris.ingen_canvasY, y}});
+ }
+}
+
+void
+NodeModule::property_changed(const URI& key, const Atom& value)
+{
+ const URIs& uris = app().uris();
+ if (value.type() == uris.forge.Float) {
+ if (key == uris.ingen_canvasX) {
+ move_to(value.get<float>(), get_y());
+ } else if (key == uris.ingen_canvasY) {
+ move_to(get_x(), value.get<float>());
+ }
+ } else if (value.type() == uris.forge.Bool) {
+ if (key == uris.ingen_polyphonic) {
+ set_stacked(value.get<int32_t>());
+ } else if (key == uris.ingen_uiEmbedded && _initialised) {
+ if (value.get<int32_t>() && !_gui_widget) {
+ embed_gui(true);
+ } else if (!value.get<int32_t>() && _gui_widget) {
+ embed_gui(false);
+ }
+ } else if (key == uris.ingen_enabled) {
+ if (value.get<int32_t>()) {
+ set_dash_length(0.0);
+ } else {
+ set_dash_length(5.0);
+ }
+ }
+ } else if (value.type() == uris.forge.String) {
+ if (key == uris.lv2_name
+ && app().world().conf().option("human-names").get<int32_t>()) {
+ set_label(value.ptr<char>());
+ }
+ }
+}
+
+bool
+NodeModule::on_selected(gboolean selected)
+{
+ GraphWindow* win = app().window_factory()->parent_graph_window(block());
+ if (!win) {
+ return true;
+ }
+
+ if (selected && win->documentation_is_visible()) {
+ GraphWindow* win = app().window_factory()->parent_graph_window(block());
+ std::string doc;
+ bool html = false;
+#ifdef HAVE_WEBKIT
+ html = true;
+#endif
+ if (block()->plugin_model()) {
+ doc = block()->plugin_model()->documentation(html);
+ }
+ win->set_documentation(doc, html);
+ }
+
+ return true;
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/NodeModule.hpp b/src/gui/NodeModule.hpp
new file mode 100644
index 00000000..ed5914de
--- /dev/null
+++ b/src/gui/NodeModule.hpp
@@ -0,0 +1,104 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_NODEMODULE_HPP
+#define INGEN_GUI_NODEMODULE_HPP
+
+#include "ganv/Module.hpp"
+#include "ingen/types.hpp"
+
+#include "Port.hpp"
+
+namespace Raul { class Atom; }
+
+namespace ingen { namespace client {
+class BlockModel;
+class PluginUI;
+class PortModel;
+} }
+
+namespace ingen {
+namespace gui {
+
+class GraphCanvas;
+class Port;
+class NodeMenu;
+
+/** A module in a graphn.
+ *
+ * This base class is extended for various types of modules.
+ *
+ * \ingroup GUI
+ */
+class NodeModule : public Ganv::Module
+{
+public:
+ static NodeModule* create(
+ GraphCanvas& canvas,
+ SPtr<const client::BlockModel> block,
+ bool human);
+
+ virtual ~NodeModule();
+
+ App& app() const;
+
+ Port* port(SPtr<const client::PortModel> model);
+
+ void delete_port_view(SPtr<const client::PortModel> model);
+
+ virtual void store_location(double ax, double ay);
+ void show_human_names(bool b);
+
+ SPtr<const client::BlockModel> block() const { return _block; }
+
+protected:
+ NodeModule(GraphCanvas& canvas, SPtr<const client::BlockModel> block);
+
+ virtual bool on_double_click(GdkEventButton* ev);
+
+ bool idle_init();
+ bool on_event(GdkEvent* ev) override;
+
+ void on_embed_gui_toggled(bool embed);
+ void embed_gui(bool embed);
+ bool popup_gui();
+ void on_gui_window_close();
+ bool on_selected(gboolean selected) override;
+
+ void rename();
+ void property_changed(const URI& key, const Atom& value);
+
+ void new_port_view(SPtr<const client::PortModel> port);
+
+ void port_activity(uint32_t index, const Atom& value);
+ void port_value_changed(uint32_t index, const Atom& value);
+ void plugin_changed();
+ void set_control_values();
+
+ bool show_menu(GdkEventButton* ev);
+
+ SPtr<const client::BlockModel> _block;
+ NodeMenu* _menu;
+ SPtr<client::PluginUI> _plugin_ui;
+ Gtk::Widget* _gui_widget;
+ Gtk::Window* _gui_window; ///< iff popped up
+ bool _initialised;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_NODEMODULE_HPP
diff --git a/src/gui/ObjectMenu.cpp b/src/gui/ObjectMenu.cpp
new file mode 100644
index 00000000..7a523f4e
--- /dev/null
+++ b/src/gui/ObjectMenu.cpp
@@ -0,0 +1,146 @@
+/*
+ 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 "ObjectMenu.hpp"
+
+#include "App.hpp"
+#include "WidgetFactory.hpp"
+#include "WindowFactory.hpp"
+
+#include "ingen/Forge.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/client/ObjectModel.hpp"
+
+#include <cstdint>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+ObjectMenu::ObjectMenu(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Gtk::Menu(cobject)
+ , _app(nullptr)
+ , _polyphonic_menuitem(nullptr)
+ , _disconnect_menuitem(nullptr)
+ , _rename_menuitem(nullptr)
+ , _destroy_menuitem(nullptr)
+ , _properties_menuitem(nullptr)
+ , _enable_signal(false)
+{
+ xml->get_widget("object_learn_menuitem", _learn_menuitem);
+ xml->get_widget("object_unlearn_menuitem", _unlearn_menuitem);
+ xml->get_widget("object_polyphonic_menuitem", _polyphonic_menuitem);
+ xml->get_widget("object_disconnect_menuitem", _disconnect_menuitem);
+ xml->get_widget("object_rename_menuitem", _rename_menuitem);
+ xml->get_widget("object_destroy_menuitem", _destroy_menuitem);
+ xml->get_widget("object_properties_menuitem", _properties_menuitem);
+ xml->get_widget("object_menu_separator", _separator_menuitem);
+}
+
+void
+ObjectMenu::init(App& app, SPtr<const ObjectModel> object)
+{
+ _app = &app;
+ _object = object;
+
+ _polyphonic_menuitem->signal_toggled().connect(
+ sigc::mem_fun(this, &ObjectMenu::on_menu_polyphonic));
+
+ _polyphonic_menuitem->set_active(object->polyphonic());
+
+ _learn_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &ObjectMenu::on_menu_learn));
+
+ _unlearn_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &ObjectMenu::on_menu_unlearn));
+
+ _disconnect_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &ObjectMenu::on_menu_disconnect));
+
+ _rename_menuitem->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(_app->window_factory(), &WindowFactory::present_rename),
+ object));
+
+ _destroy_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &ObjectMenu::on_menu_destroy));
+
+ _properties_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &ObjectMenu::on_menu_properties));
+
+ object->signal_property().connect(sigc::mem_fun(this, &ObjectMenu::property_changed));
+
+ _learn_menuitem->hide();
+ _unlearn_menuitem->hide();
+
+ _enable_signal = true;
+}
+
+void
+ObjectMenu::on_menu_learn()
+{
+ _app->interface()->set_property(_object->uri(),
+ _app->uris().midi_binding,
+ _app->uris().patch_wildcard.urid);
+}
+
+void
+ObjectMenu::on_menu_unlearn()
+{
+ Properties remove;
+ remove.emplace(_app->uris().midi_binding,
+ Property(_app->uris().patch_wildcard));
+ _app->interface()->delta(_object->uri(), remove, Properties());
+}
+
+void
+ObjectMenu::on_menu_polyphonic()
+{
+ if (_enable_signal) {
+ _app->set_property(
+ _object->uri(),
+ _app->uris().ingen_polyphonic,
+ _app->forge().make(bool(_polyphonic_menuitem->get_active())));
+ }
+}
+
+void
+ObjectMenu::property_changed(const URI& predicate, const Atom& value)
+{
+ const URIs& uris = _app->uris();
+ _enable_signal = false;
+ if (predicate == uris.ingen_polyphonic && value.type() == uris.forge.Bool) {
+ _polyphonic_menuitem->set_active(value.get<int32_t>());
+ }
+ _enable_signal = true;
+}
+
+void
+ObjectMenu::on_menu_destroy()
+{
+ _app->interface()->del(_object->uri());
+}
+
+void
+ObjectMenu::on_menu_properties()
+{
+ _app->window_factory()->present_properties(_object);
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/ObjectMenu.hpp b/src/gui/ObjectMenu.hpp
new file mode 100644
index 00000000..22eef74b
--- /dev/null
+++ b/src/gui/ObjectMenu.hpp
@@ -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/>.
+*/
+
+#ifndef INGEN_GUI_OBJECTMENU_HPP
+#define INGEN_GUI_OBJECTMENU_HPP
+
+#include "ingen/client/ObjectModel.hpp"
+#include "ingen/types.hpp"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/checkmenuitem.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menuitem.h>
+
+namespace ingen {
+namespace gui {
+
+class App;
+class ObjectControlWindow;
+class ObjectPropertiesWindow;
+class GraphCanvas;
+
+/** Menu for a Object.
+ *
+ * \ingroup GUI
+ */
+class ObjectMenu : public Gtk::Menu
+{
+public:
+ ObjectMenu(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ void init(App& app, SPtr<const client::ObjectModel> object);
+
+ SPtr<const client::ObjectModel> object() const { return _object; }
+ App* app() const { return _app; }
+
+protected:
+ void on_menu_learn();
+ void on_menu_unlearn();
+ virtual void on_menu_disconnect() = 0;
+ void on_menu_polyphonic();
+ void on_menu_destroy();
+ void on_menu_properties();
+
+ void property_changed(const URI& predicate, const Atom& value);
+
+ App* _app;
+ SPtr<const client::ObjectModel> _object;
+ Gtk::MenuItem* _learn_menuitem;
+ Gtk::MenuItem* _unlearn_menuitem;
+ Gtk::CheckMenuItem* _polyphonic_menuitem;
+ Gtk::MenuItem* _disconnect_menuitem;
+ Gtk::MenuItem* _rename_menuitem;
+ Gtk::MenuItem* _destroy_menuitem;
+ Gtk::MenuItem* _properties_menuitem;
+ Gtk::SeparatorMenuItem* _separator_menuitem;
+
+ bool _enable_signal;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_OBJECTMENU_HPP
diff --git a/src/gui/PluginMenu.cpp b/src/gui/PluginMenu.cpp
new file mode 100644
index 00000000..0090aff1
--- /dev/null
+++ b/src/gui/PluginMenu.cpp
@@ -0,0 +1,178 @@
+/*
+ This file is part of Ingen.
+ Copyright 2014-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 "PluginMenu.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/client/PluginModel.hpp"
+
+#include <utility>
+
+namespace ingen {
+namespace gui {
+
+PluginMenu::PluginMenu(ingen::World& world)
+ : _world(world)
+ , _classless_menu(nullptr, nullptr)
+{
+ clear();
+}
+
+void
+PluginMenu::clear()
+{
+ const LilvWorld* lworld = _world.lilv_world();
+ const LilvPluginClass* lv2_plugin = lilv_world_get_plugin_class(lworld);
+ const LilvPluginClasses* classes = lilv_world_get_plugin_classes(lworld);
+
+ // Empty completely
+ _classless_menu = MenuRecord(nullptr, nullptr);
+ _class_menus.clear();
+ items().clear();
+
+ // Build skeleton
+ LV2Children children;
+ LILV_FOREACH(plugin_classes, i, classes) {
+ const LilvPluginClass* c = lilv_plugin_classes_get(classes, i);
+ const LilvNode* p = lilv_plugin_class_get_parent_uri(c);
+ if (!p) {
+ p = lilv_plugin_class_get_uri(lv2_plugin);
+ }
+ children.emplace(lilv_node_as_string(p), c);
+ }
+
+ std::set<const char*> ancestors;
+ build_plugin_class_menu(this, lv2_plugin, classes, children, ancestors);
+
+ items().push_back(Gtk::Menu_Helpers::MenuElem("_Uncategorized"));
+ _classless_menu.item = &(items().back());
+ _classless_menu.menu = Gtk::manage(new Gtk::Menu());
+ _classless_menu.item->set_submenu(*_classless_menu.menu);
+ _classless_menu.item->hide();
+}
+
+void
+PluginMenu::add_plugin(SPtr<client::PluginModel> p)
+{
+ typedef ClassMenus::iterator iterator;
+
+ if (!p->lilv_plugin() || lilv_plugin_is_replaced(p->lilv_plugin())) {
+ return;
+ }
+
+ const LilvPluginClass* pc = lilv_plugin_get_class(p->lilv_plugin());
+ const LilvNode* class_uri = lilv_plugin_class_get_uri(pc);
+ const char* class_uri_str = lilv_node_as_string(class_uri);
+
+ std::pair<iterator, iterator> range = _class_menus.equal_range(class_uri_str);
+ if (range.first == _class_menus.end() || range.first == range.second
+ || range.first->second.menu == this) {
+ // Add to uncategorized plugin menu
+ add_plugin_to_menu(_classless_menu, p);
+ } else {
+ // For each menu that represents plugin's class (possibly several)
+ for (auto i = range.first; i != range.second ; ++i) {
+ add_plugin_to_menu(i->second, p);
+ }
+ }
+}
+
+size_t
+PluginMenu::build_plugin_class_menu(Gtk::Menu* menu,
+ const LilvPluginClass* plugin_class,
+ const LilvPluginClasses* classes,
+ const LV2Children& children,
+ std::set<const char*>& ancestors)
+{
+ size_t num_items = 0;
+ const LilvNode* class_uri = lilv_plugin_class_get_uri(plugin_class);
+ const char* class_uri_str = lilv_node_as_string(class_uri);
+
+ const std::pair<LV2Children::const_iterator, LV2Children::const_iterator> kids
+ = children.equal_range(class_uri_str);
+
+ if (kids.first == children.end()) {
+ return 0;
+ }
+
+ // Add submenus
+ ancestors.insert(class_uri_str);
+ for (LV2Children::const_iterator i = kids.first; i != kids.second; ++i) {
+ const LilvPluginClass* c = i->second;
+ const char* sub_label_str = lilv_node_as_string(lilv_plugin_class_get_label(c));
+ const char* sub_uri_str = lilv_node_as_string(lilv_plugin_class_get_uri(c));
+ if (ancestors.find(sub_uri_str) != ancestors.end()) {
+ _world.log().warn("Infinite LV2 class recursion: %1% <: %2%\n",
+ class_uri_str, sub_uri_str);
+ return 0;
+ }
+
+ Gtk::Menu_Helpers::MenuElem menu_elem = Gtk::Menu_Helpers::MenuElem(
+ std::string("_") + sub_label_str);
+ menu->items().push_back(menu_elem);
+ Gtk::MenuItem* menu_item = &(menu->items().back());
+
+ Gtk::Menu* submenu = Gtk::manage(new Gtk::Menu());
+ menu_item->set_submenu(*submenu);
+
+ size_t num_child_items = build_plugin_class_menu(
+ submenu, c, classes, children, ancestors);
+
+ _class_menus.emplace(sub_uri_str, MenuRecord(menu_item, submenu));
+ if (num_child_items == 0) {
+ menu_item->hide();
+ }
+
+ ++num_items;
+ }
+ ancestors.erase(class_uri_str);
+
+ return num_items;
+}
+
+void
+PluginMenu::add_plugin_to_menu(MenuRecord& menu, SPtr<client::PluginModel> p)
+{
+ const URIs& uris = _world.uris();
+ LilvWorld* lworld = _world.lilv_world();
+ LilvNode* ingen_Graph = lilv_new_uri(lworld, uris.ingen_Graph.c_str());
+ LilvNode* rdf_type = lilv_new_uri(lworld, uris.rdf_type.c_str());
+
+ bool is_graph = lilv_world_ask(lworld,
+ lilv_plugin_get_uri(p->lilv_plugin()),
+ rdf_type,
+ ingen_Graph);
+
+ menu.menu->items().push_back(
+ Gtk::Menu_Helpers::MenuElem(
+ std::string("_") + p->human_name() + (is_graph ? " ⚙" : ""),
+ sigc::bind(sigc::mem_fun(this, &PluginMenu::load_plugin), p)));
+
+ if (!menu.item->is_visible()) {
+ menu.item->show();
+ }
+
+ lilv_node_free(rdf_type);
+ lilv_node_free(ingen_Graph);
+}
+
+void
+PluginMenu::load_plugin(WPtr<client::PluginModel> weak_plugin)
+{
+ signal_load_plugin.emit(weak_plugin);
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/PluginMenu.hpp b/src/gui/PluginMenu.hpp
new file mode 100644
index 00000000..284331d6
--- /dev/null
+++ b/src/gui/PluginMenu.hpp
@@ -0,0 +1,81 @@
+/*
+ This file is part of Ingen.
+ Copyright 2014-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/>.
+*/
+
+#ifndef INGEN_GUI_PLUGINMENU_HPP
+#define INGEN_GUI_PLUGINMENU_HPP
+
+#include "ingen/World.hpp"
+#include "ingen/types.hpp"
+#include "lilv/lilv.h"
+
+#include <gtkmm/menu.h>
+
+#include <cstddef>
+#include <map>
+#include <set>
+#include <string>
+
+namespace ingen {
+
+namespace client { class PluginModel; }
+
+namespace gui {
+
+/**
+ Type-hierarchical plugin menu.
+
+ @ingroup GUI
+*/
+class PluginMenu : public Gtk::Menu
+{
+public:
+ PluginMenu(ingen::World& world);
+
+ void clear();
+ void add_plugin(SPtr<client::PluginModel> p);
+
+ sigc::signal< void, WPtr<client::PluginModel> > signal_load_plugin;
+
+private:
+ struct MenuRecord {
+ MenuRecord(Gtk::MenuItem* i, Gtk::Menu* m) : item(i), menu(m) {}
+ Gtk::MenuItem* item;
+ Gtk::Menu* menu;
+ };
+
+ typedef std::multimap<const std::string, const LilvPluginClass*> LV2Children;
+ typedef std::multimap<const std::string, MenuRecord> ClassMenus;
+
+ /// Recursively add hierarchy rooted at `plugin_class` to `menu`.
+ size_t build_plugin_class_menu(Gtk::Menu* menu,
+ const LilvPluginClass* plugin_class,
+ const LilvPluginClasses* classes,
+ const LV2Children& children,
+ std::set<const char*>& ancestors);
+
+ void add_plugin_to_menu(MenuRecord& menu, SPtr<client::PluginModel> p);
+
+ void load_plugin(WPtr<client::PluginModel> weak_plugin);
+
+ ingen::World& _world;
+ MenuRecord _classless_menu;
+ ClassMenus _class_menus;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_PLUGINMENU_HPP
diff --git a/src/gui/Port.cpp b/src/gui/Port.cpp
new file mode 100644
index 00000000..14f87fc1
--- /dev/null
+++ b/src/gui/Port.cpp
@@ -0,0 +1,535 @@
+/*
+ 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/>.
+*/
+
+#include "Port.hpp"
+
+#include "App.hpp"
+#include "GraphWindow.hpp"
+#include "PortMenu.hpp"
+#include "RDFS.hpp"
+#include "Style.hpp"
+#include "WidgetFactory.hpp"
+#include "WindowFactory.hpp"
+#include "ingen_config.h"
+#include "rgba.hpp"
+
+#include "ganv/Module.hpp"
+#include "ingen/Configuration.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/URIMap.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/client/PortModel.hpp"
+
+#include <cassert>
+#include <string>
+
+using namespace ingen::client;
+
+namespace ingen {
+namespace gui {
+
+Port*
+Port::create(App& app,
+ Ganv::Module& module,
+ SPtr<const PortModel> pm,
+ bool flip)
+{
+ return new Port(app, module, pm, port_label(app, pm), flip);
+}
+
+/** @param flip Make an input port appear as an output port, and vice versa.
+ */
+Port::Port(App& app,
+ Ganv::Module& module,
+ SPtr<const PortModel> pm,
+ const std::string& name,
+ bool flip)
+ : Ganv::Port(module, name,
+ flip ? (!pm->is_input()) : pm->is_input(),
+ app.style()->get_port_color(pm.get()))
+ , _app(app)
+ , _port_model(pm)
+ , _entered(false)
+ , _flipped(flip)
+{
+ assert(pm);
+
+ if (app.can_control(pm.get())) {
+ show_control();
+ pm->signal_value_changed().connect(
+ sigc::mem_fun(this, &Port::value_changed));
+ }
+
+ port_properties_changed();
+
+ pm->signal_property().connect(
+ sigc::mem_fun(this, &Port::property_changed));
+ pm->signal_property_removed().connect(
+ sigc::mem_fun(this, &Port::property_removed));
+ pm->signal_activity().connect(
+ sigc::mem_fun(this, &Port::activity));
+ pm->signal_moved().connect(
+ sigc::mem_fun(this, &Port::moved));
+
+ signal_value_changed.connect(
+ sigc::mem_fun(this, &Port::on_value_changed));
+
+ signal_event().connect(
+ sigc::mem_fun(this, &Port::on_event));
+
+ set_is_controllable(pm->is_numeric() && pm->is_input());
+
+ Ganv::Port::set_beveled(model()->is_a(_app.uris().lv2_ControlPort) ||
+ model()->has_property(_app.uris().atom_bufferType,
+ _app.uris().atom_Sequence));
+
+ for (const auto& p : pm->properties()) {
+ property_changed(p.first, p.second);
+ }
+
+ update_metadata();
+ value_changed(pm->value());
+}
+
+Port::~Port()
+{
+ _app.activity_port_destroyed(this);
+}
+
+std::string
+Port::port_label(App& app, SPtr<const PortModel> pm)
+{
+ if (!pm) {
+ return "";
+ }
+
+ std::string label;
+ if (app.world().conf().option("port-labels").get<int32_t>()) {
+ if (app.world().conf().option("human-names").get<int32_t>()) {
+ const Atom& name = pm->get_property(app.uris().lv2_name);
+ if (name.type() == app.forge().String) {
+ label = name.ptr<char>();
+ } else {
+ const SPtr<const BlockModel> parent(
+ dynamic_ptr_cast<const BlockModel>(pm->parent()));
+ if (parent && parent->plugin_model()) {
+ label = parent->plugin_model()->port_human_name(pm->index());
+ }
+ }
+ } else {
+ label = pm->path().symbol();
+ }
+ }
+ return label;
+}
+
+void
+Port::ensure_label()
+{
+ if (!get_label()) {
+ set_label(port_label(_app, _port_model.lock()).c_str());
+ }
+}
+
+void
+Port::update_metadata()
+{
+ SPtr<const PortModel> pm = _port_model.lock();
+ if (pm && _app.can_control(pm.get()) && pm->is_numeric()) {
+ SPtr<const BlockModel> parent = dynamic_ptr_cast<const BlockModel>(pm->parent());
+ if (parent) {
+ float min = 0.0f;
+ float max = 1.0f;
+ parent->port_value_range(pm, min, max, _app.sample_rate());
+ set_control_min(min);
+ set_control_max(max);
+ }
+ }
+}
+
+bool
+Port::show_menu(GdkEventButton* ev)
+{
+ PortMenu* menu = nullptr;
+ WidgetFactory::get_widget_derived("object_menu", menu);
+ if (!menu) {
+ _app.log().error("Failed to load port menu widget\n");
+ return false;
+ }
+
+ menu->init(_app, model(), _flipped);
+ menu->popup(ev->button, ev->time);
+ return true;
+}
+
+void
+Port::moved()
+{
+ if (_app.world().conf().option("port-labels").get<int32_t>() &&
+ !_app.world().conf().option("human-names").get<int32_t>()) {
+ set_label(model()->symbol().c_str());
+ }
+}
+
+void
+Port::on_value_changed(double value)
+{
+ const URIs& uris = _app.uris();
+ const Atom& current_value = model()->value();
+ if (current_value.type() != uris.forge.Float) {
+ return; // Non-float, unsupported
+ }
+
+ if (current_value.get<float>() == (float)value) {
+ return; // No change
+ }
+
+ const Atom atom = _app.forge().make(float(value));
+ _app.set_property(model()->uri(),
+ _app.world().uris().ingen_value,
+ atom);
+
+ if (_entered) {
+ GraphBox* box = get_graph_box();
+ if (box) {
+ box->show_port_status(model().get(), atom);
+ }
+ }
+}
+
+void
+Port::value_changed(const Atom& value)
+{
+ if (value.type() == _app.forge().Float && !get_grabbed()) {
+ Ganv::Port::set_control_value(value.get<float>());
+ }
+}
+
+void
+Port::on_scale_point_activated(float f)
+{
+ _app.set_property(model()->uri(),
+ _app.world().uris().ingen_value,
+ _app.world().forge().make(f));
+}
+
+Gtk::Menu*
+Port::build_enum_menu()
+{
+ SPtr<const BlockModel> block = dynamic_ptr_cast<BlockModel>(model()->parent());
+ Gtk::Menu* menu = Gtk::manage(new Gtk::Menu());
+
+ PluginModel::ScalePoints points = block->plugin_model()->port_scale_points(
+ model()->index());
+ for (auto i = points.begin(); i != points.end(); ++i) {
+ menu->items().push_back(Gtk::Menu_Helpers::MenuElem(i->second));
+ Gtk::MenuItem* menu_item = &(menu->items().back());
+ menu_item->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(this, &Port::on_scale_point_activated),
+ i->first));
+ }
+
+ return menu;
+}
+
+void
+Port::on_uri_activated(const URI& uri)
+{
+ _app.set_property(model()->uri(),
+ _app.world().uris().ingen_value,
+ _app.world().forge().make_urid(
+ _app.world().uri_map().map_uri(uri.c_str())));
+}
+
+Gtk::Menu*
+Port::build_uri_menu()
+{
+ World& world = _app.world();
+ SPtr<const BlockModel> block = dynamic_ptr_cast<BlockModel>(model()->parent());
+ Gtk::Menu* menu = Gtk::manage(new Gtk::Menu());
+
+ // Get the port designation, which should be a rdf:Property
+ const Atom& designation_atom = model()->get_property(
+ _app.uris().lv2_designation);
+ if (!designation_atom.is_valid()) {
+ return nullptr;
+ }
+
+ LilvNode* designation = lilv_new_uri(
+ world.lilv_world(), world.forge().str(designation_atom, false).c_str());
+ LilvNode* rdfs_range = lilv_new_uri(
+ world.lilv_world(), LILV_NS_RDFS "range");
+
+ // Get every class in the range of the port's property
+ rdfs::URISet ranges;
+ LilvNodes* range = lilv_world_find_nodes(
+ world.lilv_world(), designation, rdfs_range, nullptr);
+ LILV_FOREACH(nodes, r, range) {
+ ranges.insert(URI(lilv_node_as_string(lilv_nodes_get(range, r))));
+ }
+ rdfs::classes(world, ranges, false);
+
+ // Get all objects in range
+ rdfs::Objects values = rdfs::instances(world, ranges);
+
+ // Add a menu item for each such class
+ for (const auto& v : values) {
+ if (!v.first.empty()) {
+ const std::string qname = world.rdf_world()->prefixes().qualify(v.second);
+ const std::string label = qname + " - " + v.first;
+ menu->items().push_back(Gtk::Menu_Helpers::MenuElem(label));
+ Gtk::MenuItem* menu_item = &(menu->items().back());
+ menu_item->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(this, &Port::on_uri_activated),
+ v.second));
+ }
+ }
+
+ return menu;
+}
+
+bool
+Port::on_event(GdkEvent* ev)
+{
+ GraphBox* box = nullptr;
+ switch (ev->type) {
+ case GDK_ENTER_NOTIFY:
+ _entered = true;
+ if ((box = get_graph_box())) {
+ box->object_entered(model().get());
+ }
+ return false;
+ case GDK_LEAVE_NOTIFY:
+ _entered = false;
+ if ((box = get_graph_box())) {
+ box->object_left(model().get());
+ }
+ return false;
+ case GDK_BUTTON_PRESS:
+ if (ev->button.button == 1) {
+ if (model()->is_enumeration()) {
+ Gtk::Menu* menu = build_enum_menu();
+ menu->popup(ev->button.button, ev->button.time);
+ return true;
+ } else if (model()->is_uri()) {
+ Gtk::Menu* menu = build_uri_menu();
+ if (menu) {
+ menu->popup(ev->button.button, ev->button.time);
+ return true;
+ }
+ }
+ } else if (ev->button.button == 3) {
+ return show_menu(&ev->button);
+ }
+ break;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+inline static uint32_t
+peak_color(float peak)
+{
+ static const uint32_t min = 0x4A8A0EC0;
+ static const uint32_t max = 0xFFCE1FC0;
+ static const uint32_t peak_min = 0xFF561FC0;
+ static const uint32_t peak_max = 0xFF0A38C0;
+
+ if (peak < 1.0) {
+ return rgba_interpolate(min, max, peak);
+ } else {
+ return rgba_interpolate(peak_min, peak_max, fminf(peak, 2.0f) - 1.0f);
+ }
+}
+
+void
+Port::activity(const Atom& value)
+{
+ if (model()->is_a(_app.uris().lv2_AudioPort)) {
+ set_fill_color(peak_color(value.get<float>()));
+ } else if (_app.can_control(model().get()) && value.type() == _app.uris().atom_Float) {
+ Ganv::Port::set_control_value(value.get<float>());
+ } else {
+ _app.port_activity(this);
+ }
+}
+
+GraphBox*
+Port::get_graph_box() const
+{
+ SPtr<const GraphModel> graph = dynamic_ptr_cast<const GraphModel>(model()->parent());
+ GraphBox* box = _app.window_factory()->graph_box(graph);
+ if (!box) {
+ graph = dynamic_ptr_cast<const GraphModel>(model()->parent()->parent());
+ box = _app.window_factory()->graph_box(graph);
+ }
+ return box;
+}
+
+void
+Port::set_type_tag()
+{
+ const URIs& uris = _app.uris();
+ std::string tag;
+ if (model()->is_a(_app.uris().lv2_AudioPort)) {
+ tag = "~";
+ } else if (model()->is_a(_app.uris().lv2_CVPort)) {
+ tag = "ℝ̰";
+ } else if (model()->is_a(_app.uris().lv2_ControlPort)) {
+ if (model()->is_enumeration()) {
+ tag = "…";
+ } else if (model()->is_integer()) {
+ tag = "ℤ";
+ } else if (model()->is_toggle()) {
+ tag = ((model()->value() != _app.uris().forge.make(0.0f))
+ ? "☑" : "☐");
+
+ } else {
+ tag = "ℝ";
+ }
+ } else if (model()->is_a(_app.uris().atom_AtomPort)) {
+ if (model()->supports(_app.uris().atom_Float)) {
+ if (model()->is_toggle()) {
+ tag = ((model()->value() != _app.uris().forge.make(0.0f))
+ ? "☑" : "☐");
+ } else {
+ tag = "ℝ";
+ }
+ }
+ if (model()->supports(_app.uris().atom_Int)) {
+ tag += "ℤ";
+ }
+ if (model()->supports(_app.uris().midi_MidiEvent)) {
+ tag += "𝕄";
+ }
+ if (model()->supports(_app.uris().patch_Message)) {
+ if (tag.empty()) {
+ tag += "=";
+ } else {
+ tag += "̿";
+ }
+ }
+ if (tag.empty()) {
+ tag = "*";
+ }
+
+ if (model()->has_property(uris.atom_bufferType, uris.atom_Sequence)) {
+ tag += "̤";
+ }
+ }
+
+ if (!tag.empty()) {
+ set_value_label(tag.c_str());
+ }
+}
+
+void
+Port::port_properties_changed()
+{
+ if (model()->is_toggle()) {
+ set_control_is_toggle(true);
+ } else if (model()->is_integer()) {
+ set_control_is_integer(true);
+ }
+ set_type_tag();
+}
+
+void
+Port::property_changed(const URI& key, const Atom& value)
+{
+ const URIs& uris = _app.uris();
+ if (value.type() == uris.forge.Float) {
+ float val = value.get<float>();
+ if (key == uris.ingen_value && !get_grabbed()) {
+ Ganv::Port::set_control_value(val);
+ if (model()->is_toggle()) {
+ std::string tag = (val == 0.0f) ? "☐" : "☑";
+ if (model()->is_a(_app.uris().lv2_CVPort)) {
+ tag += "̰";
+ } else if (model()->has_property(uris.atom_bufferType,
+ uris.atom_Sequence)) {
+ tag += "̤";
+ }
+ set_value_label(tag.c_str());
+ }
+ } else if (key == uris.lv2_minimum) {
+ if (model()->port_property(uris.lv2_sampleRate)) {
+ val *= _app.sample_rate();
+ }
+ set_control_min(val);
+ } else if (key == uris.lv2_maximum) {
+ if (model()->port_property(uris.lv2_sampleRate)) {
+ val *= _app.sample_rate();
+ }
+ set_control_max(val);
+ }
+ } else if (key == uris.lv2_portProperty) {
+ port_properties_changed();
+ } else if (key == uris.lv2_name) {
+ if (value.type() == uris.forge.String &&
+ _app.world().conf().option("port-labels").get<int32_t>() &&
+ _app.world().conf().option("human-names").get<int32_t>()) {
+ set_label(value.ptr<char>());
+ }
+ } else if (key == uris.rdf_type || key == uris.atom_bufferType) {
+ set_fill_color(_app.style()->get_port_color(model().get()));
+ Ganv::Port::set_beveled(model()->is_a(uris.lv2_ControlPort) ||
+ model()->has_property(uris.atom_bufferType,
+ uris.atom_Sequence));
+ }
+}
+
+void
+Port::property_removed(const URI& key, const Atom& value)
+{
+ const URIs& uris = _app.uris();
+ if (key == uris.lv2_minimum || key == uris.lv2_maximum) {
+ update_metadata();
+ } else if (key == uris.rdf_type || key == uris.atom_bufferType) {
+ Ganv::Port::set_beveled(model()->is_a(uris.lv2_ControlPort) ||
+ model()->has_property(uris.atom_bufferType,
+ uris.atom_Sequence));
+ }
+}
+
+bool
+Port::on_selected(gboolean b)
+{
+ if (b) {
+ SPtr<const PortModel> pm = _port_model.lock();
+ if (pm) {
+ SPtr<const BlockModel> block = dynamic_ptr_cast<const BlockModel>(pm->parent());
+ GraphWindow* win = _app.window_factory()->parent_graph_window(block);
+ if (win && win->documentation_is_visible() && block->plugin_model()) {
+ bool html = false;
+#ifdef HAVE_WEBKIT
+ html = true;
+#endif
+ const std::string& doc = block->plugin_model()->port_documentation(
+ pm->index(), html);
+ win->set_documentation(doc, html);
+ }
+ }
+ }
+
+ return true;
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/Port.hpp b/src/gui/Port.hpp
new file mode 100644
index 00000000..c95c93ef
--- /dev/null
+++ b/src/gui/Port.hpp
@@ -0,0 +1,103 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_PORT_HPP
+#define INGEN_GUI_PORT_HPP
+
+#include "ganv/Port.hpp"
+#include "ingen/types.hpp"
+
+#include <gtkmm/menu.h>
+
+#include <cassert>
+#include <string>
+
+namespace Raul {
+class Atom;
+}
+
+namespace ingen {
+
+class URI;
+class Atom;
+
+namespace client { class PortModel; }
+
+namespace gui {
+
+class App;
+class GraphBox;
+
+/** A Port on an Module.
+ *
+ * \ingroup GUI
+ */
+class Port : public Ganv::Port
+{
+public:
+ static Port* create(
+ App& app,
+ Ganv::Module& module,
+ SPtr<const client::PortModel> pm,
+ bool flip = false);
+
+ ~Port();
+
+ SPtr<const client::PortModel> model() const { return _port_model.lock(); }
+
+ bool show_menu(GdkEventButton* ev);
+ void update_metadata();
+ void ensure_label();
+
+ void value_changed(const Atom& value);
+ void activity(const Atom& value);
+
+ bool on_selected(gboolean b) override;
+
+private:
+ Port(App& app,
+ Ganv::Module& module,
+ SPtr<const client::PortModel> pm,
+ const std::string& name,
+ bool flip = false);
+
+ static std::string port_label(App& app, SPtr<const client::PortModel> pm);
+
+ Gtk::Menu* build_enum_menu();
+ Gtk::Menu* build_uri_menu();
+ GraphBox* get_graph_box() const;
+
+ void property_changed(const URI& key, const Atom& value);
+ void property_removed(const URI& key, const Atom& value);
+ void moved();
+
+ void on_value_changed(double value);
+ void on_scale_point_activated(float f);
+ void on_uri_activated(const URI& uri);
+ bool on_event(GdkEvent* ev) override;
+ void port_properties_changed();
+ void set_type_tag();
+
+ App& _app;
+ WPtr<const client::PortModel> _port_model;
+ bool _entered : 1;
+ bool _flipped : 1;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_PORT_HPP
diff --git a/src/gui/PortMenu.cpp b/src/gui/PortMenu.cpp
new file mode 100644
index 00000000..ff3d2571
--- /dev/null
+++ b/src/gui/PortMenu.cpp
@@ -0,0 +1,175 @@
+/*
+ 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 "PortMenu.hpp"
+
+#include "App.hpp"
+#include "WindowFactory.hpp"
+
+#include "ingen/Interface.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/client/PortModel.hpp"
+#include "ingen/types.hpp"
+
+#include <string>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+PortMenu::PortMenu(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : ObjectMenu(cobject, xml)
+ , _internal_graph_port(false)
+{
+ xml->get_widget("object_menu", _port_menu);
+ xml->get_widget("port_set_min_menuitem", _set_min_menuitem);
+ xml->get_widget("port_set_max_menuitem", _set_max_menuitem);
+ xml->get_widget("port_reset_range_menuitem", _reset_range_menuitem);
+ xml->get_widget("port_expose_menuitem", _expose_menuitem);
+}
+
+void
+PortMenu::init(App& app, SPtr<const PortModel> port, bool internal_graph_port)
+{
+ const URIs& uris = app.uris();
+
+ ObjectMenu::init(app, port);
+ _internal_graph_port = internal_graph_port;
+
+ _set_min_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &PortMenu::on_menu_set_min));
+
+ _set_max_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &PortMenu::on_menu_set_max));
+
+ _reset_range_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &PortMenu::on_menu_reset_range));
+
+ _expose_menuitem->signal_activate().connect(
+ sigc::mem_fun(this, &PortMenu::on_menu_expose));
+
+ const bool is_control(app.can_control(port.get()) && port->is_numeric());
+ const bool is_on_graph(dynamic_ptr_cast<GraphModel>(port->parent()));
+ const bool is_input(port->is_input());
+
+ if (!is_on_graph) {
+ _polyphonic_menuitem->set_sensitive(false);
+ _rename_menuitem->set_sensitive(false);
+ _destroy_menuitem->set_sensitive(false);
+ }
+
+ if (port->is_a(uris.atom_AtomPort)) {
+ _polyphonic_menuitem->hide();
+ }
+
+ _reset_range_menuitem->set_visible(is_input && is_control && !is_on_graph);
+ _set_max_menuitem->set_visible(is_input && is_control);
+ _set_min_menuitem->set_visible(is_input && is_control);
+ _expose_menuitem->set_visible(!is_on_graph);
+ _learn_menuitem->set_visible(is_input && is_control);
+ _unlearn_menuitem->set_visible(is_input && is_control);
+
+ if (!is_control && is_on_graph) {
+ _separator_menuitem->hide();
+ }
+
+ _enable_signal = true;
+}
+
+void
+PortMenu::on_menu_disconnect()
+{
+ if (_internal_graph_port) {
+ _app->interface()->disconnect_all(
+ _object->parent()->path(), _object->path());
+ } else {
+ _app->interface()->disconnect_all(
+ _object->parent()->path().parent(), _object->path());
+ }
+}
+
+void
+PortMenu::on_menu_set_min()
+{
+ const URIs& uris = _app->uris();
+ SPtr<const PortModel> model = dynamic_ptr_cast<const PortModel>(_object);
+ const Atom& value = model->get_property(uris.ingen_value);
+ if (value.is_valid()) {
+ _app->set_property(_object->uri(), uris.lv2_minimum, value);
+ }
+}
+
+void
+PortMenu::on_menu_set_max()
+{
+ const URIs& uris = _app->uris();
+ SPtr<const PortModel> model = dynamic_ptr_cast<const PortModel>(_object);
+ const Atom& value = model->get_property(uris.ingen_value);
+ if (value.is_valid()) {
+ _app->set_property(_object->uri(), uris.lv2_maximum, value);
+ }
+}
+
+void
+PortMenu::on_menu_reset_range()
+{
+ const URIs& uris = _app->uris();
+ SPtr<const PortModel> model = dynamic_ptr_cast<const PortModel>(_object);
+
+ // Remove lv2:minimum and lv2:maximum properties
+ Properties remove;
+ remove.insert({uris.lv2_minimum, Property(uris.patch_wildcard)});
+ remove.insert({uris.lv2_maximum, Property(uris.patch_wildcard)});
+ _app->interface()->delta(_object->uri(), remove, Properties());
+}
+
+void
+PortMenu::on_menu_expose()
+{
+ const URIs& uris = _app->uris();
+ SPtr<const PortModel> port = dynamic_ptr_cast<const PortModel>(_object);
+ SPtr<const BlockModel> block = dynamic_ptr_cast<const BlockModel>(port->parent());
+
+ const std::string label = block->label() + " " + block->port_label(port);
+ const Raul::Path path = Raul::Path(block->path() + Raul::Symbol("_" + port->symbol()));
+
+ ingen::Resource r(*_object.get());
+ r.remove_property(uris.lv2_index, uris.patch_wildcard);
+ r.set_property(uris.lv2_symbol, _app->forge().alloc(path.symbol()));
+ r.set_property(uris.lv2_name, _app->forge().alloc(label.c_str()));
+
+ // TODO: Pretty kludgey coordinates
+ const float block_x = block->get_property(uris.ingen_canvasX).get<float>();
+ const float block_y = block->get_property(uris.ingen_canvasY).get<float>();
+ const float x_off = (label.length() * 16.0f) * (port->is_input() ? -1 : 1);
+ const float y_off = port->index() * 32.0f;
+ r.set_property(uris.ingen_canvasX, _app->forge().make(block_x + x_off));
+ r.set_property(uris.ingen_canvasY, _app->forge().make(block_y + y_off));
+
+ _app->interface()->put(path_to_uri(path), r.properties());
+
+ if (port->is_input()) {
+ _app->interface()->connect(path, _object->path());
+ } else {
+ _app->interface()->connect(_object->path(), path);
+ }
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/PortMenu.hpp b/src/gui/PortMenu.hpp
new file mode 100644
index 00000000..26298e76
--- /dev/null
+++ b/src/gui/PortMenu.hpp
@@ -0,0 +1,66 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_PORTMENU_HPP
+#define INGEN_GUI_PORTMENU_HPP
+
+#include "ObjectMenu.hpp"
+
+#include "ingen/client/PortModel.hpp"
+#include "ingen/types.hpp"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menushell.h>
+
+namespace ingen {
+namespace gui {
+
+/** Menu for a Port.
+ *
+ * \ingroup GUI
+ */
+class PortMenu : public ObjectMenu
+{
+public:
+ PortMenu(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ void init(App& app,
+ SPtr<const client::PortModel> port,
+ bool internal_graph_port = false);
+
+private:
+ void on_menu_disconnect() override;
+ void on_menu_set_min();
+ void on_menu_set_max();
+ void on_menu_reset_range();
+ void on_menu_expose();
+
+ Gtk::Menu* _port_menu;
+ Gtk::MenuItem* _set_min_menuitem;
+ Gtk::MenuItem* _set_max_menuitem;
+ Gtk::MenuItem* _reset_range_menuitem;
+ Gtk::MenuItem* _expose_menuitem;
+
+ /// True iff this is a (flipped) port on a GraphPortModule in its graph
+ bool _internal_graph_port;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_PORTMENU_HPP
diff --git a/src/gui/PropertiesWindow.cpp b/src/gui/PropertiesWindow.cpp
new file mode 100644
index 00000000..ee257843
--- /dev/null
+++ b/src/gui/PropertiesWindow.cpp
@@ -0,0 +1,593 @@
+/*
+ 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 "App.hpp"
+#include "PropertiesWindow.hpp"
+#include "RDFS.hpp"
+#include "URIEntry.hpp"
+
+#include "ingen/Interface.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/URIMap.hpp"
+#include "ingen/World.hpp"
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/client/PluginModel.hpp"
+
+#include <gtkmm/label.h>
+#include <gtkmm/spinbutton.h>
+
+#include <algorithm>
+#include <cfloat>
+#include <climits>
+#include <cstdint>
+#include <set>
+#include <utility>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+typedef std::set<URI> URISet;
+
+PropertiesWindow::PropertiesWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Window(cobject)
+ , _value_type(0)
+{
+ xml->get_widget("properties_vbox", _vbox);
+ xml->get_widget("properties_scrolledwindow", _scrolledwindow);
+ xml->get_widget("properties_table", _table);
+ xml->get_widget("properties_key_combo", _key_combo);
+ xml->get_widget("properties_value_bin", _value_bin);
+ xml->get_widget("properties_add_button", _add_button);
+ xml->get_widget("properties_cancel_button", _cancel_button);
+ xml->get_widget("properties_apply_button", _apply_button);
+ xml->get_widget("properties_ok_button", _ok_button);
+
+ _key_store = Gtk::ListStore::create(_combo_columns);
+ _key_combo->set_model(_key_store);
+ _key_combo->pack_start(_combo_columns.label_col);
+
+ _key_combo->signal_changed().connect(
+ sigc::mem_fun(this, &PropertiesWindow::key_changed));
+
+ _add_button->signal_clicked().connect(
+ sigc::mem_fun(this, &PropertiesWindow::add_clicked));
+
+ _cancel_button->signal_clicked().connect(
+ sigc::mem_fun(this, &PropertiesWindow::cancel_clicked));
+
+ _apply_button->signal_clicked().connect(
+ sigc::mem_fun(this, &PropertiesWindow::apply_clicked));
+
+ _ok_button->signal_clicked().connect(
+ sigc::mem_fun(this, &PropertiesWindow::ok_clicked));
+}
+
+void
+PropertiesWindow::reset()
+{
+ _property_connection.disconnect();
+ _property_removed_connection.disconnect();
+
+ _key_store->clear();
+ _records.clear();
+
+ _model.reset();
+
+ _table->children().clear();
+ _table->resize(1, 3);
+ _table->property_n_rows() = 1;
+}
+
+void
+PropertiesWindow::present(SPtr<const ObjectModel> model)
+{
+ set_object(model);
+ Gtk::Window::present();
+}
+
+void
+PropertiesWindow::add_property(const URI& key, const Atom& value)
+{
+ World& world = _app->world();
+
+ const unsigned n_rows = _table->property_n_rows() + 1;
+ _table->property_n_rows() = n_rows;
+
+ // Column 0: Property
+ LilvNode* prop = lilv_new_uri(world.lilv_world(), key.c_str());
+ std::string name = rdfs::label(world, prop);
+ if (name.empty()) {
+ name = world.rdf_world()->prefixes().qualify(key);
+ }
+ Gtk::Label* label = new Gtk::Label(
+ std::string("<a href=\"") + key.string() + "\">" + name + "</a>",
+ 1.0,
+ 0.5);
+ label->set_use_markup(true);
+ _app->set_tooltip(label, prop);
+ _table->attach(*Gtk::manage(label), 0, 1, n_rows, n_rows + 1,
+ Gtk::FILL|Gtk::SHRINK, Gtk::SHRINK);
+
+ // Column 1: Value
+ Gtk::Alignment* align = manage(new Gtk::Alignment(0.0, 0.5, 1.0, 1.0));
+ Gtk::CheckButton* present = manage(new Gtk::CheckButton());
+ const char* type = _app->world().uri_map().unmap_uri(value.type());
+ Gtk::Widget* val_widget = create_value_widget(key, type, value);
+
+ present->set_active();
+ if (val_widget) {
+ align->add(*Gtk::manage(val_widget));
+ _app->set_tooltip(val_widget, prop);
+ }
+
+ _table->attach(*align, 1, 2, n_rows, n_rows + 1,
+ Gtk::FILL|Gtk::EXPAND, Gtk::SHRINK);
+ _table->attach(*present, 2, 3, n_rows, n_rows + 1,
+ Gtk::FILL, Gtk::SHRINK);
+ _records.emplace(key, Record(value, align, n_rows, present));
+ _table->show_all();
+
+ lilv_node_free(prop);
+}
+
+bool
+PropertiesWindow::datatype_supported(const rdfs::URISet& types,
+ URI* widget_type)
+{
+ if (types.find(_app->uris().atom_Int) != types.end()) {
+ *widget_type = _app->uris().atom_Int;
+ return true;
+ } else if (types.find(_app->uris().atom_Float) != types.end()) {
+ *widget_type = _app->uris().atom_Float;
+ return true;
+ } else if (types.find(_app->uris().atom_Bool) != types.end()) {
+ *widget_type = _app->uris().atom_Bool;
+ return true;
+ } else if (types.find(_app->uris().atom_String) != types.end()) {
+ *widget_type = _app->uris().atom_String;
+ return true;
+ } else if (types.find(_app->uris().atom_URID) != types.end()) {
+ *widget_type = _app->uris().atom_URID;
+ return true;
+ }
+
+ return false;
+}
+
+bool
+PropertiesWindow::class_supported(const rdfs::URISet& types)
+{
+ World& world = _app->world();
+ LilvNode* rdf_type = lilv_new_uri(world.lilv_world(), LILV_NS_RDF "type");
+
+ for (const auto& t : types) {
+ LilvNode* range = lilv_new_uri(world.lilv_world(), t.c_str());
+ LilvNodes* instances = lilv_world_find_nodes(
+ world.lilv_world(), nullptr, rdf_type, range);
+
+ const bool has_instance = (lilv_nodes_size(instances) > 0);
+
+ lilv_nodes_free(instances);
+ lilv_node_free(range);
+ if (has_instance) {
+ lilv_node_free(rdf_type);
+ return true;
+ }
+ }
+
+ lilv_node_free(rdf_type);
+ return false;
+}
+
+/** Set the node this window is associated with.
+ * This function MUST be called before using this object in any way.
+ */
+void
+PropertiesWindow::set_object(SPtr<const ObjectModel> model)
+{
+ reset();
+ _model = model;
+
+ set_title(model->path() + " Properties - Ingen");
+
+ World& world = _app->world();
+
+ LilvNode* rdf_type = lilv_new_uri(
+ world.lilv_world(), LILV_NS_RDF "type");
+ LilvNode* rdfs_DataType = lilv_new_uri(
+ world.lilv_world(), LILV_NS_RDFS "Datatype");
+
+ // Populate key combo
+ const URISet props = rdfs::properties(world, model);
+ std::map<std::string, URI> entries;
+ for (const auto& p : props) {
+ LilvNode* prop = lilv_new_uri(world.lilv_world(), p.c_str());
+ const std::string label = rdfs::label(world, prop);
+ URISet ranges = rdfs::range(world, prop, true);
+
+ lilv_node_free(prop);
+ if (label.empty() || ranges.empty()) {
+ // Property has no label or range, can't show a widget for it
+ continue;
+ }
+
+ LilvNode* range = lilv_new_uri(world.lilv_world(), (*ranges.begin()).c_str());
+ if (rdfs::is_a(world, range, rdfs_DataType)) {
+ // Range is a datatype, show if type or any subtype is supported
+ rdfs::datatypes(_app->world(), ranges, false);
+ URI widget_type("urn:nothing");
+ if (datatype_supported(ranges, &widget_type)) {
+ entries.emplace(label, p);
+ }
+ } else {
+ // Range is presumably a class, show if any instances are known
+ if (class_supported(ranges)) {
+ entries.emplace(label, p);
+ }
+ }
+ }
+
+ for (const auto& e : entries) {
+ Gtk::ListStore::iterator ki = _key_store->append();
+ Gtk::ListStore::Row row = *ki;
+ row[_combo_columns.uri_col] = e.second.string();
+ row[_combo_columns.label_col] = e.first;
+ }
+
+ lilv_node_free(rdfs_DataType);
+ lilv_node_free(rdf_type);
+
+ for (const auto& p : model->properties()) {
+ add_property(p.first, p.second);
+ }
+
+ _table->show_all();
+
+ _property_connection = model->signal_property().connect(
+ sigc::mem_fun(this, &PropertiesWindow::add_property));
+ _property_removed_connection = model->signal_property_removed().connect(
+ sigc::mem_fun(this, &PropertiesWindow::remove_property));
+}
+
+Gtk::Widget*
+PropertiesWindow::create_value_widget(const URI& key,
+ const char* type_uri,
+ const Atom& value)
+{
+ if (!type_uri || !URI::is_valid(type_uri)) {
+ return nullptr;
+ }
+
+ URI type(type_uri);
+ ingen::World& world = _app->world();
+ LilvWorld* lworld = world.lilv_world();
+
+ // See if type is a datatype we support
+ std::set<URI> types{type};
+ rdfs::datatypes(world, types, false);
+
+ URI widget_type("urn:nothing");
+ const bool supported = datatype_supported(types, &widget_type);
+ if (supported) {
+ type = widget_type;
+ _value_type = world.uri_map().map_uri(type);
+ }
+
+ if (type == _app->uris().atom_Int) {
+ Gtk::SpinButton* widget = manage(new Gtk::SpinButton(0.0, 0));
+ widget->property_numeric() = true;
+ widget->set_range(INT_MIN, INT_MAX);
+ widget->set_increments(1, 10);
+ if (value.is_valid()) {
+ widget->set_value(value.get<int32_t>());
+ }
+ widget->signal_value_changed().connect(
+ sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key));
+ return widget;
+ } else if (type == _app->uris().atom_Float) {
+ Gtk::SpinButton* widget = manage(new Gtk::SpinButton(0.0, 4));
+ widget->property_numeric() = true;
+ widget->set_snap_to_ticks(false);
+ widget->set_range(-FLT_MAX, FLT_MAX);
+ widget->set_increments(0.1, 1.0);
+ if (value.is_valid()) {
+ widget->set_value(value.get<float>());
+ }
+ widget->signal_value_changed().connect(
+ sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key));
+ return widget;
+ } else if (type == _app->uris().atom_Bool) {
+ Gtk::CheckButton* widget = manage(new Gtk::CheckButton());
+ if (value.is_valid()) {
+ widget->set_active(value.get<int32_t>());
+ }
+ widget->signal_toggled().connect(
+ sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key));
+ return widget;
+ } else if (type == _app->uris().atom_String) {
+ Gtk::Entry* widget = manage(new Gtk::Entry());
+ if (value.is_valid()) {
+ widget->set_text(value.ptr<char>());
+ }
+ widget->signal_changed().connect(
+ sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key));
+ return widget;
+ } else if (type == _app->uris().atom_URID) {
+ const char* str = (value.is_valid()
+ ? world.uri_map().unmap_uri(value.get<int32_t>())
+ : "");
+
+ LilvNode* pred = lilv_new_uri(lworld, key.c_str());
+ URISet ranges = rdfs::range(world, pred, true);
+ URIEntry* widget = manage(new URIEntry(_app, ranges, str ? str : ""));
+ widget->signal_changed().connect(
+ sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key));
+ lilv_node_free(pred);
+ return widget;
+ }
+
+ LilvNode* type_node = lilv_new_uri(lworld, type.c_str());
+ LilvNode* rdfs_Class = lilv_new_uri(lworld, LILV_NS_RDFS "Class");
+ const bool is_class = rdfs::is_a(world, type_node, rdfs_Class);
+ lilv_node_free(rdfs_Class);
+ lilv_node_free(type_node);
+
+ if (type == _app->uris().atom_URI ||
+ type == _app->uris().rdfs_Class ||
+ is_class) {
+ LilvNode* pred = lilv_new_uri(lworld, key.c_str());
+ URISet ranges = rdfs::range(world, pred, true);
+ const char* str = value.is_valid() ? value.ptr<const char>() : "";
+ URIEntry* widget = manage(new URIEntry(_app, ranges, str));
+ widget->signal_changed().connect(
+ sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key));
+ lilv_node_free(pred);
+ return widget;
+ }
+
+ _app->log().error("No widget for value type %1%\n", type);
+
+ return nullptr;
+}
+
+void
+PropertiesWindow::on_show()
+{
+ static const int WIN_PAD = 64;
+ static const int VBOX_PAD = 16;
+
+ int width = 0;
+ int height = 0;
+
+ for (const auto& c : _vbox->children()) {
+ const Gtk::Requisition& req = c.get_widget()->size_request();
+
+ width = std::max(width, req.width);
+ height += req.height + VBOX_PAD;
+ }
+
+ const Gtk::Requisition& req = _table->size_request();
+
+ width = 1.2 * std::max(width, req.width + 128);
+ height += req.height;
+
+ set_default_size(width + WIN_PAD, height + WIN_PAD);
+ resize(width + WIN_PAD, height + WIN_PAD);
+ Gtk::Window::on_show();
+}
+
+void
+PropertiesWindow::change_property(const URI& key, const Atom& value)
+{
+ auto r = _records.find(key);
+ if (r == _records.end()) {
+ add_property(key, value);
+ _table->show_all();
+ return;
+ }
+
+ Record& record = r->second;
+ const char* type = _app->world().uri_map().unmap_uri(value.type());
+ Gtk::Widget* val_widget = create_value_widget(key, type, value);
+
+ if (val_widget) {
+ record.value_widget->remove();
+ record.value_widget->add(*Gtk::manage(val_widget));
+ val_widget->show_all();
+ }
+
+ record.value = value;
+}
+
+void
+PropertiesWindow::remove_property(const URI& key, const Atom& value)
+{
+ // Bleh, there doesn't seem to be an easy way to remove a Gtk::Table row...
+ _records.clear();
+ _table->children().clear();
+ _table->resize(1, 3);
+ _table->property_n_rows() = 1;
+
+ for (const auto& p : _model->properties()) {
+ add_property(p.first, p.second);
+ }
+ _table->show_all();
+}
+
+Atom
+PropertiesWindow::get_value(LV2_URID type, Gtk::Widget* value_widget)
+{
+ Forge& forge = _app->forge();
+
+ if (type == forge.Int) {
+ Gtk::SpinButton* spin = dynamic_cast<Gtk::SpinButton*>(value_widget);
+ if (spin) {
+ return _app->forge().make(spin->get_value_as_int());
+ }
+ } else if (type == forge.Float) {
+ Gtk::SpinButton* spin = dynamic_cast<Gtk::SpinButton*>(value_widget);
+ if (spin) {
+ return _app->forge().make(static_cast<float>(spin->get_value()));
+ }
+ } else if (type == forge.Bool) {
+ Gtk::CheckButton* check = dynamic_cast<Gtk::CheckButton*>(value_widget);
+ if (check) {
+ return _app->forge().make(check->get_active());
+ }
+ } else if (type == forge.URI || type == forge.URID) {
+ URIEntry* uri_entry = dynamic_cast<URIEntry*>(value_widget);
+ if (uri_entry && URI::is_valid(uri_entry->get_text())) {
+ return _app->forge().make_urid(URI(uri_entry->get_text()));
+ } else {
+ _app->log().error("Invalid URI <%1%>\n", uri_entry->get_text());
+ }
+ } else if (type == forge.String) {
+ Gtk::Entry* entry = dynamic_cast<Gtk::Entry*>(value_widget);
+ if (entry) {
+ return _app->forge().alloc(entry->get_text());
+ }
+ }
+
+ return Atom();
+}
+
+void
+PropertiesWindow::on_change(const URI& key)
+{
+ auto r = _records.find(key);
+ if (r == _records.end()) {
+ return;
+ }
+
+ Record& record = r->second;
+ const Atom value = get_value(record.value.type(),
+ record.value_widget->get_child());
+
+ if (value.is_valid()) {
+ record.value = value;
+ } else {
+ _app->log().error("Failed to get `%1%' value from widget\n", key);
+ }
+}
+
+std::string
+PropertiesWindow::active_key() const
+{
+ const Gtk::ListStore::iterator iter = _key_combo->get_active();
+ if (!iter) {
+ return "";
+ }
+
+ Glib::ustring prop_uri = (*iter)[_combo_columns.uri_col];
+ return prop_uri;
+}
+
+void
+PropertiesWindow::key_changed()
+{
+ _value_bin->remove();
+ if (!_key_combo->get_active()) {
+ return;
+ }
+
+ LilvWorld* lworld = _app->world().lilv_world();
+ const Gtk::ListStore::Row key_row = *(_key_combo->get_active());
+ const Glib::ustring key_uri = key_row[_combo_columns.uri_col];
+ LilvNode* prop = lilv_new_uri(lworld, key_uri.c_str());
+
+ // Try to create a value widget in the range of this property
+ const URISet ranges = rdfs::range(_app->world(), prop, true);
+ for (const auto& r : ranges) {
+ Gtk::Widget* value_widget = create_value_widget(
+ URI(key_uri), r.c_str(), Atom());
+
+ if (value_widget) {
+ _add_button->set_sensitive(true);
+ _value_bin->remove();
+ _value_bin->add(*Gtk::manage(value_widget));
+ _value_bin->show_all();
+ break;
+ }
+ }
+
+ lilv_node_free(prop);
+}
+
+void
+PropertiesWindow::add_clicked()
+{
+ if (!_key_combo->get_active() || !_value_type || !_value_bin->get_child()) {
+ return;
+ }
+
+ // Get selected key URI
+ const Gtk::ListStore::Row key_row = *(_key_combo->get_active());
+ const Glib::ustring key_uri = key_row[_combo_columns.uri_col];
+
+ // Try to get value from value widget
+ const Atom& value = get_value(_value_type, _value_bin->get_child());
+ if (value.is_valid()) {
+ // Send property to engine
+ Properties properties;
+ properties.emplace(URI(key_uri.c_str()), Property(value));
+ _app->interface()->put(_model->uri(), properties);
+ }
+}
+
+void
+PropertiesWindow::cancel_clicked()
+{
+ reset();
+ Gtk::Window::hide();
+}
+
+void
+PropertiesWindow::apply_clicked()
+{
+ Properties remove;
+ Properties add;
+ for (const auto& r : _records) {
+ const URI& uri = r.first;
+ const Record& record = r.second;
+ if (record.present_button->get_active()) {
+ if (!_model->has_property(uri, record.value)) {
+ add.emplace(uri, record.value);
+ }
+ } else {
+ remove.emplace(uri, record.value);
+ }
+ }
+
+ if (remove.empty()) {
+ _app->interface()->put(_model->uri(), add);
+ } else {
+ _app->interface()->delta(_model->uri(), remove, add);
+ }
+}
+
+void
+PropertiesWindow::ok_clicked()
+{
+ apply_clicked();
+ Gtk::Window::hide();
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/PropertiesWindow.hpp b/src/gui/PropertiesWindow.hpp
new file mode 100644
index 00000000..c847da20
--- /dev/null
+++ b/src/gui/PropertiesWindow.hpp
@@ -0,0 +1,131 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_PROPERTIES_WINDOW_HPP
+#define INGEN_GUI_PROPERTIES_WINDOW_HPP
+
+#include "Window.hpp"
+
+#include "ingen/client/BlockModel.hpp"
+#include "ingen/types.hpp"
+
+#include <gtkmm/alignment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/table.h>
+
+#include <map>
+#include <set>
+#include <string>
+
+namespace ingen {
+
+namespace client { class ObjectModel; }
+
+namespace gui {
+
+/** Object properties window.
+ *
+ * Loaded from XML as a derived object.
+ *
+ * \ingroup GUI
+ */
+class PropertiesWindow : public Window
+{
+public:
+ PropertiesWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ void present(SPtr<const client::ObjectModel> model);
+ void set_object(SPtr<const client::ObjectModel> model);
+
+private:
+ /** Record of a property (row in the table) */
+ struct Record {
+ Record(const Atom& v, Gtk::Alignment* vw, int r, Gtk::CheckButton* cb)
+ : value(v), value_widget(vw), row(r), present_button(cb)
+ {}
+ Atom value;
+ Gtk::Alignment* value_widget;
+ int row;
+ Gtk::CheckButton* present_button;
+ };
+
+ struct ComboColumns : public Gtk::TreeModel::ColumnRecord {
+ ComboColumns() {
+ add(label_col);
+ add(uri_col);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> label_col;
+ Gtk::TreeModelColumn<Glib::ustring> uri_col;
+ };
+
+ void add_property(const URI& key, const Atom& value);
+ void change_property(const URI& key, const Atom& value);
+ void remove_property(const URI& key, const Atom& value);
+ void on_change(const URI& key);
+
+ bool datatype_supported(const std::set<URI>& types,
+ URI* widget_type);
+
+ bool class_supported(const std::set<URI>& types);
+
+ Gtk::Widget* create_value_widget(const URI& key,
+ const char* type_uri,
+ const Atom& value = Atom());
+
+ Atom get_value(LV2_URID type, Gtk::Widget* value_widget);
+
+ void reset();
+ void on_show() override;
+
+ std::string active_key() const;
+
+ void key_changed();
+ void add_clicked();
+ void cancel_clicked();
+ void apply_clicked();
+ void ok_clicked();
+
+ typedef std::map<URI, Record> Records;
+ Records _records;
+
+ SPtr<const client::ObjectModel> _model;
+ ComboColumns _combo_columns;
+ Glib::RefPtr<Gtk::ListStore> _key_store;
+ sigc::connection _property_connection;
+ sigc::connection _property_removed_connection;
+ Gtk::VBox* _vbox;
+ Gtk::ScrolledWindow* _scrolledwindow;
+ Gtk::Table* _table;
+ Gtk::ComboBox* _key_combo;
+ LV2_URID _value_type;
+ Gtk::Bin* _value_bin;
+ Gtk::Button* _add_button;
+ Gtk::Button* _cancel_button;
+ Gtk::Button* _apply_button;
+ Gtk::Button* _ok_button;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_PROPERTIES_WINDOW_HPP
diff --git a/src/gui/RDFS.cpp b/src/gui/RDFS.cpp
new file mode 100644
index 00000000..0b4a4e62
--- /dev/null
+++ b/src/gui/RDFS.cpp
@@ -0,0 +1,261 @@
+/*
+ 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/Forge.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/Resource.hpp"
+#include "ingen/World.hpp"
+#include "ingen/client/ObjectModel.hpp"
+#include "lilv/lilv.h"
+
+#include "RDFS.hpp"
+
+#include <utility>
+
+namespace ingen {
+namespace gui {
+namespace rdfs {
+
+std::string
+label(World& world, const LilvNode* node)
+{
+ LilvNode* rdfs_label = lilv_new_uri(
+ world.lilv_world(), LILV_NS_RDFS "label");
+ LilvNodes* labels = lilv_world_find_nodes(
+ world.lilv_world(), node, rdfs_label, nullptr);
+
+ const LilvNode* first = lilv_nodes_get_first(labels);
+ std::string label = first ? lilv_node_as_string(first) : "";
+
+ lilv_nodes_free(labels);
+ lilv_node_free(rdfs_label);
+ return label;
+}
+
+std::string
+comment(World& world, const LilvNode* node)
+{
+ LilvNode* rdfs_comment = lilv_new_uri(
+ world.lilv_world(), LILV_NS_RDFS "comment");
+ LilvNodes* comments = lilv_world_find_nodes(
+ world.lilv_world(), node, rdfs_comment, nullptr);
+
+ const LilvNode* first = lilv_nodes_get_first(comments);
+ std::string comment = first ? lilv_node_as_string(first) : "";
+
+ lilv_nodes_free(comments);
+ lilv_node_free(rdfs_comment);
+ return comment;
+}
+
+static void
+closure(World& world, const LilvNode* pred, URISet& types, bool super)
+{
+ unsigned added = 0;
+ do {
+ added = 0;
+ URISet klasses;
+ for (const auto& t : types) {
+ LilvNode* type = lilv_new_uri(world.lilv_world(), t.c_str());
+ LilvNodes* matches = (super)
+ ? lilv_world_find_nodes(
+ world.lilv_world(), type, pred, nullptr)
+ : lilv_world_find_nodes(
+ world.lilv_world(), nullptr, pred, type);
+ LILV_FOREACH(nodes, m, matches) {
+ const LilvNode* klass_node = lilv_nodes_get(matches, m);
+ if (lilv_node_is_uri(klass_node)) {
+ URI klass(lilv_node_as_uri(klass_node));
+ if (!types.count(klass)) {
+ ++added;
+ klasses.insert(klass);
+ }
+ }
+ }
+ lilv_nodes_free(matches);
+ lilv_node_free(type);
+ }
+ types.insert(klasses.begin(), klasses.end());
+ } while (added > 0);
+}
+
+void
+classes(World& world, URISet& types, bool super)
+{
+ LilvNode* rdfs_subClassOf = lilv_new_uri(
+ world.lilv_world(), LILV_NS_RDFS "subClassOf");
+
+ closure(world, rdfs_subClassOf, types, super);
+
+ lilv_node_free(rdfs_subClassOf);
+}
+
+void
+datatypes(World& world, URISet& types, bool super)
+{
+ LilvNode* owl_onDatatype = lilv_new_uri(
+ world.lilv_world(), LILV_NS_OWL "onDatatype");
+
+ closure(world, owl_onDatatype, types, super);
+
+ lilv_node_free(owl_onDatatype);
+}
+
+URISet
+types(World& world, SPtr<const client::ObjectModel> model)
+{
+ typedef Properties::const_iterator PropIter;
+ typedef std::pair<PropIter, PropIter> PropRange;
+
+ // Start with every rdf:type
+ URISet types;
+ types.insert(URI(LILV_NS_RDFS "Resource"));
+ PropRange range = model->properties().equal_range(world.uris().rdf_type);
+ for (auto t = range.first; t != range.second; ++t) {
+ if (t->second.type() == world.forge().URI ||
+ t->second.type() == world.forge().URID) {
+ const URI type(world.forge().str(t->second, false));
+ types.insert(type);
+ if (world.uris().ingen_Graph == type) {
+ // Add lv2:Plugin as a type for graphs so plugin properties show up
+ types.insert(world.uris().lv2_Plugin);
+ }
+ } else {
+ world.log().error("<%1%> has non-URI type\n", model->uri());
+ }
+ }
+
+ // Add every superclass of every type, recursively
+ rdfs::classes(world, types, true);
+
+ return types;
+}
+
+URISet
+properties(World& world, SPtr<const client::ObjectModel> model)
+{
+ URISet properties;
+ URISet types = rdfs::types(world, model);
+
+ LilvNode* rdf_type = lilv_new_uri(world.lilv_world(),
+ LILV_NS_RDF "type");
+ LilvNode* rdf_Property = lilv_new_uri(world.lilv_world(),
+ LILV_NS_RDF "Property");
+ LilvNode* rdfs_domain = lilv_new_uri(world.lilv_world(),
+ LILV_NS_RDFS "domain");
+
+ LilvNodes* props = lilv_world_find_nodes(
+ world.lilv_world(), nullptr, rdf_type, rdf_Property);
+ LILV_FOREACH(nodes, p, props) {
+ const LilvNode* prop = lilv_nodes_get(props, p);
+ if (lilv_node_is_uri(prop)) {
+ LilvNodes* domains = lilv_world_find_nodes(
+ world.lilv_world(), prop, rdfs_domain, nullptr);
+ unsigned n_matching_domains = 0;
+ LILV_FOREACH(nodes, d, domains) {
+ const LilvNode* domain_node = lilv_nodes_get(domains, d);
+ if (!lilv_node_is_uri(domain_node)) {
+ // TODO: Blank node domains (e.g. unions)
+ continue;
+ }
+
+ const URI domain(lilv_node_as_uri(domain_node));
+ if (types.count(domain)) {
+ ++n_matching_domains;
+ }
+ }
+
+ if (lilv_nodes_size(domains) == 0 || (
+ n_matching_domains > 0 &&
+ n_matching_domains == lilv_nodes_size(domains))) {
+ properties.insert(URI(lilv_node_as_uri(prop)));
+ }
+
+ lilv_nodes_free(domains);
+ }
+ }
+
+ lilv_node_free(rdfs_domain);
+ lilv_node_free(rdf_Property);
+ lilv_node_free(rdf_type);
+
+ return properties;
+}
+
+Objects
+instances(World& world, const URISet& types)
+{
+ LilvNode* rdf_type = lilv_new_uri(
+ world.lilv_world(), LILV_NS_RDF "type");
+
+ Objects result;
+ for (const auto& t : types) {
+ LilvNode* type = lilv_new_uri(world.lilv_world(), t.c_str());
+ LilvNodes* objects = lilv_world_find_nodes(
+ world.lilv_world(), nullptr, rdf_type, type);
+ LILV_FOREACH(nodes, o, objects) {
+ const LilvNode* object = lilv_nodes_get(objects, o);
+ if (!lilv_node_is_uri(object)) {
+ continue;
+ }
+ const std::string label = rdfs::label(world, object);
+ result.emplace(label, URI(lilv_node_as_string(object)));
+ }
+ lilv_node_free(type);
+ }
+
+ lilv_node_free(rdf_type);
+ return result;
+}
+
+URISet
+range(World& world, const LilvNode* prop, bool recursive)
+{
+ LilvNode* rdfs_range = lilv_new_uri(
+ world.lilv_world(), LILV_NS_RDFS "range");
+
+ LilvNodes* nodes = lilv_world_find_nodes(
+ world.lilv_world(), prop, rdfs_range, nullptr);
+
+ URISet ranges;
+ LILV_FOREACH(nodes, n, nodes) {
+ ranges.insert(URI(lilv_node_as_string(lilv_nodes_get(nodes, n))));
+ }
+
+ if (recursive) {
+ rdfs::classes(world, ranges, false);
+ }
+
+ lilv_nodes_free(nodes);
+ lilv_node_free(rdfs_range);
+ return ranges;
+}
+
+bool
+is_a(World& world, const LilvNode* inst, const LilvNode* klass)
+{
+ LilvNode* rdf_type = lilv_new_uri(world.lilv_world(), LILV_NS_RDF "type");
+
+ const bool is_instance = lilv_world_ask(
+ world.lilv_world(), inst, rdf_type, klass);
+
+ lilv_node_free(rdf_type);
+ return is_instance;
+}
+
+} // namespace rdfs
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/RDFS.hpp b/src/gui/RDFS.hpp
new file mode 100644
index 00000000..4e9a8ab1
--- /dev/null
+++ b/src/gui/RDFS.hpp
@@ -0,0 +1,81 @@
+/*
+ This file is part of Ingen.
+ Copyright 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/>.
+*/
+
+#ifndef INGEN_GUI_RDF_HPP
+#define INGEN_GUI_RDF_HPP
+
+#include "ingen/URI.hpp"
+#include "ingen/types.hpp"
+#include "lilv/lilv.h"
+
+#include <map>
+#include <set>
+#include <string>
+
+namespace ingen {
+
+class World;
+
+namespace client { class ObjectModel; }
+
+namespace gui {
+
+namespace rdfs {
+
+/** Set of URIs. */
+typedef std::set<URI> URISet;
+
+/** Label => Resource map. */
+typedef std::map<std::string, URI> Objects;
+
+/** Return the label of `node`. */
+std::string label(World& world, const LilvNode* node);
+
+/** Return the comment of `node`. */
+std::string comment(World& world, const LilvNode* node);
+
+/** Set `types` to its super/sub class closure.
+ * @param super If true, find all superclasses, otherwise all subclasses
+ */
+void classes(World& world, URISet& types, bool super);
+
+/** Set `types` to its super/sub datatype closure.
+ * @param super If true, find all supertypes, otherwise all subtypes.
+ */
+void datatypes(World& world, URISet& types, bool super);
+
+/** Get all instances of any class in `types`. */
+Objects instances(World& world, const URISet& types);
+
+/** Get all the types which `model` is an instance of. */
+URISet types(World& world, SPtr<const client::ObjectModel> model);
+
+/** Get all the properties with domains appropriate for `model`. */
+URISet properties(World& world, SPtr<const client::ObjectModel> model);
+
+/** Return the range (value types) of `prop`.
+ * @param recursive If true, include all subclasses.
+ */
+URISet range(World& world, const LilvNode* prop, bool recursive);
+
+/** Return true iff `inst` is-a `klass`. */
+bool is_a(World& world, const LilvNode* inst, const LilvNode* klass);
+
+} // namespace rdfs
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_RDF_HPP
diff --git a/src/gui/RenameWindow.cpp b/src/gui/RenameWindow.cpp
new file mode 100644
index 00000000..8c5e9edb
--- /dev/null
+++ b/src/gui/RenameWindow.cpp
@@ -0,0 +1,137 @@
+/*
+ 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 "RenameWindow.hpp"
+
+#include "App.hpp"
+
+#include "ingen/Forge.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/ObjectModel.hpp"
+#include "lv2/core/lv2.h"
+
+#include <string>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+RenameWindow::RenameWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml)
+ : Window(cobject)
+{
+ xml->get_widget("rename_symbol_entry", _symbol_entry);
+ xml->get_widget("rename_label_entry", _label_entry);
+ xml->get_widget("rename_message_label", _message_label);
+ xml->get_widget("rename_cancel_button", _cancel_button);
+ xml->get_widget("rename_ok_button", _ok_button);
+
+ _symbol_entry->signal_changed().connect(
+ sigc::mem_fun(this, &RenameWindow::values_changed));
+ _label_entry->signal_changed().connect(
+ sigc::mem_fun(this, &RenameWindow::values_changed));
+ _cancel_button->signal_clicked().connect(
+ sigc::mem_fun(this, &RenameWindow::cancel_clicked));
+ _ok_button->signal_clicked().connect(
+ sigc::mem_fun(this, &RenameWindow::ok_clicked));
+
+ _ok_button->property_sensitive() = false;
+}
+
+/** Set the object this window is renaming.
+ * This function MUST be called before using this object in any way.
+ */
+void
+RenameWindow::set_object(SPtr<const ObjectModel> object)
+{
+ _object = object;
+ _symbol_entry->set_text(object->path().symbol());
+ const Atom& name_atom = object->get_property(_app->uris().lv2_name);
+ _label_entry->set_text(
+ (name_atom.type() == _app->forge().String) ? name_atom.ptr<char>() : "");
+}
+
+void
+RenameWindow::present(SPtr<const ObjectModel> object)
+{
+ set_object(object);
+ _symbol_entry->grab_focus();
+ Gtk::Window::present();
+}
+
+void
+RenameWindow::values_changed()
+{
+ const std::string& symbol = _symbol_entry->get_text();
+ if (!Raul::Symbol::is_valid(symbol)) {
+ _message_label->set_text("Invalid symbol");
+ _ok_button->property_sensitive() = false;
+ } else if (_object->symbol() != symbol &&
+ _app->store()->object(
+ _object->parent()->path().child(Raul::Symbol(symbol)))) {
+ _message_label->set_text("An object already exists with that path");
+ _ok_button->property_sensitive() = false;
+ } else {
+ _message_label->set_text("");
+ _ok_button->property_sensitive() = true;
+ }
+}
+
+void
+RenameWindow::cancel_clicked()
+{
+ _symbol_entry->set_text("");
+ hide();
+}
+
+/** Rename the object.
+ *
+ * It shouldn't be possible for this to be called with an invalid name set
+ * (since the Rename button should be deactivated). This is just shinification
+ * though - the engine will handle invalid names gracefully.
+ */
+void
+RenameWindow::ok_clicked()
+{
+ const URIs& uris = _app->uris();
+ const std::string& symbol_str = _symbol_entry->get_text();
+ const std::string& label = _label_entry->get_text();
+ Raul::Path path = _object->path();
+ const Atom& name_atom = _object->get_property(uris.lv2_name);
+
+ if (!label.empty() && (name_atom.type() != uris.forge.String ||
+ label != name_atom.ptr<char>())) {
+ _app->set_property(path_to_uri(path),
+ uris.lv2_name,
+ _app->forge().alloc(label));
+ }
+
+ if (Raul::Symbol::is_valid(symbol_str)) {
+ const Raul::Symbol symbol(symbol_str);
+ if (symbol != _object->symbol()) {
+ path = _object->path().parent().child(symbol);
+ _app->interface()->move(_object->path(), path);
+ }
+ }
+
+ hide();
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/RenameWindow.hpp b/src/gui/RenameWindow.hpp
new file mode 100644
index 00000000..3b50f04e
--- /dev/null
+++ b/src/gui/RenameWindow.hpp
@@ -0,0 +1,64 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_RENAMEWINDOW_HPP
+#define INGEN_GUI_RENAMEWINDOW_HPP
+
+#include "Window.hpp"
+
+#include "ingen/client/ObjectModel.hpp"
+#include "ingen/types.hpp"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/label.h>
+
+namespace ingen {
+namespace gui {
+
+/** Rename window. Handles renaming of any (Ingen) object.
+ *
+ * \ingroup GUI
+ */
+class RenameWindow : public Window
+{
+public:
+ RenameWindow(BaseObjectType* cobject,
+ const Glib::RefPtr<Gtk::Builder>& xml);
+
+ void present(SPtr<const client::ObjectModel> object);
+
+private:
+ void set_object(SPtr<const client::ObjectModel> object);
+
+ void values_changed();
+ void cancel_clicked();
+ void ok_clicked();
+
+ SPtr<const client::ObjectModel> _object;
+
+ Gtk::Entry* _symbol_entry;
+ Gtk::Entry* _label_entry;
+ Gtk::Label* _message_label;
+ Gtk::Button* _cancel_button;
+ Gtk::Button* _ok_button;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_RENAMEWINDOW_HPP
diff --git a/src/gui/Style.cpp b/src/gui/Style.cpp
new file mode 100644
index 00000000..81c9da2f
--- /dev/null
+++ b/src/gui/Style.cpp
@@ -0,0 +1,107 @@
+/*
+ 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 "Style.hpp"
+
+#include "App.hpp"
+#include "Port.hpp"
+
+#include "ganv/Port.hpp"
+#include "ingen/Log.hpp"
+#include "ingen/Parser.hpp"
+#include "ingen/client/PluginModel.hpp"
+#include "ingen/client/PortModel.hpp"
+
+#include <cassert>
+#include <cstdlib>
+#include <fstream>
+#include <map>
+#include <string>
+
+namespace ingen {
+namespace gui {
+
+using namespace ingen::client;
+
+Style::Style(App& app)
+ // Colours from the Tango palette with modified V
+ : _app(app)
+#ifdef INGEN_USE_LIGHT_THEME
+ , _audio_port_color(0xC8E6ABFF) // Green
+ , _control_port_color(0xAAC0E6FF) // Blue
+ , _cv_port_color(0xACE6E0FF) // Teal (between audio and control)
+ , _event_port_color(0xE6ABABFF) // Red
+ , _string_port_color(0xD8ABE6FF) // Plum
+#else
+ , _audio_port_color(0x4A8A0EFF) // Green
+ , _control_port_color(0x244678FF) // Blue
+ , _cv_port_color(0x248780FF) // Teal (between audio and control)
+ , _event_port_color(0x960909FF) // Red
+ , _string_port_color(0x5C3566FF) // Plum
+#endif
+{
+}
+
+/** Loads settings from the rc file. Passing no parameter will load from
+ * the default location.
+ */
+void
+Style::load_settings(std::string filename)
+{
+ /* ... */
+}
+
+/** Saves settings to rc file. Passing no parameter will save to the
+ * default location.
+ */
+void
+Style::save_settings(std::string filename)
+{
+ /* ... */
+}
+
+/** Applies the current loaded settings to whichever parts of the app
+ * need updating.
+ */
+void
+Style::apply_settings()
+{
+ /* ... */
+}
+
+uint32_t
+Style::get_port_color(const client::PortModel* p)
+{
+ const URIs& uris = _app.uris();
+ if (p->is_a(uris.lv2_AudioPort)) {
+ return _audio_port_color;
+ } else if (p->is_a(uris.lv2_ControlPort)) {
+ return _control_port_color;
+ } else if (p->is_a(uris.lv2_CVPort)) {
+ return _cv_port_color;
+ } else if (p->supports(uris.atom_String)) {
+ return _string_port_color;
+ } else if (_app.can_control(p)) {
+ return _control_port_color;
+ } else if (p->is_a(uris.atom_AtomPort)) {
+ return _event_port_color;
+ }
+
+ return 0x555555FF;
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/Style.hpp b/src/gui/Style.hpp
new file mode 100644
index 00000000..bb403ffd
--- /dev/null
+++ b/src/gui/Style.hpp
@@ -0,0 +1,56 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_STYLE_HPP
+#define INGEN_GUI_STYLE_HPP
+
+#include <cstdint>
+#include <string>
+
+namespace ingen { namespace client { class PortModel; } }
+
+namespace ingen {
+namespace gui {
+
+class App;
+class Port;
+
+class Style
+{
+public:
+ explicit Style(App& app);
+
+ void load_settings(std::string filename = "");
+ void save_settings(std::string filename = "");
+
+ void apply_settings();
+
+ uint32_t get_port_color(const client::PortModel* p);
+
+private:
+ App& _app;
+
+ uint32_t _audio_port_color;
+ uint32_t _control_port_color;
+ uint32_t _cv_port_color;
+ uint32_t _event_port_color;
+ uint32_t _string_port_color;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_STYLE_HPP
diff --git a/src/gui/SubgraphModule.cpp b/src/gui/SubgraphModule.cpp
new file mode 100644
index 00000000..52c37787
--- /dev/null
+++ b/src/gui/SubgraphModule.cpp
@@ -0,0 +1,103 @@
+/*
+ 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 "SubgraphModule.hpp"
+
+#include "App.hpp"
+#include "NodeModule.hpp"
+#include "GraphCanvas.hpp"
+#include "GraphWindow.hpp"
+#include "Port.hpp"
+#include "WindowFactory.hpp"
+
+#include "ingen/Interface.hpp"
+#include "ingen/client/GraphModel.hpp"
+
+#include <cassert>
+#include <utility>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+SubgraphModule::SubgraphModule(GraphCanvas& canvas,
+ SPtr<const GraphModel> graph)
+ : NodeModule(canvas, graph)
+ , _graph(graph)
+{
+ assert(graph);
+}
+
+bool
+SubgraphModule::on_double_click(GdkEventButton* event)
+{
+ assert(_graph);
+
+ SPtr<GraphModel> parent = dynamic_ptr_cast<GraphModel>(_graph->parent());
+
+ GraphWindow* const preferred = ( (parent && (event->state & GDK_SHIFT_MASK))
+ ? nullptr
+ : app().window_factory()->graph_window(parent) );
+
+ app().window_factory()->present_graph(_graph, preferred);
+ return true;
+}
+
+void
+SubgraphModule::store_location(double ax, double ay)
+{
+ const URIs& uris = app().uris();
+
+ const Atom x(app().forge().make(static_cast<float>(ax)));
+ const Atom y(app().forge().make(static_cast<float>(ay)));
+
+ if (x != _block->get_property(uris.ingen_canvasX) ||
+ y != _block->get_property(uris.ingen_canvasY))
+ {
+ app().interface()->put(_graph->uri(),
+ {{uris.ingen_canvasX, x},
+ {uris.ingen_canvasY, y}},
+ Resource::Graph::EXTERNAL);
+ }
+}
+
+/** Browse to this graph in current (parent's) window
+ * (unless an existing window is displaying it)
+ */
+void
+SubgraphModule::browse_to_graph()
+{
+ assert(_graph->parent());
+
+ SPtr<GraphModel> parent = dynamic_ptr_cast<GraphModel>(_graph->parent());
+
+ GraphWindow* const preferred = (parent)
+ ? app().window_factory()->graph_window(parent)
+ : nullptr;
+
+ app().window_factory()->present_graph(_graph, preferred);
+}
+
+void
+SubgraphModule::menu_remove()
+{
+ app().interface()->del(_graph->uri());
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/SubgraphModule.hpp b/src/gui/SubgraphModule.hpp
new file mode 100644
index 00000000..6f4e1c4c
--- /dev/null
+++ b/src/gui/SubgraphModule.hpp
@@ -0,0 +1,64 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_SUBGRAPHMODULE_HPP
+#define INGEN_GUI_SUBGRAPHMODULE_HPP
+
+#include "ingen/types.hpp"
+
+#include "NodeModule.hpp"
+#include "GraphPortModule.hpp"
+
+namespace ingen { namespace client {
+class GraphModel;
+class GraphWindow;
+class PortModel;
+} }
+
+namespace ingen {
+namespace gui {
+
+class GraphCanvas;
+
+/** A module to represent a subgraph
+ *
+ * \ingroup GUI
+ */
+class SubgraphModule : public NodeModule
+{
+public:
+ SubgraphModule(GraphCanvas& canvas,
+ SPtr<const client::GraphModel> graph);
+
+ virtual ~SubgraphModule() {}
+
+ bool on_double_click(GdkEventButton* event) override;
+
+ void store_location(double ax, double ay) override;
+
+ void browse_to_graph();
+ void menu_remove();
+
+ SPtr<const client::GraphModel> graph() const { return _graph; }
+
+protected:
+ SPtr<const client::GraphModel> _graph;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_SUBGRAPHMODULE_HPP
diff --git a/src/gui/ThreadedLoader.cpp b/src/gui/ThreadedLoader.cpp
new file mode 100644
index 00000000..45ac4f7f
--- /dev/null
+++ b/src/gui/ThreadedLoader.cpp
@@ -0,0 +1,149 @@
+/*
+ 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 "ThreadedLoader.hpp"
+
+#include "App.hpp"
+
+#include "ingen/Log.hpp"
+#include "ingen/Module.hpp"
+#include "ingen/World.hpp"
+#include "ingen/client/GraphModel.hpp"
+
+#include <cassert>
+#include <string>
+
+using boost::optional;
+
+namespace ingen {
+namespace gui {
+
+ThreadedLoader::ThreadedLoader(App& app, SPtr<Interface> engine)
+ : _app(app)
+ , _sem(0)
+ , _engine(std::move(engine))
+ , _exit_flag(false)
+ , _thread(&ThreadedLoader::run, this)
+{
+ if (!parser()) {
+ app.log().warn("Parser unavailable, graph loading disabled\n");
+ }
+}
+
+ThreadedLoader::~ThreadedLoader()
+{
+ _exit_flag = true;
+ _sem.post();
+ if (_thread.joinable()) {
+ _thread.join();
+ }
+}
+
+SPtr<Parser>
+ThreadedLoader::parser()
+{
+ return _app.world().parser();
+}
+
+void
+ThreadedLoader::run()
+{
+ while (_sem.wait() && !_exit_flag) {
+ std::lock_guard<std::mutex> lock(_mutex);
+ while (!_events.empty()) {
+ _events.front()();
+ _events.pop_front();
+ }
+ }
+}
+
+void
+ThreadedLoader::load_graph(bool merge,
+ const FilePath& file_path,
+ optional<Raul::Path> engine_parent,
+ optional<Raul::Symbol> engine_symbol,
+ optional<Properties> engine_data)
+{
+ std::lock_guard<std::mutex> lock(_mutex);
+
+ Glib::ustring engine_base = "";
+ if (engine_parent) {
+ if (merge) {
+ engine_base = engine_parent.get();
+ } else {
+ engine_base = engine_parent.get().base();
+ }
+ }
+
+ _events.push_back(sigc::hide_return(
+ sigc::bind(sigc::mem_fun(this, &ThreadedLoader::load_graph_event),
+ file_path,
+ engine_parent,
+ engine_symbol,
+ engine_data)));
+
+ _sem.post();
+}
+
+void
+ThreadedLoader::load_graph_event(const FilePath& file_path,
+ optional<Raul::Path> engine_parent,
+ optional<Raul::Symbol> engine_symbol,
+ optional<Properties> engine_data)
+{
+ std::lock_guard<std::mutex> lock(_app.world().rdf_mutex());
+
+ _app.world().parser()->parse_file(_app.world(),
+ *_app.world().interface(),
+ file_path,
+ engine_parent,
+ engine_symbol,
+ engine_data);
+}
+
+void
+ThreadedLoader::save_graph(SPtr<const client::GraphModel> model, const URI& uri)
+{
+ std::lock_guard<std::mutex> lock(_mutex);
+
+ _events.push_back(sigc::hide_return(
+ sigc::bind(sigc::mem_fun(this, &ThreadedLoader::save_graph_event),
+ model,
+ uri)));
+
+ _sem.post();
+}
+
+void
+ThreadedLoader::save_graph_event(SPtr<const client::GraphModel> model,
+ const URI& uri)
+{
+ assert(uri.scheme() == "file");
+ if (_app.serialiser()) {
+ std::lock_guard<std::mutex> lock(_app.world().rdf_mutex());
+
+ if (uri.string().find(".ingen") != std::string::npos) {
+ _app.serialiser()->write_bundle(model, uri);
+ } else {
+ _app.serialiser()->start_to_file(model->path(), uri.file_path());
+ _app.serialiser()->serialise(model);
+ _app.serialiser()->finish();
+ }
+ }
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/ThreadedLoader.hpp b/src/gui/ThreadedLoader.hpp
new file mode 100644
index 00000000..c1e97b6e
--- /dev/null
+++ b/src/gui/ThreadedLoader.hpp
@@ -0,0 +1,99 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_THREADEDLOADER_HPP
+#define INGEN_GUI_THREADEDLOADER_HPP
+
+#include "ingen/FilePath.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/Parser.hpp"
+#include "ingen/Serialiser.hpp"
+#include "raul/Semaphore.hpp"
+
+#include <boost/optional/optional.hpp>
+#include <sigc++/sigc++.h>
+
+#include <list>
+#include <mutex>
+#include <thread>
+#include <utility>
+
+namespace ingen {
+
+class URI;
+
+namespace client { class GraphModel; }
+
+namespace gui {
+
+class App;
+
+/** Thread for loading graph files.
+ *
+ * This is a seperate thread so it can send all the loading message without
+ * blocking everything else, so the app can respond to the incoming events
+ * caused as a result of the graph loading, while the graph loads.
+ *
+ * Implemented as a slave with a list of closures (events) which processes
+ * all events in the (mutex protected) list each time it's whipped.
+ *
+ * \ingroup GUI
+ */
+class ThreadedLoader
+{
+public:
+ ThreadedLoader(App& app,
+ SPtr<Interface> engine);
+
+ ~ThreadedLoader();
+
+ void load_graph(bool merge,
+ const FilePath& file_path,
+ boost::optional<Raul::Path> engine_parent,
+ boost::optional<Raul::Symbol> engine_symbol,
+ boost::optional<Properties> engine_data);
+
+ void save_graph(SPtr<const client::GraphModel> model, const URI& uri);
+
+ SPtr<Parser> parser();
+
+private:
+ void load_graph_event(const FilePath& file_path,
+ boost::optional<Raul::Path> engine_parent,
+ boost::optional<Raul::Symbol> engine_symbol,
+ boost::optional<Properties> engine_data);
+
+ void save_graph_event(SPtr<const client::GraphModel> model,
+ const URI& filename);
+
+ /** Returns nothing and takes no parameters (because they have all been bound) */
+ typedef sigc::slot<void> Closure;
+
+ void run();
+
+ App& _app;
+ Raul::Semaphore _sem;
+ SPtr<Interface> _engine;
+ std::mutex _mutex;
+ std::list<Closure> _events;
+ bool _exit_flag;
+ std::thread _thread;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_LOADERRTHREAD_HPP
diff --git a/src/gui/URIEntry.cpp b/src/gui/URIEntry.cpp
new file mode 100644
index 00000000..9d5249e8
--- /dev/null
+++ b/src/gui/URIEntry.cpp
@@ -0,0 +1,194 @@
+/*
+ This file is part of Ingen.
+ Copyright 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 "URIEntry.hpp"
+
+#include "App.hpp"
+#include "RDFS.hpp"
+
+#include <map>
+#include <utility>
+
+namespace ingen {
+namespace gui {
+
+URIEntry::URIEntry(App* app, std::set<URI> types, const std::string& value)
+ : Gtk::HBox(false, 4)
+ , _app(app)
+ , _types(std::move(types))
+ , _menu_button(Gtk::manage(new Gtk::Button("≡")))
+ , _entry(Gtk::manage(new Gtk::Entry()))
+{
+ pack_start(*_entry, true, true);
+ pack_start(*_menu_button, false, true);
+
+ _entry->set_text(value);
+
+ _menu_button->signal_event().connect(
+ sigc::mem_fun(this, &URIEntry::menu_button_event));
+}
+
+Gtk::Menu*
+URIEntry::build_value_menu()
+{
+ World& world = _app->world();
+ LilvWorld* lworld = world.lilv_world();
+ Gtk::Menu* menu = new Gtk::Menu();
+
+ LilvNode* owl_onDatatype = lilv_new_uri(lworld, LILV_NS_OWL "onDatatype");
+ LilvNode* rdf_type = lilv_new_uri(lworld, LILV_NS_RDF "type");
+ LilvNode* rdfs_Class = lilv_new_uri(lworld, LILV_NS_RDFS "Class");
+ LilvNode* rdfs_Datatype = lilv_new_uri(lworld, LILV_NS_RDFS "Datatype");
+ LilvNode* rdfs_subClassOf = lilv_new_uri(lworld, LILV_NS_RDFS "subClassOf");
+
+ rdfs::Objects values = rdfs::instances(world, _types);
+
+ for (const auto& v : values) {
+ const LilvNode* inst = lilv_new_uri(lworld, v.second.c_str());
+ std::string label = v.first;
+ if (label.empty()) {
+ // No label, show raw URI
+ label = lilv_node_as_string(inst);
+ }
+
+ if (lilv_world_ask(world.lilv_world(), inst, rdf_type, rdfs_Class) ||
+ lilv_world_ask(world.lilv_world(), inst, rdf_type, rdfs_Datatype)) {
+ // This value is a class or datatype...
+ if (!lilv_world_ask(lworld, inst, rdfs_subClassOf, nullptr) &&
+ !lilv_world_ask(lworld, inst, owl_onDatatype, nullptr)) {
+ // ... which is not a subtype of another, add menu
+ add_class_menu_item(menu, inst, label);
+ }
+ } else {
+ // Value is not a class, add item
+ menu->items().push_back(
+ Gtk::Menu_Helpers::MenuElem(
+ std::string("_") + label,
+ sigc::bind(sigc::mem_fun(this, &URIEntry::uri_chosen),
+ std::string(lilv_node_as_uri(inst)))));
+ _app->set_tooltip(&menu->items().back(), inst);
+ }
+ }
+
+ lilv_node_free(owl_onDatatype);
+ lilv_node_free(rdf_type);
+ lilv_node_free(rdfs_Class);
+ lilv_node_free(rdfs_Datatype);
+ lilv_node_free(rdfs_subClassOf);
+
+ return menu;
+}
+
+Gtk::Menu*
+URIEntry::build_subclass_menu(const LilvNode* klass)
+{
+ World& world = _app->world();
+ LilvWorld* lworld = world.lilv_world();
+
+ LilvNode* owl_onDatatype = lilv_new_uri(lworld, LILV_NS_OWL "onDatatype");
+ LilvNode* rdfs_subClassOf = lilv_new_uri(lworld, LILV_NS_RDFS "subClassOf");
+
+ LilvNodes* subclasses = lilv_world_find_nodes(
+ lworld, nullptr, rdfs_subClassOf, klass);
+ LilvNodes* subtypes = lilv_world_find_nodes(
+ lworld, nullptr, owl_onDatatype, klass);
+
+ if (lilv_nodes_size(subclasses) == 0 && lilv_nodes_size(subtypes) == 0) {
+ return nullptr;
+ }
+
+ Gtk::Menu* menu = new Gtk::Menu();
+
+ // Add "header" item for choosing this class itself
+ add_leaf_menu_item(menu, klass, rdfs::label(world, klass));
+ menu->items().push_back(Gtk::Menu_Helpers::SeparatorElem());
+
+ // Put subclasses/types in a map keyed by label (to sort menu)
+ std::map<std::string, const LilvNode*> entries;
+ LILV_FOREACH(nodes, s, subclasses) {
+ const LilvNode* node = lilv_nodes_get(subclasses, s);
+ entries.emplace(rdfs::label(world, node), node);
+ }
+ LILV_FOREACH(nodes, s, subtypes) {
+ const LilvNode* node = lilv_nodes_get(subtypes, s);
+ entries.emplace(rdfs::label(world, node), node);
+ }
+
+ // Add an item (possibly with a submenu) for each subclass/type
+ for (const auto& e : entries) {
+ add_class_menu_item(menu, e.second, e.first);
+ }
+
+ lilv_nodes_free(subtypes);
+ lilv_nodes_free(subclasses);
+ lilv_node_free(rdfs_subClassOf);
+ lilv_node_free(owl_onDatatype);
+
+ return menu;
+}
+
+void
+URIEntry::add_leaf_menu_item(Gtk::Menu* menu,
+ const LilvNode* node,
+ const std::string& label)
+{
+ menu->items().push_back(
+ Gtk::Menu_Helpers::MenuElem(
+ std::string("_") + label,
+ sigc::bind(sigc::mem_fun(this, &URIEntry::uri_chosen),
+ std::string(lilv_node_as_uri(node)))));
+
+ _app->set_tooltip(&menu->items().back(), node);
+}
+
+void
+URIEntry::add_class_menu_item(Gtk::Menu* menu,
+ const LilvNode* klass,
+ const std::string& label)
+{
+ Gtk::Menu* submenu = build_subclass_menu(klass);
+
+ if (submenu) {
+ menu->items().push_back(Gtk::Menu_Helpers::MenuElem(label));
+ menu->items().back().set_submenu(*Gtk::manage(submenu));
+ } else {
+ add_leaf_menu_item(menu, klass, label);
+ }
+
+ _app->set_tooltip(&menu->items().back(), klass);
+}
+
+void
+URIEntry::uri_chosen(const std::string& uri)
+{
+ _entry->set_text(uri);
+}
+
+bool
+URIEntry::menu_button_event(GdkEvent* ev)
+{
+ if (ev->type != GDK_BUTTON_PRESS) {
+ return false;
+ }
+
+ Gtk::Menu* menu = Gtk::manage(build_value_menu());
+ menu->popup(ev->button.button, ev->button.time);
+
+ return true;
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/URIEntry.hpp b/src/gui/URIEntry.hpp
new file mode 100644
index 00000000..535f0805
--- /dev/null
+++ b/src/gui/URIEntry.hpp
@@ -0,0 +1,72 @@
+/*
+ This file is part of Ingen.
+ Copyright 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/>.
+*/
+
+#ifndef INGEN_GUI_URI_ENTRY_HPP
+#define INGEN_GUI_URI_ENTRY_HPP
+
+#include "ingen/URI.hpp"
+#include "lilv/lilv.h"
+
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/menu.h>
+
+#include <set>
+#include <string>
+
+namespace ingen {
+namespace gui {
+
+class App;
+
+class URIEntry : public Gtk::HBox {
+public:
+ /** Create a widget for entering URIs.
+ *
+ * If `types` is given, then a menu button will be shown which pops up a
+ * enu for easily choosing known values with valid types.
+ */
+ URIEntry(App* app, std::set<URI> types, const std::string& value);
+
+ std::string get_text() { return _entry->get_text(); }
+ Glib::SignalProxy0<void> signal_changed() { return _entry->signal_changed(); }
+
+private:
+ Gtk::Menu* build_value_menu();
+ Gtk::Menu* build_subclass_menu(const LilvNode* klass);
+
+ void add_leaf_menu_item(Gtk::Menu* menu,
+ const LilvNode* node,
+ const std::string& label);
+
+ void add_class_menu_item(Gtk::Menu* menu,
+ const LilvNode* klass,
+ const std::string& label);
+
+ void uri_chosen(const std::string& uri);
+ bool menu_button_event(GdkEvent* ev);
+
+ App* _app;
+ const std::set<URI> _types;
+ Gtk::Button* _menu_button;
+ Gtk::Entry* _entry;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_URI_ENTRY_HPP
diff --git a/src/gui/WidgetFactory.cpp b/src/gui/WidgetFactory.cpp
new file mode 100644
index 00000000..3a71425d
--- /dev/null
+++ b/src/gui/WidgetFactory.cpp
@@ -0,0 +1,82 @@
+/*
+ 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 "WidgetFactory.hpp"
+
+#include "ingen/Log.hpp"
+#include "ingen/runtime_paths.hpp"
+
+#include <cstdlib>
+#include <fstream>
+#include <stdexcept>
+#include <string>
+
+namespace ingen {
+namespace gui {
+
+Glib::ustring WidgetFactory::ui_filename = "";
+
+inline static bool
+is_readable(const std::string& filename)
+{
+ std::ifstream fs(filename.c_str());
+ const bool fail = fs.fail();
+ fs.close();
+ return !fail;
+}
+
+void
+WidgetFactory::find_ui_file()
+{
+ // Try file in bundle (directory where executable resides)
+ ui_filename = ingen::bundle_file_path("ingen_gui.ui");
+ if (is_readable(ui_filename)) {
+ return;
+ }
+
+ // Try ENGINE_UI_PATH from the environment
+ const char* const env_path = getenv("INGEN_UI_PATH");
+ if (env_path && is_readable(env_path)) {
+ ui_filename = env_path;
+ return;
+ }
+
+ // Try the default system installed path
+ ui_filename = ingen::data_file_path("ingen_gui.ui");
+ if (is_readable(ui_filename)) {
+ return;
+ }
+
+ throw std::runtime_error(fmt("Unable to find ingen_gui.ui in %1%\n",
+ INGEN_DATA_DIR));
+}
+
+Glib::RefPtr<Gtk::Builder>
+WidgetFactory::create(const std::string& toplevel_widget)
+{
+ if (ui_filename.empty()) {
+ find_ui_file();
+ }
+
+ if (toplevel_widget.empty()) {
+ return Gtk::Builder::create_from_file(ui_filename);
+ } else {
+ return Gtk::Builder::create_from_file(ui_filename, toplevel_widget.c_str());
+ }
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/WidgetFactory.hpp b/src/gui/WidgetFactory.hpp
new file mode 100644
index 00000000..0a9ea4c3
--- /dev/null
+++ b/src/gui/WidgetFactory.hpp
@@ -0,0 +1,61 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_GLADEFACTORY_HPP
+#define INGEN_GUI_GLADEFACTORY_HPP
+
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/builder.h>
+
+#include <string>
+
+namespace Gtk { class Builder; }
+
+namespace ingen {
+namespace gui {
+
+/** Loads widgets from an XML description.
+ * Purely static.
+ *
+ * \ingroup GUI
+ */
+class WidgetFactory {
+public:
+ static Glib::RefPtr<Gtk::Builder>
+ create(const std::string& toplevel_widget="");
+
+ template<typename T>
+ static void get_widget(const Glib::ustring& name, T*& widget) {
+ Glib::RefPtr<Gtk::Builder> xml = create(name);
+ xml->get_widget(name, widget);
+ }
+
+ template<typename T>
+ static void get_widget_derived(const Glib::ustring& name, T*& widget) {
+ Glib::RefPtr<Gtk::Builder> xml = create(name);
+ xml->get_widget_derived(name, widget);
+ }
+
+private:
+ static void find_ui_file();
+ static Glib::ustring ui_filename;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_GLADEFACTORY_HPP
diff --git a/src/gui/Window.hpp b/src/gui/Window.hpp
new file mode 100644
index 00000000..5f49bc10
--- /dev/null
+++ b/src/gui/Window.hpp
@@ -0,0 +1,79 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_WINDOW_HPP
+#define INGEN_GUI_WINDOW_HPP
+
+#include <gdk/gdkkeysyms.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/window.h>
+
+namespace ingen {
+
+namespace gui {
+
+class App;
+
+/** Ingen GUI Window
+ * \ingroup GUI
+ */
+class Window : public Gtk::Window
+{
+public:
+ Window() : Gtk::Window(), _app(nullptr) {}
+ explicit Window(BaseObjectType* cobject) : Gtk::Window(cobject), _app(nullptr) {}
+
+ virtual void init_window(App& app) { _app = &app; }
+
+ bool on_key_press_event(GdkEventKey* event) override {
+ if (event->keyval == GDK_w && event->state & GDK_CONTROL_MASK) {
+ hide();
+ return true;
+ }
+ return Gtk::Window::on_key_press_event(event);
+ }
+
+ static bool key_press_handler(Gtk::Window* win, GdkEventKey* event);
+
+ App* _app;
+};
+
+/** Ingen GUI Dialog
+ * \ingroup GUI
+ */
+class Dialog : public Gtk::Dialog
+{
+public:
+ Dialog() : Gtk::Dialog(), _app(nullptr) {}
+ explicit Dialog(BaseObjectType* cobject) : Gtk::Dialog(cobject), _app(nullptr) {}
+
+ virtual void init_dialog(App& app) { _app = &app; }
+
+ bool on_key_press_event(GdkEventKey* event) override {
+ if (event->keyval == GDK_w && event->state & GDK_CONTROL_MASK) {
+ hide();
+ return true;
+ }
+ return Gtk::Window::on_key_press_event(event);
+ }
+
+ App* _app;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_WINDOW_HPP
diff --git a/src/gui/WindowFactory.cpp b/src/gui/WindowFactory.cpp
new file mode 100644
index 00000000..d85987f0
--- /dev/null
+++ b/src/gui/WindowFactory.cpp
@@ -0,0 +1,304 @@
+/*
+ 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 "App.hpp"
+#include "LoadGraphWindow.hpp"
+#include "LoadPluginWindow.hpp"
+#include "NewSubgraphWindow.hpp"
+#include "GraphView.hpp"
+#include "GraphWindow.hpp"
+#include "PropertiesWindow.hpp"
+#include "RenameWindow.hpp"
+#include "WidgetFactory.hpp"
+#include "WindowFactory.hpp"
+
+#include "ingen/Log.hpp"
+#include "ingen/client/GraphModel.hpp"
+
+#include <cassert>
+#include <stdexcept>
+#include <string>
+#include <utility>
+
+namespace ingen {
+
+using namespace client;
+
+namespace gui {
+
+WindowFactory::WindowFactory(App& app)
+ : _app(app)
+ , _main_box(nullptr)
+ , _load_plugin_win(nullptr)
+ , _load_graph_win(nullptr)
+ , _new_subgraph_win(nullptr)
+ , _properties_win(nullptr)
+{
+ WidgetFactory::get_widget_derived("load_plugin_win", _load_plugin_win);
+ WidgetFactory::get_widget_derived("load_graph_win", _load_graph_win);
+ WidgetFactory::get_widget_derived("new_subgraph_win", _new_subgraph_win);
+ WidgetFactory::get_widget_derived("properties_win", _properties_win);
+ WidgetFactory::get_widget_derived("rename_win", _rename_win);
+
+ if (!(_load_plugin_win && _load_graph_win && _new_subgraph_win
+ && _properties_win && _rename_win)) {
+ throw std::runtime_error("failed to load window widgets\n");
+ }
+
+ _load_plugin_win->init_window(app);
+ _load_graph_win->init(app);
+ _new_subgraph_win->init_window(app);
+ _properties_win->init_window(app);
+ _rename_win->init_window(app);
+}
+
+WindowFactory::~WindowFactory()
+{
+ for (const auto& w : _graph_windows) {
+ delete w.second;
+ }
+}
+
+void
+WindowFactory::clear()
+{
+ for (const auto& w : _graph_windows) {
+ delete w.second;
+ }
+
+ _graph_windows.clear();
+}
+
+/** Returns the number of Graph windows currently visible.
+ */
+size_t
+WindowFactory::num_open_graph_windows()
+{
+ size_t ret = 0;
+ for (const auto& w : _graph_windows) {
+ if (w.second->is_visible()) {
+ ++ret;
+ }
+ }
+
+ return ret;
+}
+
+GraphBox*
+WindowFactory::graph_box(SPtr<const GraphModel> graph)
+{
+ GraphWindow* window = graph_window(graph);
+ if (window) {
+ return window->box();
+ } else {
+ return _main_box;
+ }
+}
+
+GraphWindow*
+WindowFactory::graph_window(SPtr<const GraphModel> graph)
+{
+ if (!graph) {
+ return nullptr;
+ }
+
+ auto w = _graph_windows.find(graph->path());
+
+ return (w == _graph_windows.end()) ? nullptr : w->second;
+}
+
+GraphWindow*
+WindowFactory::parent_graph_window(SPtr<const BlockModel> block)
+{
+ if (!block) {
+ return nullptr;
+ }
+
+ return graph_window(dynamic_ptr_cast<GraphModel>(block->parent()));
+}
+
+/** Present a GraphWindow for a Graph.
+ *
+ * If `preferred` is not null, it will be set to display `graph` if the graph
+ * does not already have a visible window, otherwise that window will be
+ * presented and `preferred` left unmodified.
+ */
+void
+WindowFactory::present_graph(SPtr<const GraphModel> graph,
+ GraphWindow* preferred,
+ SPtr<GraphView> view)
+{
+ assert(!view || view->graph() == graph);
+
+ auto w = _graph_windows.find(graph->path());
+
+ if (w != _graph_windows.end()) {
+ (*w).second->present();
+ } else if (preferred) {
+ w = _graph_windows.find(preferred->graph()->path());
+ assert((*w).second == preferred);
+
+ preferred->box()->set_graph(graph, view);
+ _graph_windows.erase(w);
+ _graph_windows[graph->path()] = preferred;
+ preferred->present();
+
+ } else {
+ GraphWindow* win = new_graph_window(graph, view);
+ win->present();
+ }
+}
+
+GraphWindow*
+WindowFactory::new_graph_window(SPtr<const GraphModel> graph,
+ SPtr<GraphView> view)
+{
+ assert(!view || view->graph() == graph);
+
+ GraphWindow* win = nullptr;
+ WidgetFactory::get_widget_derived("graph_win", win);
+ if (!win) {
+ _app.log().error("Failed to load graph window widget\n");
+ return nullptr;
+ }
+
+ win->init_window(_app);
+
+ win->box()->set_graph(graph, view);
+ _graph_windows[graph->path()] = win;
+
+ win->signal_delete_event().connect(
+ sigc::bind<0>(sigc::mem_fun(this, &WindowFactory::remove_graph_window),
+ win));
+
+ return win;
+}
+
+bool
+WindowFactory::remove_graph_window(GraphWindow* win, GdkEventAny* ignored)
+{
+ if (_graph_windows.size() <= 1) {
+ return !_app.quit(win);
+ }
+
+ auto w = _graph_windows.find(win->graph()->path());
+
+ assert((*w).second == win);
+ _graph_windows.erase(w);
+
+ delete win;
+
+ return false;
+}
+
+void
+WindowFactory::present_load_plugin(SPtr<const GraphModel> graph,
+ Properties data)
+{
+ _app.request_plugins_if_necessary();
+
+ auto w = _graph_windows.find(graph->path());
+
+ if (w != _graph_windows.end()) {
+ _load_plugin_win->set_transient_for(*w->second);
+ }
+
+ _load_plugin_win->set_modal(false);
+ _load_plugin_win->set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG);
+ if (w->second) {
+ int width, height;
+ w->second->get_size(width, height);
+ _load_plugin_win->set_default_size(width - width / 8, height / 2);
+ }
+ _load_plugin_win->set_title(
+ std::string("Load Plugin - ") + graph->path() + " - Ingen");
+ _load_plugin_win->present(graph, data);
+}
+
+void
+WindowFactory::present_load_graph(SPtr<const GraphModel> graph,
+ Properties data)
+{
+ auto w = _graph_windows.find(graph->path());
+
+ if (w != _graph_windows.end()) {
+ _load_graph_win->set_transient_for(*w->second);
+ }
+
+ _load_graph_win->present(graph, true, data);
+}
+
+void
+WindowFactory::present_load_subgraph(SPtr<const GraphModel> graph,
+ Properties data)
+{
+ auto w = _graph_windows.find(graph->path());
+
+ if (w != _graph_windows.end()) {
+ _load_graph_win->set_transient_for(*w->second);
+ }
+
+ _load_graph_win->present(graph, false, data);
+}
+
+void
+WindowFactory::present_new_subgraph(SPtr<const GraphModel> graph,
+ Properties data)
+{
+ auto w = _graph_windows.find(graph->path());
+
+ if (w != _graph_windows.end()) {
+ _new_subgraph_win->set_transient_for(*w->second);
+ }
+
+ _new_subgraph_win->present(graph, data);
+}
+
+void
+WindowFactory::present_rename(SPtr<const ObjectModel> object)
+{
+ auto w = _graph_windows.find(object->path());
+ if (w == _graph_windows.end()) {
+ w = _graph_windows.find(object->path().parent());
+ }
+
+ if (w != _graph_windows.end()) {
+ _rename_win->set_transient_for(*w->second);
+ }
+
+ _rename_win->present(object);
+}
+
+void
+WindowFactory::present_properties(SPtr<const ObjectModel> object)
+{
+ auto w = _graph_windows.find(object->path());
+ if (w == _graph_windows.end()) {
+ w = _graph_windows.find(object->path().parent());
+ }
+ if (w == _graph_windows.end()) {
+ w = _graph_windows.find(object->path().parent().parent());
+ }
+
+ if (w != _graph_windows.end()) {
+ _properties_win->set_transient_for(*w->second);
+ }
+
+ _properties_win->present(object);
+}
+
+} // namespace gui
+} // namespace ingen
diff --git a/src/gui/WindowFactory.hpp b/src/gui/WindowFactory.hpp
new file mode 100644
index 00000000..1b6201af
--- /dev/null
+++ b/src/gui/WindowFactory.hpp
@@ -0,0 +1,100 @@
+/*
+ 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/>.
+*/
+
+#ifndef INGEN_GUI_WINDOWFACTORY_HPP
+#define INGEN_GUI_WINDOWFACTORY_HPP
+
+#include "ingen/Node.hpp"
+#include "ingen/types.hpp"
+
+#include <cstddef>
+#include <map>
+
+namespace ingen {
+
+namespace client {
+class BlockModel;
+class ObjectModel;
+class GraphModel;
+}
+
+namespace gui {
+
+class App;
+class GraphBox;
+class GraphView;
+class GraphWindow;
+class LoadGraphWindow;
+class LoadPluginWindow;
+class NewSubgraphWindow;
+class PropertiesWindow;
+class RenameWindow;
+
+/** Manager/Factory for all windows.
+ *
+ * This serves as a nice centralized spot for all window management issues,
+ * as well as an enumeration of all windows (the goal being to reduce that
+ * number as much as possible).
+ */
+class WindowFactory {
+public:
+ explicit WindowFactory(App& app);
+ ~WindowFactory();
+
+ size_t num_open_graph_windows();
+
+ GraphBox* graph_box(SPtr<const client::GraphModel> graph);
+ GraphWindow* graph_window(SPtr<const client::GraphModel> graph);
+ GraphWindow* parent_graph_window(SPtr<const client::BlockModel> block);
+
+ void present_graph(
+ SPtr<const client::GraphModel> graph,
+ GraphWindow* preferred = nullptr,
+ SPtr<GraphView> view = SPtr<GraphView>());
+
+ void present_load_plugin(SPtr<const client::GraphModel> graph, Properties data=Properties());
+ void present_load_graph(SPtr<const client::GraphModel> graph, Properties data=Properties());
+ void present_load_subgraph(SPtr<const client::GraphModel> graph, Properties data=Properties());
+ void present_new_subgraph(SPtr<const client::GraphModel> graph, Properties data=Properties());
+ void present_rename(SPtr<const client::ObjectModel> object);
+ void present_properties(SPtr<const client::ObjectModel> object);
+
+ bool remove_graph_window(GraphWindow* win, GdkEventAny* ignored = nullptr);
+
+ void set_main_box(GraphBox* box) { _main_box = box; }
+
+ void clear();
+
+private:
+ typedef std::map<Raul::Path, GraphWindow*> GraphWindowMap;
+
+ GraphWindow* new_graph_window(SPtr<const client::GraphModel> graph,
+ SPtr<GraphView> view);
+
+ App& _app;
+ GraphBox* _main_box;
+ GraphWindowMap _graph_windows;
+ LoadPluginWindow* _load_plugin_win;
+ LoadGraphWindow* _load_graph_win;
+ NewSubgraphWindow* _new_subgraph_win;
+ PropertiesWindow* _properties_win;
+ RenameWindow* _rename_win;
+};
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_WINDOWFACTORY_HPP
diff --git a/src/gui/ingen_gui.cpp b/src/gui/ingen_gui.cpp
new file mode 100644
index 00000000..4504d3fe
--- /dev/null
+++ b/src/gui/ingen_gui.cpp
@@ -0,0 +1,67 @@
+/*
+ This file is part of Ingen.
+ Copyright 2007-2018 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/Configuration.hpp"
+#include "ingen/Module.hpp"
+#include "ingen/QueuedInterface.hpp"
+#include "ingen/client/SigClientInterface.hpp"
+
+#include "App.hpp"
+
+namespace ingen {
+namespace gui {
+
+struct GUIModule : public Module {
+ using SigClientInterface = client::SigClientInterface;
+
+ void load(World& world) override {
+ URI uri(world.conf().option("connect").ptr<char>());
+ if (!world.interface()) {
+ world.set_interface(
+ world.new_interface(URI(uri), make_client(world)));
+ } else if (!dynamic_ptr_cast<SigClientInterface>(
+ world.interface()->respondee())) {
+ world.interface()->set_respondee(make_client(world));
+ }
+
+ app = gui::App::create(world);
+ }
+
+ void run(World& world) override {
+ app->run();
+ }
+
+ SPtr<Interface> make_client(World& world) {
+ SPtr<SigClientInterface> sci(new SigClientInterface());
+ return world.engine() ? sci : SPtr<Interface>(new QueuedInterface(sci));
+ }
+
+ SPtr<gui::App> app;
+};
+
+} // namespace gui
+} // namespace ingen
+
+extern "C" {
+
+ingen::Module*
+ingen_module_load()
+{
+ Glib::thread_init();
+ return new ingen::gui::GUIModule();
+}
+
+} // extern "C"
diff --git a/src/gui/ingen_gui.gladep b/src/gui/ingen_gui.gladep
new file mode 100644
index 00000000..184ff460
--- /dev/null
+++ b/src/gui/ingen_gui.gladep
@@ -0,0 +1,9 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-project SYSTEM "http://glade.gnome.org/glade-project-2.0.dtd">
+
+<glade-project>
+ <name>Ingen</name>
+ <program_name>ingen</program_name>
+ <language>C++</language>
+ <gnome_support>FALSE</gnome_support>
+</glade-project>
diff --git a/src/gui/ingen_gui.ui b/src/gui/ingen_gui.ui
new file mode 100644
index 00000000..9e751064
--- /dev/null
+++ b/src/gui/ingen_gui.ui
@@ -0,0 +1,3049 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="2.24"/>
+ <!-- interface-naming-policy toplevel-contextual -->
+ <object class="GtkAboutDialog" id="about_win">
+ <property name="can_focus">False</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">normal</property>
+ <property name="program_name">Ingen</property>
+ <property name="version">@INGEN_VERSION@</property>
+ <property name="copyright" translatable="yes">Copyright 2005-2015 David Robillard &lt;http://drobilla.net&gt;</property>
+ <property name="website">http://drobilla.net/software/ingen</property>
+ <property name="license" translatable="yes">Licensed under the GNU Affero GPL, Version 3 or later.
+
+See COPYING file included with this distribution, or http://www.gnu.org/licenses/agpl.txt for more information</property>
+ <property name="authors">David Robillard &lt;d@drobilla.net&gt;</property>
+ <property name="translator_credits" translatable="yes" comments="TRANSLATORS: Replace this string with your names, one name per line.">translator-credits</property>
+ <property name="artists">Usability / UI Design:
+ Thorsten Wilms</property>
+ <property name="wrap_license">True</property>
+ <child internal-child="vbox">
+ <object class="GtkVBox" id="dialog-vbox3">
+ <property name="can_focus">False</property>
+ <child internal-child="action_area">
+ <object class="GtkHButtonBox" id="dialog-action_area3">
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkMenu" id="canvas_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkCheckMenuItem" id="canvas_menu_edit">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Edit</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="menuitem5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="input1">
+ <property name="label">_Input</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <child type="submenu">
+ <object class="GtkMenu" id="input1_menu">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="canvas_menu_add_audio_input">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Audio</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_canvas_menu_add_audio_input_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="canvas_menu_add_cv_input">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">C_V</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_canvas_menu_add_cv_input_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="canvas_menu_add_control_input">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Control</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_canvas_menu_add_control_input_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="canvas_menu_add_event_input">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Event</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_canvas_menu_add_event_input_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="output1">
+ <property name="label">_Output</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <child type="submenu">
+ <object class="GtkMenu" id="output1_menu">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="canvas_menu_add_audio_output">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Audio</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_canvas_menu_add_audio_output_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="canvas_menu_add_cv_output">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">C_V</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_canvas_menu_add_cv_output_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="canvas_menu_add_control_output">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Control</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_canvas_menu_add_control_output_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="canvas_menu_add_event_output">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Event</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_canvas_menu_add_event_output_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="canvas_menu_load_plugin">
+ <property name="label">_Find Plugin...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <signal name="activate" handler="on_canvas_menu_add_plugin_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="canvas_menu_load_graph">
+ <property name="label">_Load Graph...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <signal name="activate" handler="on_canvas_menu_load_graph_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="canvas_menu_new_graph">
+ <property name="label">_New Graph...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <signal name="activate" handler="on_canvas_menu_new_graph_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="menuitem7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="canvas_menu_properties">
+ <property name="label">P_roperties...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <signal name="activate" handler="on_canvas_menu_properties_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="config_win">
+ <property name="can_focus">False</property>
+ <property name="border_width">8</property>
+ <property name="title" translatable="yes">Configuration - Ingen</property>
+ <child>
+ <object class="GtkVBox" id="vbox13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkTable" id="table9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">2</property>
+ <property name="n_columns">2</property>
+ <child>
+ <object class="GtkLabel" id="label90">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Graph Search Path: &lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="config_path_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label91">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;i&gt;Example: /foo/bar:/home/user/graphs&lt;/i&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label103">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="config_save_button">
+ <property name="label">gtk-save</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="config_cancel_button">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="config_ok_button">
+ <property name="label">gtk-ok</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkDialog" id="connect_win">
+ <property name="can_focus">False</property>
+ <property name="border_width">6</property>
+ <property name="title" translatable="yes">Engine - Ingen</property>
+ <property name="resizable">False</property>
+ <property name="type_hint">dialog</property>
+ <child internal-child="vbox">
+ <object class="GtkVBox" id="dialog-vbox4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child internal-child="action_area">
+ <object class="GtkHButtonBox" id="dialog-action_area4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="connect_quit_button">
+ <property name="label">gtk-quit</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="connect_disconnect_button">
+ <property name="label">gtk-disconnect</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="connect_connect_button">
+ <property name="label">gtk-connect</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="vbox19">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkHBox" id="hbox61">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkImage" id="connect_icon">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xpad">12</property>
+ <property name="stock">gtk-disconnect</property>
+ <property name="icon-size">3</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="vbox20">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkProgressBar" id="connect_progress_bar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pulse_step">0.10000000149</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="connect_progress_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Not connected</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHSeparator" id="hseparator4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">4</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table18">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">3</property>
+ <property name="n_columns">2</property>
+ <property name="row_spacing">8</property>
+ <child>
+ <object class="GtkHBox" id="hbox64">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkSpinButton" id="connect_port_spinbutton">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ <property name="climb_rate">1</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options">GTK_FILL</property>
+ <property name="x_padding">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox67">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkEntry" id="connect_url_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="activates_default">True</property>
+ <property name="width_chars">28</property>
+ <property name="text" translatable="yes">unix:///tmp/ingen.sock</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ <property name="x_padding">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="connect_server_radiobutton">
+ <property name="label" translatable="yes">_Connect to engine at: </property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="connect_launch_radiobutton">
+ <property name="label" translatable="yes">_Launch separate engine on port: </property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">connect_server_radiobutton</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="connect_internal_radiobutton">
+ <property name="label" translatable="yes">Start local _JACK engine</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">connect_server_radiobutton</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label131">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHSeparator" id="hseparator8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <property name="layout_style">start</property>
+ <child>
+ <object class="GtkButton" id="connect_deactivate_button">
+ <property name="label" translatable="yes">D_eactivate</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="connect_activate_button">
+ <property name="label" translatable="yes">_Activate</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="padding">6</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="0">connect_quit_button</action-widget>
+ <action-widget response="-6">connect_disconnect_button</action-widget>
+ <action-widget response="-6">connect_connect_button</action-widget>
+ </action-widgets>
+ </object>
+ <object class="GtkWindow" id="graph_tree_win">
+ <property name="width_request">320</property>
+ <property name="height_request">340</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">8</property>
+ <property name="title" translatable="yes">Graphs - Ingen</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow8">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="border_width">3</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTreeView" id="graphs_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="rules_hint">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="graph_win">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Ingen</property>
+ <property name="default_width">776</property>
+ <property name="default_height">480</property>
+ <child>
+ <object class="GtkVBox" id="graph_win_vbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuBar" id="menubar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="graph_file_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_File</property>
+ <property name="use_underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu" id="graph_file_menu_menu">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_import_menuitem">
+ <property name="label">_Import...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <accelerator key="I" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_import_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="separator9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_save_menuitem">
+ <property name="label">gtk-save</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="S" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_file_save_graph_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_save_as_menuitem">
+ <property name="label">Save _As...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <accelerator key="S" signal="activate" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_save_as_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_export_image_menuitem">
+ <property name="label">_Export Image...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <accelerator key="R" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_draw_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="separator11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_close_menuitem">
+ <property name="label">gtk-close</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="W" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_file_close_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_quit_menuitem">
+ <property name="label">gtk-quit</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="Q" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_file_quit_nokill_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="edit2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Edit</property>
+ <property name="use_underline">True</property>
+ <child type="submenu">
+ <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="visible">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="visible">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_SHIFT_MASK | GDK_CONTROL_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>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_cut_menuitem">
+ <property name="label">gtk-cut</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="on_graph_cut_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_copy_menuitem">
+ <property name="label">gtk-copy</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="C" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_copy_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_paste_menuitem">
+ <property name="label">gtk-paste</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="V" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_paste_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_delete_menuitem">
+ <property name="label">gtk-delete</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="Delete" signal="activate"/>
+ <signal name="activate" handler="on_graph_delete_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_select_all_menuitem">
+ <property name="label">gtk-select-all</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="A" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_select_all_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="menuitem1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_arrange_menuitem">
+ <property name="label">Arrange</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <accelerator key="G" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="menuitem2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_view_control_window_menuitem">
+ <property name="label">C_ontrols...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <accelerator key="O" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_view_control_window_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_properties_menuitem">
+ <property name="label">gtk-properties</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="P" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_properties_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="graph_graph_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_View</property>
+ <property name="use_underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu" id="graph_graph_menu_menu">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkCheckMenuItem" id="graph_animate_signals_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">Update control ports as values change.</property>
+ <property name="label" translatable="yes">Animate Signa_ls</property>
+ <property name="use_underline">True</property>
+ <accelerator key="l" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="graph_sprung_layout_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Sprung Layou_t</property>
+ <property name="use_underline">True</property>
+ <accelerator key="t" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="menuitem6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="graph_human_names_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Human names</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ <accelerator key="H" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="graph_show_port_names_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Port _Names</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ <accelerator key="n" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="graph_doc_pane_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Documentation Pane</property>
+ <property name="use_underline">True</property>
+ <accelerator key="D" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="graph_status_bar_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Status Bar</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ <accelerator key="b" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="separator1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_zoom_in_menuitem">
+ <property name="label">gtk-zoom-in</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="equal" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_zoom_out_menuitem">
+ <property name="label">gtk-zoom-out</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="minus" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_zoom_normal_menuitem">
+ <property name="label">gtk-zoom-100</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="0" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_zoom_full_menuitem">
+ <property name="label">gtk-zoom-fit</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="F" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="menuitem3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="graph_increase_font_size_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Increase Font Size</property>
+ <property name="use_underline">True</property>
+ <accelerator key="Up" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="graph_decrease_font_size_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Decrease Font Size</property>
+ <property name="use_underline">True</property>
+ <accelerator key="Down" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="graph_normal_font_size_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Normal Font Size</property>
+ <property name="use_underline">True</property>
+ <accelerator key="1" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="menuitem4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_parent_menuitem">
+ <property name="label">_Parent</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="BackSpace" signal="activate"/>
+ <signal name="activate" handler="graph_parent_menuitem" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_refresh_menuitem">
+ <property name="label">gtk-refresh</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="F5" signal="activate"/>
+ <signal name="activate" handler="graph_refresh_menuitem" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_fullscreen_menuitem">
+ <property name="label">gtk-fullscreen</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <accelerator key="F11" signal="activate"/>
+ <signal name="activate" handler="graph_fullscreen_menuitem" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="view1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Windows</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_view1_activate" swapped="no"/>
+ <child type="submenu">
+ <object class="GtkMenu" id="view1_menu">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_view_engine_window_menuitem">
+ <property name="label">_Engine</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <accelerator key="E" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_view_engine_window_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_view_graph_tree_window_menuitem">
+ <property name="label">_Graph Tree</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <accelerator key="T" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_view_tree_window_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_view_messages_window_menuitem">
+ <property name="label">_Messages</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ <accelerator key="M" signal="activate" modifiers="GDK_CONTROL_MASK"/>
+ <signal name="activate" handler="on_graph_view_messages_window_menuitem_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="help_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Help</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_help_menu_activate" swapped="no"/>
+ <child type="submenu">
+ <object class="GtkMenu" id="help_menu_menu">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkImageMenuItem" id="right-click_the_canvas_to_add_objects1">
+ <property name="label">Right-click the canvas to add objects</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="separator13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="graph_help_about_menuitem">
+ <property name="label">gtk-about</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="on_about1_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHPaned" id="graph_documentation_paned">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkAlignment" id="graph_win_alignment">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">True</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="graph_documentation_scrolledwindow">
+ <property name="can_focus">False</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">False</property>
+ <property name="shrink">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStatusbar" id="graph_win_status_bar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">2</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkFileChooserDialog" id="load_graph_win">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Load Graph - Ingen</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="type_hint">dialog</property>
+ <child internal-child="vbox">
+ <object class="GtkVBox" id="vbox11">
+ <property name="can_focus">False</property>
+ <property name="spacing">24</property>
+ <child internal-child="action_area">
+ <object class="GtkHButtonBox" id="hbuttonbox3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="load_graph_cancel_button">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="load_graph_ok_button">
+ <property name="label">gtk-open</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="xscale">0</property>
+ <child>
+ <object class="GtkTable" id="table14">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">3</property>
+ <property name="n_columns">3</property>
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="load_graph_poly_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Polyphony: </property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="load_graph_poly_from_file_radio">
+ <property name="label" translatable="yes">Load from _File</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="has_tooltip">True</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">load_graph_poly_voices_radio</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="load_graph_ports_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Ports: </property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="load_graph_insert_ports_radio">
+ <property name="label" translatable="yes">_Insert new ports</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="has_tooltip">True</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">load_graph_merge_ports_radio</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="load_graph_merge_ports_radio">
+ <property name="label" translatable="yes">_Merge with existing ports</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="has_tooltip">True</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox58">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkRadioButton" id="load_graph_poly_voices_radio">
+ <property name="label" translatable="yes">_Voices:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="has_tooltip">True</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="load_graph_poly_spinbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ <property name="climb_rate">1</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="load_graph_symbol_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">_Symbol: </property>
+ <property name="use_markup">True</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">load_graph_symbol_entry</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="load_graph_symbol_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">3</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">load_graph_cancel_button</action-widget>
+ <action-widget response="-5">load_graph_ok_button</action-widget>
+ </action-widgets>
+ </object>
+ <object class="GtkWindow" id="load_plugin_win">
+ <property name="can_focus">False</property>
+ <property name="border_width">8</property>
+ <property name="title" translatable="yes">Load Plugin - Ingen</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <child>
+ <object class="GtkVBox" id="vbox9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">1</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="border_width">2</property>
+ <child>
+ <object class="GtkTreeView" id="load_plugin_plugins_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="border_width">2</property>
+ <property name="reorderable">True</property>
+ <property name="rules_hint">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table16">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">3</property>
+ <property name="n_columns">3</property>
+ <property name="row_spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="label66">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Node _Symbol:</property>
+ <property name="use_markup">True</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">load_plugin_name_entry</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHSeparator" id="hseparator1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHSeparator" id="hseparator2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHSeparator" id="hseparator3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox63">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkEntry" id="load_plugin_name_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="load_plugin_polyphonic_checkbutton">
+ <property name="label" translatable="yes">_Polyphonic</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="padding">8</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="y_options">GTK_FILL</property>
+ <property name="x_padding">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="load_plugin_search_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="secondary_icon_stock">gtk-clear</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">3</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="load_plugin_filter_combo">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">4</property>
+ <child>
+ <object class="GtkButton" id="load_plugin_close_button">
+ <property name="label">gtk-close</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="load_plugin_add_button">
+ <property name="label">gtk-add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="messages_win">
+ <property name="width_request">400</property>
+ <property name="height_request">180</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">8</property>
+ <property name="title" translatable="yes">Messages - Ingen</property>
+ <child>
+ <object class="GtkVBox" id="vbox12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTextView" id="messages_textview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="pixels_above_lines">1</property>
+ <property name="pixels_below_lines">1</property>
+ <property name="editable">False</property>
+ <property name="wrap_mode">word</property>
+ <property name="left_margin">5</property>
+ <property name="right_margin">5</property>
+ <property name="cursor_visible">False</property>
+ <property name="accepts_tab">False</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="messages_clear_button">
+ <property name="label">gtk-clear</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="messages_close_button">
+ <property name="label">gtk-close</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="new_subgraph_win">
+ <property name="width_request">320</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">8</property>
+ <property name="title" translatable="yes">Create Subgraph - Ingen</property>
+ <property name="resizable">False</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="type_hint">dialog</property>
+ <child>
+ <object class="GtkVBox" id="vbox4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">2</property>
+ <property name="n_columns">2</property>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">_Symbol: </property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">new_subgraph_name_entry</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_EXPAND</property>
+ <property name="x_padding">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">_Polyphony: </property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">new_subgraph_polyphony_spinbutton</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_EXPAND</property>
+ <property name="x_padding">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="new_subgraph_polyphony_spinbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ <property name="climb_rate">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ <property name="y_padding">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="new_subgraph_name_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="y_options"/>
+ <property name="y_padding">4</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="new_subgraph_message_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">4</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="new_subgraph_cancel_button">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="new_subgraph_ok_button">
+ <property name="label">gtk-ok</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkMenu" id="object_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <child>
+ <object class="GtkImageMenuItem" id="node_popup_gui_menuitem">
+ <property name="label" translatable="yes">Show GUI...</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="use_stock">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="node_embed_gui_menuitem">
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Embed GUI</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="node_enabled_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Enabled</property>
+ <property name="active">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="node_randomize_menuitem">
+ <property name="label" translatable="yes">Randomi_ze</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="port_set_min_menuitem">
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Set Value as Mi_nimum</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="port_set_max_menuitem">
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Set Value as Ma_ximum</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="port_reset_range_menuitem">
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Re_set Range</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="port_expose_menuitem">
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Expose</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem" id="object_menu_separator">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="object_polyphonic_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">P_olyphonic</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="object_learn_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Learn</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="object_unlearn_menuitem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Unlearn</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="object_disconnect_menuitem">
+ <property name="label">Dis_connect</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="object_destroy_menuitem">
+ <property name="label">gtk-delete</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="object_rename_menuitem">
+ <property name="label">_Rename...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImageMenuItem" id="object_properties_menuitem">
+ <property name="label">_Properties...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">False</property>
+ </object>
+ </child>
+ </object>
+ <object class="GtkMenu" id="port_control_menu">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkImageMenuItem" id="port_control_menu_properties">
+ <property name="label">gtk-properties</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="on_port_control_menu_properties_activate" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="port_properties_win">
+ <property name="can_focus">False</property>
+ <property name="border_width">8</property>
+ <property name="title" translatable="yes">Port Properties - Ingen</property>
+ <property name="resizable">False</property>
+ <property name="window_position">mouse</property>
+ <child>
+ <object class="GtkVBox" id="dialog-vbox7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">8</property>
+ <child>
+ <object class="GtkHButtonBox" id="dialog-action_area7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="port_properties_cancel_button">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="port_properties_ok_button">
+ <property name="label">gtk-ok</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table20">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">2</property>
+ <property name="n_columns">2</property>
+ <property name="column_spacing">2</property>
+ <property name="row_spacing">4</property>
+ <child>
+ <object class="GtkSpinButton" id="port_properties_min_spinner">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ <property name="climb_rate">1</property>
+ <property name="digits">5</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="port_properties_max_spinner">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ <property name="climb_rate">1</property>
+ <property name="digits">5</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label138">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Minimum Value: </property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label139">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Maximum Value: </property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="properties_win">
+ <property name="can_focus">False</property>
+ <property name="border_width">12</property>
+ <property name="window_position">center-on-parent</property>
+ <child>
+ <object class="GtkVBox" id="properties_vbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkScrolledWindow" id="properties_scrolledwindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkViewport" id="viewport2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="resize_mode">queue</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkTable" id="properties_table">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_columns">3</property>
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">6</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkComboBox" id="properties_key_combo">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="properties_value_bin">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="properties_add_button">
+ <property name="label">gtk-add</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="properties_buttonbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="properties_cancel_button">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="properties_apply_button">
+ <property name="label">gtk-apply</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="properties_ok_button">
+ <property name="label">gtk-ok</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="rename_win">
+ <property name="width_request">250</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Rename</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <child>
+ <object class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <child>
+ <object class="GtkTable" id="table2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">2</property>
+ <property name="n_columns">2</property>
+ <property name="row_spacing">8</property>
+ <child>
+ <object class="GtkLabel" id="label95">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Symbol: </property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">rename_symbol_entry</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="rename_label_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Label: </property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">rename_label_entry</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="rename_symbol_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="rename_message_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="padding">12</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">8</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="rename_cancel_button">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="rename_ok_button">
+ <property name="label">gtk-ok</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="warehouse_win">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Warehouse - Ingen</property>
+ <child>
+ <object class="GtkTable" id="table8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">6</property>
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">12</property>
+ <child>
+ <object class="GtkVBox" id="toggle_control">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <child>
+ <object class="GtkHBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <child>
+ <object class="GtkLabel" id="toggle_control_name_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="xpad">4</property>
+ <property name="label" translatable="yes">&lt;b&gt;Name&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ <property name="single_line_mode">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="yalign">1</property>
+ <property name="yscale">0</property>
+ <property name="bottom_padding">1</property>
+ <property name="left_padding">1</property>
+ <property name="right_padding">4</property>
+ <child>
+ <object class="GtkHSeparator" id="hseparator7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="toggle_control_check">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="y_padding">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="control_panel_vbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkAlignment" id="alignment6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="yalign">0</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwin1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <child>
+ <object class="GtkViewport" id="viewport1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkVBox" id="control_panel_controls_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkVBox" id="graph_view_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkHBox" id="hbox70">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkToolbar" id="toolbar6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="toolbar_style">icons</property>
+ <property name="icon_size">1</property>
+ <child>
+ <object class="GtkToolItem" id="graph_view_breadcrumb_container">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolbar" id="graph_view_toolbar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="toolbar_style">icons</property>
+ <property name="show_arrow">False</property>
+ <property name="icon_size">1</property>
+ <child>
+ <object class="GtkToggleToolButton" id="graph_view_process_but">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-execute</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolItem" id="toolitem7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkImage" id="image1978">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xpad">4</property>
+ <property name="stock">gtk-copy</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolItem" id="toolitem10">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkSpinButton" id="graph_view_poly_spin">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ <property name="climb_rate">1</property>
+ <property name="numeric">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="graph_view_scrolledwindow">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="events">GDK_EXPOSURE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_MOTION_MASK | GDK_BUTTON1_MOTION_MASK | GDK_BUTTON2_MOTION_MASK | GDK_BUTTON3_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_FOCUS_CHANGE_MASK | GDK_STRUCTURE_MASK | GDK_PROPERTY_CHANGE_MASK | GDK_VISIBILITY_NOTIFY_MASK | GDK_PROXIMITY_IN_MASK | GDK_PROXIMITY_OUT_MASK | GDK_SUBSTRUCTURE_MASK | GDK_SCROLL_MASK</property>
+ <property name="border_width">1</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHSeparator" id="hseparator5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="control_strip">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <child>
+ <object class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <child>
+ <object class="GtkLabel" id="control_strip_name_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">1</property>
+ <property name="xpad">4</property>
+ <property name="label" translatable="yes">&lt;b&gt;Name&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ <property name="single_line_mode">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="yalign">1</property>
+ <property name="yscale">0</property>
+ <property name="bottom_padding">1</property>
+ <property name="left_padding">1</property>
+ <property name="right_padding">4</property>
+ <child>
+ <object class="GtkHSeparator" id="hseparator6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="control_strip_spinner">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="width_chars">12</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ <property name="digits">4</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHScale" id="control_strip_slider">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="digits">63</property>
+ <property name="draw_value">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="y_padding">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="string_control">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <child>
+ <object class="GtkHBox" id="hbox3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <child>
+ <object class="GtkLabel" id="string_control_name_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="xpad">4</property>
+ <property name="label" translatable="yes">&lt;b&gt;Name&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ <property name="single_line_mode">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="string_control_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="caps_lock_warning">False</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="y_padding">8</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/src/gui/ingen_gui_lv2.cpp b/src/gui/ingen_gui_lv2.cpp
new file mode 100644
index 00000000..4817e9ae
--- /dev/null
+++ b/src/gui/ingen_gui_lv2.cpp
@@ -0,0 +1,226 @@
+/*
+ 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 "App.hpp"
+#include "GraphBox.hpp"
+
+#include "ingen/AtomReader.hpp"
+#include "ingen/AtomSink.hpp"
+#include "ingen/AtomWriter.hpp"
+#include "ingen/Forge.hpp"
+#include "ingen/Interface.hpp"
+#include "ingen/Properties.hpp"
+#include "ingen/URI.hpp"
+#include "ingen/URIs.hpp"
+#include "ingen/World.hpp"
+#include "ingen/client/ClientStore.hpp"
+#include "ingen/client/GraphModel.hpp"
+#include "ingen/client/SigClientInterface.hpp"
+#include "ingen/ingen.h"
+#include "ingen/paths.hpp"
+#include "ingen/runtime_paths.hpp"
+#include "ingen/types.hpp"
+#include "lv2/atom/atom.h"
+#include "lv2/atom/util.h"
+#include "lv2/core/lv2.h"
+#include "lv2/core/lv2.h"
+#include "lv2/log/log.h"
+#include "lv2/ui/ui.h"
+#include "lv2/urid/urid.h"
+#include "lv2/urid/urid.h"
+#include "raul/Path.hpp"
+
+#include <cstdint>
+#include <cstring>
+
+#define INGEN_LV2_UI_URI INGEN_NS "GraphUIGtk2"
+
+namespace ingen {
+
+/** A sink that writes atoms to a port via the UI extension. */
+struct IngenLV2AtomSink : public AtomSink {
+ IngenLV2AtomSink(URIs& uris,
+ LV2UI_Write_Function ui_write,
+ LV2UI_Controller ui_controller)
+ : _uris(uris)
+ , _ui_write(ui_write)
+ , _ui_controller(ui_controller)
+ {}
+
+ bool write(const LV2_Atom* atom, int32_t default_id) override {
+ _ui_write(_ui_controller,
+ 0,
+ lv2_atom_total_size(atom),
+ _uris.atom_eventTransfer,
+ atom);
+ return true;
+ }
+
+ URIs& _uris;
+ LV2UI_Write_Function _ui_write;
+ LV2UI_Controller _ui_controller;
+};
+
+struct IngenLV2UI {
+ IngenLV2UI()
+ : argc(0)
+ , argv(nullptr)
+ , forge(nullptr)
+ , world(nullptr)
+ , sink(nullptr)
+ {}
+
+ int argc;
+ char** argv;
+ Forge* forge;
+ World* world;
+ IngenLV2AtomSink* sink;
+ SPtr<gui::App> app;
+ SPtr<gui::GraphBox> view;
+ SPtr<Interface> engine;
+ SPtr<AtomReader> reader;
+ SPtr<client::SigClientInterface> client;
+};
+
+} // namespace ingen
+
+static LV2UI_Handle
+instantiate(const LV2UI_Descriptor* descriptor,
+ const char* plugin_uri,
+ const char* bundle_path,
+ LV2UI_Write_Function write_function,
+ LV2UI_Controller controller,
+ LV2UI_Widget* widget,
+ const LV2_Feature* const* features)
+{
+#if __cplusplus >= 201103L
+ using ingen::SPtr;
+#endif
+
+ ingen::set_bundle_path(bundle_path);
+
+ ingen::IngenLV2UI* ui = new ingen::IngenLV2UI();
+
+ LV2_URID_Map* map = nullptr;
+ LV2_URID_Unmap* unmap = nullptr;
+ LV2_Log_Log* log = nullptr;
+ for (int i = 0; features[i]; ++i) {
+ if (!strcmp(features[i]->URI, LV2_URID__map)) {
+ map = (LV2_URID_Map*)features[i]->data;
+ } else if (!strcmp(features[i]->URI, LV2_URID__unmap)) {
+ unmap = (LV2_URID_Unmap*)features[i]->data;
+ } else if (!strcmp(features[i]->URI, LV2_LOG__log)) {
+ log = (LV2_Log_Log*)features[i]->data;
+ }
+ }
+
+ ui->world = new ingen::World(map, unmap, log);
+ ui->forge = new ingen::Forge(ui->world->uri_map());
+
+ ui->world->load_configuration(ui->argc, ui->argv);
+
+ if (!ui->world->load_module("client")) {
+ delete ui;
+ return nullptr;
+ }
+
+ ui->sink = new ingen::IngenLV2AtomSink(
+ ui->world->uris(), write_function, controller);
+
+ // Set up an engine interface that writes LV2 atoms
+ ui->engine = SPtr<ingen::Interface>(
+ new ingen::AtomWriter(
+ ui->world->uri_map(), ui->world->uris(), *ui->sink));
+
+ ui->world->set_interface(ui->engine);
+
+ // Create App and client
+ ui->app = ingen::gui::App::create(*ui->world);
+ ui->client = SPtr<ingen::client::SigClientInterface>(
+ new ingen::client::SigClientInterface());
+ ui->app->set_is_plugin(true);
+ ui->app->attach(ui->client);
+
+ ui->reader = SPtr<ingen::AtomReader>(
+ new ingen::AtomReader(ui->world->uri_map(),
+ ui->world->uris(),
+ ui->world->log(),
+ *ui->client.get()));
+
+ // Create empty root graph model
+ ingen::Properties props;
+ props.emplace(ui->app->uris().rdf_type,
+ ingen::Property(ui->app->uris().ingen_Graph));
+ ui->app->store()->put(ingen::main_uri(), props);
+
+ // Create a GraphBox for the root and set as the UI widget
+ SPtr<const ingen::client::GraphModel> root =
+ ingen::dynamic_ptr_cast<const ingen::client::GraphModel>(
+ ui->app->store()->object(Raul::Path("/")));
+ ui->view = ingen::gui::GraphBox::create(*ui->app, root);
+ ui->view->unparent();
+ *widget = ui->view->gobj();
+
+ // Request the actual root graph
+ ui->world->interface()->get(ingen::main_uri());
+
+ return ui;
+}
+
+static void
+cleanup(LV2UI_Handle handle)
+{
+ ingen::IngenLV2UI* ui = (ingen::IngenLV2UI*)handle;
+ delete ui;
+}
+
+static void
+port_event(LV2UI_Handle handle,
+ uint32_t port_index,
+ uint32_t buffer_size,
+ uint32_t format,
+ const void* buffer)
+{
+ ingen::IngenLV2UI* ui = (ingen::IngenLV2UI*)handle;
+ const LV2_Atom* atom = (const LV2_Atom*)buffer;
+ ui->reader->write(atom);
+}
+
+static const void*
+extension_data(const char* uri)
+{
+ return nullptr;
+}
+
+static const LV2UI_Descriptor descriptor = {
+ INGEN_LV2_UI_URI,
+ instantiate,
+ cleanup,
+ port_event,
+ extension_data
+};
+
+LV2_SYMBOL_EXPORT
+const LV2UI_Descriptor*
+lv2ui_descriptor(uint32_t index)
+{
+ switch (index) {
+ case 0:
+ return &descriptor;
+ default:
+ return nullptr;
+ }
+}
diff --git a/src/gui/ingen_style.rc b/src/gui/ingen_style.rc
new file mode 100644
index 00000000..252b69d5
--- /dev/null
+++ b/src/gui/ingen_style.rc
@@ -0,0 +1,150 @@
+style "ingen-default"
+{
+ GtkMenuItem::selected_shadow_type = out
+
+ GtkWidget::interior_focus = 1
+ GtkWidget::focus_padding = 1
+
+ GtkButton::default_border = { 0, 0, 0, 0 }
+ GtkButton::default_outside_border = { 0, 0, 0, 0 }
+
+ GtkCheckButton::indicator_size = 12
+ GtkExpander::expander_size = 16
+ GtkMenuBar::internal-padding = 0
+ GtkPaned::handle_size = 6
+ GtkRange::slider_width = 15
+ GtkRange::stepper_size = 15
+ GtkRange::trough_border = 0
+ GtkScrollbar::min_slider_length = 30
+ GtkTreeView::expander_size = 14
+ GtkTreeView::odd_row_color = "#343"
+
+ xthickness = 1
+ ythickness = 1
+
+ fg[NORMAL] = "#B8BBB9"
+ fg[PRELIGHT] = "#B8BBB9"
+ fg[ACTIVE] = "#B8BBB9"
+ fg[SELECTED] = "#B8BBB9"
+ fg[INSENSITIVE] = "#48494B"
+
+ bg[NORMAL] = "#1E2224"
+ bg[PRELIGHT] = "#333537"
+ bg[ACTIVE] = "#333537"
+ bg[SELECTED] = "#00A150"
+ bg[INSENSITIVE] = "#1E2224"
+
+ base[NORMAL] = "#111"
+ base[PRELIGHT] = "#222"
+ base[ACTIVE] = "#0A2"
+ base[SELECTED] = "#0A2"
+ base[INSENSITIVE] = "#444"
+
+ text[NORMAL] = "#FFF"
+ text[PRELIGHT] = "#FFF"
+ text[ACTIVE] = "#FFF"
+ text[SELECTED] = "#FFF"
+ text[INSENSITIVE] = "#666"
+}
+
+style "ingen-progressbar" = "ingen-default"
+{
+ xthickness = 1
+ ythickness = 1
+}
+
+style "ingen-wide" = "ingen-default"
+{
+ xthickness = 2
+ ythickness = 2
+}
+
+style "ingen-notebook" = "ingen-wide"
+{
+ bg[NORMAL] = "#383B39"
+ bg[ACTIVE] = "#383B39"
+}
+
+style "ingen-tasklist" = "ingen-default"
+{
+ xthickness = 5
+ ythickness = 3
+}
+
+style "ingen-menu" = "ingen-default"
+{
+ xthickness = 5
+ ythickness = 5
+ bg[NORMAL] = "#262626"
+}
+
+style "ingen-menu-item" = "ingen-default"
+{
+ xthickness = 2
+ ythickness = 3
+}
+
+style "ingen-menu-itembar" = "ingen-default"
+{
+ xthickness = 3
+ ythickness = 3
+}
+
+style "ingen-tree" = "ingen-default"
+{
+ xthickness = 2
+ ythickness = 2
+}
+
+style "ingen-frame-title" = "ingen-default"
+{
+ fg[NORMAL] = "#B8BBB9"
+}
+
+style "ingen-panel" = "ingen-default"
+{
+ xthickness = 3
+ ythickness = 3
+}
+
+style "ingen-tooltips" = "ingen-default"
+{
+ xthickness = 4
+ ythickness = 4
+ bg[NORMAL] = "#585B59"
+}
+
+style "ingen-combo" = "ingen-default"
+{
+ xthickness = 1
+ ythickness = 2
+}
+
+class "*Ingen*GtkWidget" style : highest "ingen-default"
+class "*Ingen*GtkButton" style : highest "ingen-wide"
+class "*Ingen*GtkRange" style : highest "ingen-wide"
+class "*Ingen*GtkFrame" style : highest "ingen-wide"
+class "*Ingen*GtkStatusbar" style : highest "ingen-wide"
+class "*Ingen*GtkMenu" style : highest "ingen-menu"
+class "*Ingen*GtkMenuItem" style : highest "ingen-menu-item"
+widget_class "*Ingen*MenuItem.*" style : highest "ingen-menu-item"
+widget_class "*Ingen*.GtkAccelMenuItem.*" style : highest "ingen-menu-item"
+widget_class "*Ingen*.GtkRadioMenuItem.*" style : highest "ingen-menu-item"
+widget_class "*Ingen*.GtkCheckMenuItem.*" style : highest "ingen-menu-item"
+widget_class "*Ingen*.GtkImageMenuItem.*" style : highest "ingen-menu-item"
+widget_class "*Ingen*.GtkSeparatorMenuItem.*" style : highest "ingen-menu-item"
+class "*Ingen*GtkEntry" style : highest "ingen-wide"
+widget_class "*Ingen*.tooltips.*.GtkToggleButton" style : highest "ingen-tasklist"
+widget_class "*Ingen*.GtkTreeView.GtkButton" style : highest "ingen-tree"
+widget_class "*Ingen*.GtkCTree.GtkButton" style : highest "ingen-tree"
+widget_class "*Ingen*.GtkList.GtkButton" style : highest "ingen-tree"
+widget_class "*Ingen*.GtkCList.GtkButton" style : highest "ingen-tree"
+widget_class "*Ingen*.GtkFrame.GtkLabel" style : highest "ingen-frame-title"
+widget_class "*Ingen*BasePWidget.GtkEventBox.GtkTable.GtkFrame" style : highest "ingen-panel"
+widget "gtk-tooltips" style : highest "ingen-tooltips"
+class "*Ingen*GtkNotebook" style : highest "ingen-notebook"
+class "*Ingen*GtkProgressBar" style : highest "ingen-progressbar"
+widget_class "*Ingen*.GtkComboBox.GtkButton" style : highest "ingen-combo"
+widget_class "*Ingen*.GtkCombo.GtkButton" style : highest "ingen-combo"
+
+widget "*Ingen*" style : highest "ingen-default"
diff --git a/src/gui/rgba.hpp b/src/gui/rgba.hpp
new file mode 100644
index 00000000..f31e958c
--- /dev/null
+++ b/src/gui/rgba.hpp
@@ -0,0 +1,58 @@
+/*
+ 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_GUI_RGBA_HPP
+#define INGEN_GUI_RGBA_HPP
+
+#include <cmath>
+
+namespace ingen {
+namespace gui {
+
+static inline uint32_t
+rgba_to_uint(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
+{
+ return ((((uint32_t)(r)) << 24) |
+ (((uint32_t)(g)) << 16) |
+ (((uint32_t)(b)) << 8) |
+ (((uint32_t)(a))));
+}
+
+static inline uint8_t
+mono_interpolate(uint8_t v1, uint8_t v2, float f)
+{
+ return ((int)rint((v2) * (f) + (v1) * (1 - (f))));
+}
+
+#define RGBA_R(x) (((uint32_t)(x)) >> 24)
+#define RGBA_G(x) ((((uint32_t)(x)) >> 16) & 0xFF)
+#define RGBA_B(x) ((((uint32_t)(x)) >> 8) & 0xFF)
+#define RGBA_A(x) (((uint32_t)(x)) & 0xFF)
+
+static inline uint32_t
+rgba_interpolate(uint32_t c1, uint32_t c2, float f)
+{
+ return rgba_to_uint(
+ mono_interpolate(RGBA_R(c1), RGBA_R(c2), f),
+ mono_interpolate(RGBA_G(c1), RGBA_G(c2), f),
+ mono_interpolate(RGBA_B(c1), RGBA_B(c2), f),
+ mono_interpolate(RGBA_A(c1), RGBA_A(c2), f));
+}
+
+} // namespace gui
+} // namespace ingen
+
+#endif // INGEN_GUI_RGBA_HPP
diff --git a/src/gui/wscript b/src/gui/wscript
new file mode 100644
index 00000000..a23e2084
--- /dev/null
+++ b/src/gui/wscript
@@ -0,0 +1,111 @@
+#!/usr/bin/env python
+import waflib.extras.autowaf as autowaf
+import waflib.Utils as Utils
+import waflib.Options as Options
+
+def options(ctx):
+ opt = ctx.configuration_options()
+ opt.add_option('--light-theme', action='store_true', dest='light_theme',
+ help='use light coloured theme')
+
+def configure(conf):
+ autowaf.check_pkg(conf, 'glibmm-2.4', uselib_store='GLIBMM',
+ atleast_version='2.14.0', system=True, mandatory=False)
+ autowaf.check_pkg(conf, 'gthread-2.0', uselib_store='GTHREAD',
+ atleast_version='2.14.0', system=True, mandatory=False)
+ autowaf.check_pkg(conf, 'gtkmm-2.4', uselib_store='GTKMM',
+ atleast_version='2.14.0', system=True, mandatory=False)
+ autowaf.check_pkg(conf, 'ganv-1', uselib_store='GANV',
+ atleast_version='1.5.4', mandatory=False)
+ if not Options.options.no_webkit:
+ autowaf.check_pkg(conf, 'webkit-1.0', uselib_store='WEBKIT',
+ atleast_version='1.4.0', system=True, mandatory=False)
+
+ if conf.env.HAVE_GANV and conf.env.HAVE_GTKMM:
+ autowaf.define(conf, 'INGEN_BUILD_GUI', 1)
+
+ if Options.options.light_theme:
+ autowaf.define(conf, 'INGEN_USE_LIGHT_THEME', 1)
+
+def build(bld):
+ obj = bld(features = 'cxx cxxshlib',
+ export_includes = ['../..'],
+ includes = ['../..'],
+ name = 'libingen_gui',
+ target = 'ingen_gui',
+ install_path = '${LIBDIR}',
+ use = 'libingen libingen_client')
+ autowaf.use_lib(bld, obj, '''
+ GANV
+ GLADEMM
+ GLIBMM
+ GNOMECANVAS
+ GTKMM
+ LILV
+ LV2
+ RAUL
+ SIGCPP
+ SORD
+ SOUP
+ SUIL
+ WEBKIT
+ ''')
+
+ obj.source = '''
+ App.cpp
+ Arc.cpp
+ BreadCrumbs.cpp
+ ConnectWindow.cpp
+ GraphBox.cpp
+ GraphCanvas.cpp
+ GraphPortModule.cpp
+ GraphTreeWindow.cpp
+ GraphView.cpp
+ GraphWindow.cpp
+ LoadGraphWindow.cpp
+ LoadPluginWindow.cpp
+ MessagesWindow.cpp
+ NewSubgraphWindow.cpp
+ NodeMenu.cpp
+ NodeModule.cpp
+ ObjectMenu.cpp
+ PluginMenu.cpp
+ Port.cpp
+ PortMenu.cpp
+ PropertiesWindow.cpp
+ RDFS.cpp
+ RenameWindow.cpp
+ Style.cpp
+ SubgraphModule.cpp
+ ThreadedLoader.cpp
+ URIEntry.cpp
+ WidgetFactory.cpp
+ WindowFactory.cpp
+ ingen_gui.cpp
+ '''
+
+ # XML UI definition
+ bld(features = 'subst',
+ source = 'ingen_gui.ui',
+ target = '../../ingen_gui.ui',
+ install_path = '${DATADIR}/ingen',
+ chmod = Utils.O755,
+ INGEN_VERSION = bld.env.INGEN_VERSION)
+
+ # Gtk style
+ bld(features = 'subst',
+ is_copy = True,
+ source = 'ingen_style.rc',
+ target = '../../ingen_style.rc',
+ install_path = '${DATADIR}/ingen',
+ chmod = Utils.O755)
+
+ # LV2 UI
+ obj = bld(features = 'cxx cxxshlib',
+ source = 'ingen_gui_lv2.cpp',
+ includes = ['.', '../..'],
+ name = 'ingen_gui_lv2',
+ target = 'ingen_gui_lv2',
+ install_path = '${LV2DIR}/ingen.lv2/',
+ use = 'libingen libingen_gui')
+ autowaf.use_lib(bld, obj, 'LV2 SERD SORD LILV RAUL GLIBMM GTKMM')