/* This file is part of Ingen. Copyright 2007-2012 David Robillard 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 . */ #include #include #include #include #include #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 "App.hpp" #include "PropertiesWindow.hpp" #include "RDFS.hpp" using namespace std; namespace Ingen { using namespace Client; namespace GUI { typedef std::set URISet; PropertiesWindow::PropertiesWindow(BaseObjectType* cobject, const Glib::RefPtr& xml) : Window(cobject) { 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_entry", _value_entry); xml->get_widget("properties_value_button", _value_button); 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)); _value_button->signal_event().connect( sigc::mem_fun(this, &PropertiesWindow::value_clicked)); _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(); _value_entry->set_text(""); _records.clear(); _model.reset(); _table->children().clear(); _table->resize(1, 3); _table->property_n_rows() = 1; } void PropertiesWindow::present(SPtr model) { set_object(model); Gtk::Window::present(); } void PropertiesWindow::add_property(const Raul::URI& uri, 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(), uri.c_str()); Glib::ustring lab_text = RDFS::label(world, prop); if (lab_text.empty()) { lab_text = world->rdf_world()->prefixes().qualify(uri); } lab_text = Glib::ustring("" + lab_text + ""; Gtk::Label* lab = manage(new Gtk::Label(lab_text, 1.0, 0.5)); lab->set_use_markup(true); set_tooltip(lab, prop); _table->attach(*lab, 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, 0.0)); Gtk::CheckButton* present = manage(new Gtk::CheckButton()); Gtk::Widget* val_widget = create_value_widget(uri, value); present->set_active(true); if (val_widget) { align->add(*val_widget); 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.insert(make_pair(uri, Record(value, align, n_rows, present))); lilv_node_free(prop); } /** 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 model) { reset(); _model = model; set_title(model->path() + " Properties - Ingen"); World* world = _app->world(); LilvNode* rdfs_range = lilv_new_uri( world->lilv_world(), LILV_NS_RDFS "range"); LilvNode* rdf_type = lilv_new_uri( world->lilv_world(), LILV_NS_RDF "type"); // Populate key combo const URISet props = RDFS::properties(world, model); for (const auto& p : props) { LilvNode* prop = lilv_new_uri(world->lilv_world(), p.c_str()); const Glib::ustring label = RDFS::label(world, prop); if (label.empty()) { continue; } // Get all classes in the range of this property (including sub-classes) LilvNodes* range = lilv_world_find_nodes( world->lilv_world(), prop, rdfs_range, NULL); URISet ranges; LILV_FOREACH(nodes, r, range) { ranges.insert(Raul::URI(lilv_node_as_string(lilv_nodes_get(range, r)))); } RDFS::classes(world, ranges, false); bool show = false; for (const auto& r : ranges) { LilvNode* range = lilv_new_uri(world->lilv_world(), r.c_str()); LilvNodes* objects = lilv_world_find_nodes( world->lilv_world(), NULL, rdf_type, range); show = lilv_nodes_get_first(objects); lilv_nodes_free(objects); lilv_node_free(range); if (show) { break; // At least one appliable object } } if (show || ranges.empty()) { Gtk::ListStore::iterator ki = _key_store->append(); Gtk::ListStore::Row row = *ki; row[_combo_columns.uri_col] = p; row[_combo_columns.label_col] = label; } lilv_node_free(prop); } lilv_node_free(rdfs_range); 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::property_changed)); _property_removed_connection = model->signal_property_removed().connect( sigc::mem_fun(this, &PropertiesWindow::property_removed)); } Gtk::Widget* PropertiesWindow::create_value_widget(const Raul::URI& uri, const Atom& value) { Ingen::Forge& forge = _app->forge(); if (value.type() == forge.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); widget->set_value(value.get()); widget->signal_value_changed().connect( sigc::bind(sigc::mem_fun(this, &PropertiesWindow::value_edited), uri)); return widget; } else if (value.type() == forge.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_value(value.get()); widget->set_increments(0.1, 1.0); widget->signal_value_changed().connect( sigc::bind(sigc::mem_fun(this, &PropertiesWindow::value_edited), uri)); return widget; } else if (value.type() == forge.Bool) { Gtk::CheckButton* widget = manage(new Gtk::CheckButton()); widget->set_active(value.get()); widget->signal_toggled().connect( sigc::bind(sigc::mem_fun(this, &PropertiesWindow::value_edited), uri)); return widget; } else if (value.type() == forge.URI) { Gtk::Entry* widget = manage(new Gtk::Entry()); widget->set_text(value.ptr()); widget->signal_changed().connect( sigc::bind(sigc::mem_fun(this, &PropertiesWindow::value_edited), uri)); return widget; } else if (value.type() == forge.URID) { const char* val_uri = _app->world()->uri_map().unmap_uri(value.get()); Gtk::Entry* widget = manage(new Gtk::Entry()); if (val_uri) { widget->set_text(val_uri); } widget->signal_changed().connect( sigc::bind(sigc::mem_fun(this, &PropertiesWindow::value_edited), uri)); return widget; } else if (value.type() == forge.String) { Gtk::Entry* widget = manage(new Gtk::Entry()); widget->set_text(value.ptr()); widget->signal_changed().connect( sigc::bind(sigc::mem_fun(this, &PropertiesWindow::value_edited), uri)); return widget; } _app->log().error(fmt("Unable to create widget for value %1% type %2%\n") % forge.str(value) % value.type()); return NULL; } void PropertiesWindow::on_show() { static const int WIN_PAD = 64; static const int VBOX_PAD = 16; int width = 0; int height = 0; Gtk::Requisition req; for (const auto& c : _vbox->children()) { req = c.get_widget()->size_request(); width = std::max(width, req.width); height += req.height + VBOX_PAD; } req = _table->size_request(); width = 1.6 * 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::property_changed(const Raul::URI& predicate, const Atom& value) { Records::iterator r = _records.find(predicate); if (r == _records.end()) { add_property(predicate, value); _table->show_all(); return; } Record& record = r->second; Gtk::Widget* value_widget = create_value_widget(predicate, value); record.value_widget->remove(); if (value_widget) { record.value_widget->add(*value_widget); value_widget->show(); } record.value = value; } void PropertiesWindow::property_removed(const Raul::URI& predicate, 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(); } void PropertiesWindow::value_edited(const Raul::URI& predicate) { Records::iterator r = _records.find(predicate); if (r == _records.end()) { _app->log().error(fmt("Unknown property `%1%' edited\n") % predicate); return; } Forge& forge = _app->forge(); Record& record = r->second; LV2_URID type = record.value.type(); if (type == forge.Int) { Gtk::SpinButton* widget = dynamic_cast(record.value_widget->get_child()); if (!widget) goto bad_type; record.value = _app->forge().make(widget->get_value_as_int()); } else if (type == forge.Float) { Gtk::SpinButton* widget = dynamic_cast(record.value_widget->get_child()); if (!widget) goto bad_type; record.value = _app->forge().make(static_cast(widget->get_value())); } else if (type == forge.Bool) { Gtk::CheckButton* widget = dynamic_cast(record.value_widget->get_child()); if (!widget) goto bad_type; record.value = _app->forge().make(widget->get_active()); } else if (type == forge.URI) { Gtk::Entry* widget = dynamic_cast(record.value_widget->get_child()); if (!widget) goto bad_type; record.value = _app->forge().alloc_uri(widget->get_text()); } else if (type == forge.String) { Gtk::Entry* widget = dynamic_cast(record.value_widget->get_child()); if (!widget) goto bad_type; record.value = _app->forge().alloc(widget->get_text()); } return; bad_type: _app->log().error(fmt("Property `%1%' value widget has wrong type\n") % predicate); return; } std::string PropertiesWindow::active_property() 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() { /* TODO: Clear value? Build new selector widget, once one for things other than URIs actually exists. At the moment, clicking the menu button will generate the appropriate menu anyway. */ } void PropertiesWindow::set_tooltip(Gtk::Widget* widget, const LilvNode* node) { const Glib::ustring comment = RDFS::comment(_app->world(), node); if (!comment.empty()) { widget->set_tooltip_text(comment); } } void PropertiesWindow::add_class_menu_item(Gtk::Menu* menu, const LilvNode* klass) { const Glib::ustring label = RDFS::label(_app->world(), klass); Gtk::Menu* submenu = build_subclass_menu(klass); if (submenu) { menu->items().push_back(Gtk::Menu_Helpers::MenuElem(label)); Gtk::MenuItem* menu_item = &(menu->items().back()); set_tooltip(menu_item, klass); menu_item->set_submenu(*submenu); } else { menu->items().push_back( Gtk::Menu_Helpers::MenuElem( label, sigc::bind(sigc::mem_fun(this, &PropertiesWindow::uri_chosen), std::string(lilv_node_as_uri(klass))))); } set_tooltip(&(menu->items().back()), klass); } Gtk::Menu* PropertiesWindow::build_subclass_menu(const LilvNode* klass) { World* world = _app->world(); LilvNode* rdfs_subClassOf = lilv_new_uri( world->lilv_world(), LILV_NS_RDFS "subClassOf"); LilvNodes* subclasses = lilv_world_find_nodes( world->lilv_world(), NULL, rdfs_subClassOf, klass); if (lilv_nodes_size(subclasses) == 0) { return NULL; } const Glib::ustring label = RDFS::label(world, klass); Gtk::Menu* menu = new Gtk::Menu(); // Add "header" item for choosing this class itself menu->items().push_back( Gtk::Menu_Helpers::MenuElem( label, sigc::bind(sigc::mem_fun(this, &PropertiesWindow::uri_chosen), std::string(lilv_node_as_uri(klass))))); menu->items().push_back(Gtk::Menu_Helpers::SeparatorElem()); set_tooltip(&(menu->items().back()), klass); // Add an item (and maybe submenu) for each subclass LILV_FOREACH(nodes, s, subclasses) { add_class_menu_item(menu, lilv_nodes_get(subclasses, s)); } lilv_nodes_free(subclasses); return menu; } void PropertiesWindow::build_value_menu(Gtk::Menu* menu, const LilvNodes* ranges) { World* world = _app->world(); LilvWorld* lworld = world->lilv_world(); LilvNode* rdf_type = lilv_new_uri(lworld, LILV_NS_RDF "type"); LilvNode* rdfs_Class = lilv_new_uri(lworld, LILV_NS_RDFS "Class"); LilvNode* rdfs_subClassOf = lilv_new_uri(lworld, LILV_NS_RDFS "subClassOf"); LILV_FOREACH(nodes, r, ranges) { const LilvNode* klass = lilv_nodes_get(ranges, r); if (!lilv_node_is_uri(klass)) { continue; } const char* uri = lilv_node_as_string(klass); // Add items for instances of this class RDFS::URISet ranges_uris; ranges_uris.insert(Raul::URI(uri)); RDFS::Objects values = RDFS::instances(world, ranges_uris); for (const auto& v : values) { const LilvNode* inst = lilv_new_uri(lworld, v.first.c_str()); Glib::ustring label = RDFS::label(world, inst); if (label.empty()) { label = lilv_node_as_string(inst); } if (lilv_world_ask(world->lilv_world(), inst, rdf_type, rdfs_Class)) { if (!lilv_world_ask(lworld, inst, rdfs_subClassOf, NULL) || lilv_world_ask(lworld, inst, rdfs_subClassOf, inst)) { add_class_menu_item(menu, inst); } } else { menu->items().push_back( Gtk::Menu_Helpers::MenuElem( label, sigc::bind(sigc::mem_fun(this, &PropertiesWindow::uri_chosen), std::string(lilv_node_as_uri(inst))))); set_tooltip(&(menu->items().back()), inst); } } } } void PropertiesWindow::uri_chosen(const std::string& uri) { _value_entry->set_text(uri); } bool PropertiesWindow::value_clicked(GdkEvent* ev) { if (ev->type != GDK_BUTTON_PRESS) { return false; } // Get currently selected property (key) to add const std::string prop_uri = active_property(); if (prop_uri.empty()) { return false; } World* world = _app->world(); LilvNode* rdfs_range = lilv_new_uri( world->lilv_world(), LILV_NS_RDFS "range"); LilvNode* prop = lilv_new_uri(world->lilv_world(), prop_uri.c_str()); LilvNodes* ranges = lilv_world_find_nodes( world->lilv_world(), prop, rdfs_range, NULL); Gtk::Menu* menu = new Gtk::Menu(); build_value_menu(menu, ranges); menu->popup(ev->button.button, ev->button.time); return true; } void PropertiesWindow::add_clicked() { if (!_key_combo->get_active() || _value_entry->get_text().empty()) { return; } const Gtk::ListStore::Row krow = *(_key_combo->get_active()); const Glib::ustring key_uri = krow[_combo_columns.uri_col]; const Glib::ustring value_uri = _value_entry->get_text(); Atom value = _app->forge().alloc_uri(value_uri); Resource::Properties properties; properties.insert(make_pair(Raul::URI(key_uri.c_str()), Resource::Property(value))); _app->interface()->put(_model->uri(), properties); } void PropertiesWindow::cancel_clicked() { reset(); Gtk::Window::hide(); } void PropertiesWindow::apply_clicked() { Resource::Properties remove; Resource::Properties add; for (const auto& r : _records) { const Raul::URI& uri = r.first; const Record& record = r.second; if (record.present_button->get_active()) { if (!_model->has_property(uri, record.value)) { add.insert(make_pair(uri, record.value)); } } else { remove.insert(make_pair(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