diff options
Diffstat (limited to 'src/gui')
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 <http://drobilla.net></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 <d@drobilla.net></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"><b>Graph Search Path: </b></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"><i>Example: /foo/bar:/home/user/graphs</i></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"><b>Name</b></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"><b>Name</b></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"><b>Name</b></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') |