/** @file * @brief A dialog for CSS selectors */ /* Authors: * Kamalpreet Kaur Grewal * Tavmjong Bah * * Copyright (C) Kamalpreet Kaur Grewal 2016 * Copyright (C) Tavmjong Bah 2017 * * Released under GNU GPL, read the file 'COPYING' for more information */ #include "styledialog.h" #include "verbs.h" #include "selection.h" #include "attribute-rel-svg.h" #include "inkscape.h" #include "document-undo.h" #include "ui/widget/addtoicon.h" #include "xml/attribute-record.h" #include "xml/node-observer.h" #include #include #include #include //#define DEBUG_STYLEDIALOG using Inkscape::DocumentUndo; using Inkscape::Util::List; using Inkscape::XML::AttributeRecord; /** * This macro is used to remove spaces around selectors or any strings when * parsing is done to update XML style element or row labels in this dialog. */ #define REMOVE_SPACES(x) x.erase(0, x.find_first_not_of(' ')); \ x.erase(x.find_last_not_of(' ') + 1); namespace Inkscape { namespace UI { namespace Dialog { // Keeps a watch on style element class StyleDialog::NodeObserver : public Inkscape::XML::NodeObserver { public: NodeObserver(StyleDialog* styleDialog) : _styleDialog(styleDialog) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::NodeObserver: Constructor" << std::endl; #endif }; void notifyContentChanged(Inkscape::XML::Node &node, Inkscape::Util::ptr_shared old_content, Inkscape::Util::ptr_shared new_content) override; StyleDialog * _styleDialog; }; void StyleDialog::NodeObserver::notifyContentChanged( Inkscape::XML::Node &/*node*/, Inkscape::Util::ptr_shared /*old_content*/, Inkscape::Util::ptr_shared /*new_content*/ ) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::NodeObserver::notifyContentChanged" << std::endl; #endif _styleDialog->_readStyleElement(); _styleDialog->_selectRow(); } // Keeps a watch for new/removed/changed nodes // (Must update objects that selectors match.) class StyleDialog::NodeWatcher : public Inkscape::XML::NodeObserver { public: NodeWatcher(StyleDialog* styleDialog, Inkscape::XML::Node *repr) : _styleDialog(styleDialog), _repr(repr) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::NodeWatcher: Constructor" << std::endl; #endif }; void notifyChildAdded( Inkscape::XML::Node &/*node*/, Inkscape::XML::Node &child, Inkscape::XML::Node */*prev*/ ) override { if ( _styleDialog && _repr ) { _styleDialog->_nodeAdded( child ); } } void notifyChildRemoved( Inkscape::XML::Node &/*node*/, Inkscape::XML::Node &child, Inkscape::XML::Node */*prev*/ ) override { if ( _styleDialog && _repr ) { _styleDialog->_nodeRemoved( child ); } } void notifyAttributeChanged( Inkscape::XML::Node &node, GQuark qname, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ ) override { if ( _styleDialog && _repr ) { // For the moment only care about attributes that are directly used in selectors. const gchar * cname = g_quark_to_string (qname ); Glib::ustring name; if (cname) { name = cname; } if ( name == "id" || name == "class" ) { _styleDialog->_nodeChanged( node ); } } } StyleDialog * _styleDialog; Inkscape::XML::Node * _repr; // Need to track if document changes. }; void StyleDialog::_nodeAdded( Inkscape::XML::Node &node ) { StyleDialog::NodeWatcher *w = new StyleDialog::NodeWatcher (this, &node); node.addObserver (*w); _nodeWatchers.push_back(w); _readStyleElement(); _selectRow(); } void StyleDialog::_nodeRemoved( Inkscape::XML::Node &repr ) { for (auto it = _nodeWatchers.begin(); it != _nodeWatchers.end(); ++it) { if ( (*it)->_repr == &repr ) { (*it)->_repr->removeObserver (**it); _nodeWatchers.erase( it ); break; } } _readStyleElement(); _selectRow(); } void StyleDialog::_nodeChanged( Inkscape::XML::Node &object ) { _readStyleElement(); _selectRow(); } StyleDialog::TreeStore::TreeStore() = default; /** * Allow dragging only selectors. */ bool StyleDialog::TreeStore::row_draggable_vfunc(const Gtk::TreeModel::Path& path) const { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::TreeStore::row_draggable_vfunc" << std::endl; #endif auto unconstThis = const_cast(this); const_iterator iter = unconstThis->get_iter(path); if (iter) { Gtk::TreeModel::Row row = *iter; bool is_draggable = row[_styledialog->_mColumns._colIsSelector]; return is_draggable; } return Gtk::TreeStore::row_draggable_vfunc(path); } /** * Allow dropping only in between other selectors. */ bool StyleDialog::TreeStore::row_drop_possible_vfunc(const Gtk::TreeModel::Path& dest, const Gtk::SelectionData& selection_data) const { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::TreeStore::row_drop_possible_vfunc" << std::endl; #endif Gtk::TreeModel::Path dest_parent = dest; dest_parent.up(); return dest_parent.empty(); } // This is only here to handle updating style element after a drag and drop. void StyleDialog::TreeStore::on_row_deleted(const TreeModel::Path& path) { #ifdef DEBUG_STYLEDIALOG std::cout << "on_row_deleted" << std::endl; #endif if (_styledialog->_updating) return; // Don't write if we deleted row (other than from DND) _styledialog->_writeStyleElement(); } Glib::RefPtr StyleDialog::TreeStore::create(StyleDialog *styledialog) { StyleDialog::TreeStore * store = new StyleDialog::TreeStore(); store->_styledialog = styledialog; store->set_column_types( store->_styledialog->_mColumns ); return Glib::RefPtr( store ); } /** * Constructor * A treeview and a set of two buttons are added to the dialog. _addSelector * adds selectors to treeview. _delSelector deletes the selector from the dialog. * Any addition/deletion of the selectors updates XML style element accordingly. */ StyleDialog::StyleDialog() : UI::Widget::Panel("/dialogs/style", SP_VERB_DIALOG_STYLE), _updating(false), _textNode(nullptr), _desktopTracker() { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::StyleDialog" << std::endl; #endif // Tree Inkscape::UI::Widget::AddToIcon * addRenderer = manage( new Inkscape::UI::Widget::AddToIcon() ); _store = TreeStore::create(this); _treeView.set_model(_store); _treeView.set_headers_visible(true); _treeView.enable_model_drag_source(); _treeView.enable_model_drag_dest( Gdk::ACTION_MOVE ); int addCol = _treeView.append_column("", *addRenderer) - 1; Gtk::TreeViewColumn *col = _treeView.get_column(addCol); if ( col ) { col->add_attribute( addRenderer->property_active(), _mColumns._colIsSelector ); } _treeView.append_column("CSS Selector", _mColumns._colSelector); _treeView.set_expander_column(*(_treeView.get_column(1))); // Pack widgets _paned.set_orientation(Gtk::ORIENTATION_VERTICAL); _paned.pack1(_mainBox, Gtk::SHRINK); _mainBox.set_orientation(Gtk::ORIENTATION_VERTICAL); _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET); _scrolledWindow.add(_treeView); _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); create = manage( new Gtk::Button() ); _styleButton(*create, "list-add", "Add a new CSS Selector"); create->signal_clicked().connect(sigc::mem_fun(*this, &StyleDialog::_addSelector)); del = manage( new Gtk::Button() ); _styleButton(*del, "list-remove", "Remove a CSS Selector"); del->signal_clicked().connect(sigc::mem_fun(*this, &StyleDialog::_delSelector)); del->set_sensitive(false); _mainBox.pack_end(_buttonBox, Gtk::PACK_SHRINK); _buttonBox.pack_start(*create, Gtk::PACK_SHRINK); _buttonBox.pack_start(*del, Gtk::PACK_SHRINK); _getContents()->pack_start(_paned, Gtk::PACK_EXPAND_WIDGET); // Dialog size request Gtk::Requisition sreq1, sreq2; get_preferred_size(sreq1, sreq2); int minWidth = 300; int minHeight = 300; minWidth = (sreq2.width > minWidth ? sreq2.width : minWidth ); minHeight = (sreq2.height > minHeight ? sreq2.height : minHeight); set_size_request(minWidth, minHeight); // Signal handlers _treeView.signal_button_release_event().connect( // Needs to be release, not press. sigc::mem_fun(*this, &StyleDialog::_handleButtonEvent), false); _treeView.signal_button_release_event().connect_notify( sigc::mem_fun(*this, &StyleDialog::_buttonEventsSelectObjs), false); //_treeView.get_selection()->signal_changed().connect( // sigc::mem_fun(*this, &StyleDialog::_selChanged)); _objObserver.signal_changed().connect(sigc::mem_fun(*this, &StyleDialog::_objChanged)); // Add CSS dialog _cssPane = new CssDialog; _paned.pack2(*_cssPane, Gtk::SHRINK); _cssPane->show_all(); _cssPane->_propRenderer->signal_edited().connect( sigc::mem_fun(*this, &StyleDialog::_handleProp)); _cssPane->_sheetRenderer->signal_edited().connect( sigc::mem_fun(*this, &StyleDialog::_handleSheet)); _cssPane->_attrRenderer->signal_edited().connect( sigc::mem_fun(*this, &StyleDialog::_handleAttr)); _cssPane->_treeView.signal_button_release_event().connect( sigc::mem_fun(*this, &StyleDialog::_delProperty), false); // Document & Desktop _desktop_changed_connection = _desktopTracker.connectDesktopChanged( sigc::mem_fun(*this, &StyleDialog::_handleDesktopChanged) ); _desktopTracker.connect(GTK_WIDGET(gobj())); _document_replaced_connection = getDesktop()->connectDocumentReplaced( sigc::mem_fun(this, &StyleDialog::_handleDocumentReplaced)); _selection_changed_connection = getDesktop()->getSelection()->connectChanged( sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged))); // Add watchers _updateWatchers(); // Load tree _readStyleElement(); _selectRow(); if (!_store->children().empty()) { del->set_sensitive(true); } } /** * @brief StyleDialog::~StyleDialog * Class destructor */ StyleDialog::~StyleDialog() { #ifdef DEBUG_STYLEDIALOOG std::cout << "StyleDialog::~StyleDialog" << std::endl; #endif _desktop_changed_connection.disconnect(); _document_replaced_connection.disconnect(); _selection_changed_connection.disconnect(); } /** * @brief StyleDialog::_styleTextNode * @return Inkscape::XML::Node* pointing to a style element's text node. * Returns the style element's text node. If there is no style element, one is created. * Ditto for text node. */ Inkscape::XML::Node* StyleDialog::_getStyleTextNode() { Inkscape::XML::Node *styleNode = nullptr; Inkscape::XML::Node *textNode = nullptr; Inkscape::XML::Node *root = SP_ACTIVE_DOCUMENT->getReprRoot(); for (unsigned i = 0; i < root->childCount(); ++i) { if (Glib::ustring(root->nthChild(i)->name()) == "svg:style") { styleNode = root->nthChild(i); for (unsigned j = 0; j < styleNode->childCount(); ++j) { if (styleNode->nthChild(j)->type() == Inkscape::XML::TEXT_NODE) { textNode = styleNode->nthChild(j); } } if (textNode == nullptr) { // Style element found but does not contain text node! std::cerr << "StyleDialog::_getStyleTextNode(): No text node!" << std::endl; textNode = SP_ACTIVE_DOCUMENT->getReprDoc()->createTextNode(""); styleNode->appendChild(textNode); Inkscape::GC::release(textNode); } } } if (styleNode == nullptr) { // Style element not found, create one styleNode = SP_ACTIVE_DOCUMENT->getReprDoc()->createElement("svg:style"); textNode = SP_ACTIVE_DOCUMENT->getReprDoc()->createTextNode(""); styleNode->appendChild(textNode); Inkscape::GC::release(textNode); root->addChild(styleNode, nullptr); Inkscape::GC::release(styleNode); } if (_textNode != textNode) { _textNode = textNode; NodeObserver *no = new NodeObserver(this); textNode->addObserver(*no); } return textNode; } /** * @brief StyleDialog::_readStyleElement * Fill the Gtk::TreeStore from the svg:style element. */ void StyleDialog::_readStyleElement() { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_readStyleElement: updating " << (_updating?"true":"false")<< std::endl; #endif if (_updating) return; // Don't read if we wrote style element. _updating = true; _store->clear(); Inkscape::XML::Node * textNode = _getStyleTextNode(); if (textNode == nullptr) { std::cerr << "StyleDialog::_readStyleElement: No text node!" << std::endl; } // Get content from style text node. std::string content = (textNode->content() ? textNode->content() : ""); // Remove end-of-lines (check it works on Windoze). content.erase(std::remove(content.begin(), content.end(), '\n'), content.end()); // Remove comments (/* xxx */) while(content.find("/*") != std::string::npos) { size_t start = content.find("/*"); content.erase(start, (content.find("*/", start) - start) +2); } // First split into selector/value chunks. // An attempt to use Glib::Regex failed. A C++11 version worked but // reportedly has problems on Windows. Using split_simple() is simpler // and probably faster. // // Glib::RefPtr regex1 = // Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}"); // // Glib::MatchInfo minfo; // regex1->match(content, minfo); // Split on curly brackets. Even tokens are selectors, odd are values. std::vector tokens = Glib::Regex::split_simple("[}{]", content); // If text node is empty, return (avoids problem with negative below). if (tokens.size() == 0) { _updating = false; return; } for (unsigned i = 0; i < tokens.size()-1; i += 2) { Glib::ustring selector = tokens[i]; REMOVE_SPACES(selector); // Remove leading/trailing spaces // Get list of objects selector matches std::vector objVec = _getObjVec( selector ); Glib::ustring properties; // Check to make sure we do have a value to match selector. if ((i+1) < tokens.size()) { properties = tokens[i+1]; } else { std::cerr << "StyleDialog::_readStyleElement: Missing values " "for last selector!" << std::endl; } REMOVE_SPACES(properties); Gtk::TreeModel::Row row = *(_store->append()); row[_mColumns._colSelector] = selector; row[_mColumns._colIsSelector] = true; row[_mColumns._colObj] = objVec; row[_mColumns._colProperties] = properties; // Add as children, objects that match selector. for (auto& obj: objVec) { if (obj->cloned) continue; // Skip cloned objects (they also don't have 'id'). Gtk::TreeModel::Row childrow = *(_store->append(row->children())); childrow[_mColumns._colSelector] = "#" + Glib::ustring(obj->getId()); childrow[_mColumns._colIsSelector] = false; childrow[_mColumns._colObj] = std::vector(1, obj); childrow[_mColumns._colProperties] = ""; // Unused } } _updating = false; } /** * @brief StyleDialog::_writeStyleElement * Update the content of the style element as selectors (or objects) are added/removed. */ void StyleDialog::_writeStyleElement() { _updating = true; Glib::ustring styleContent; for (auto& row: _store->children()) { styleContent = styleContent + row[_mColumns._colSelector] + " { " + row[_mColumns._colProperties] + " }\n"; } // We could test if styleContent is empty and then delete the style node here but there is no // harm in keeping it around ... Inkscape::XML::Node *textNode = _getStyleTextNode(); textNode->setContent(styleContent.c_str()); DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_STYLE, _("Edited style element.")); _updating = false; #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_writeStyleElement(): |" << styleContent << "|" << std::endl; #endif } void StyleDialog::_addWatcherRecursive(Inkscape::XML::Node *node) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_addWatcherRecursive()" << std::endl; #endif StyleDialog::NodeWatcher *w = new StyleDialog::NodeWatcher(this, node); node->addObserver(*w); _nodeWatchers.push_back(w); for (unsigned i = 0; i < node->childCount(); ++i) { _addWatcherRecursive(node->nthChild(i)); } } /** * @brief StyleDialog::_updateWatchers * Update the watchers on objects. */ void StyleDialog::_updateWatchers() { _updating = true; // Remove old document watchers while (!_nodeWatchers.empty()) { StyleDialog::NodeWatcher *w = _nodeWatchers.back(); w->_repr->removeObserver(*w); _nodeWatchers.pop_back(); delete w; } // Recursively add new watchers Inkscape::XML::Node *root = SP_ACTIVE_DOCUMENT->getReprRoot(); _addWatcherRecursive(root); #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_updateWatchers(): " << _nodeWatchers.size() << std::endl; #endif _updating = false; } /** * @brief StyleDialog::_addToSelector * @param row * Add selected objects on the desktop to the selector corresponding to 'row'. */ void StyleDialog::_addToSelector(Gtk::TreeModel::Row row) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_addToSelector: Entrance" << std::endl; #endif if (*row) { Glib::ustring selector = row[_mColumns._colSelector]; if (selector[0] == '#') { // 'id' selector... add selected object's id's to list. Inkscape::Selection* selection = getDesktop()->getSelection(); for (auto& obj: selection->objects()) { Glib::ustring id = (obj->getId()?obj->getId():""); std::vector objVec = row[_mColumns._colObj]; bool found = false; for (auto& obj: objVec) { if (id == obj->getId()) { found = true; break; } } if (!found) { // Update row objVec.push_back(obj); // Adding to copy so need to update tree row[_mColumns._colObj] = objVec; row[_mColumns._colSelector] = _getIdList( objVec ); // Add child row Gtk::TreeModel::Row childrow = *(_store->append(row->children())); childrow[_mColumns._colSelector] = "#" + Glib::ustring(obj->getId()); childrow[_mColumns._colIsSelector] = false; childrow[_mColumns._colObj] = std::vector(1, obj); childrow[_mColumns._colProperties] = ""; // Unused } } } else if (selector[0] == '.') { // 'class' selector... add value to class attribute of selected objects. // Get first class (split on white space or comma) std::vector tokens = Glib::Regex::split_simple("[,\\s]+", selector); Glib::ustring className = tokens[0]; className.erase(0,1); // Get list of objects to modify Inkscape::Selection* selection = getDesktop()->getSelection(); std::vector objVec( selection->objects().begin(), selection->objects().end() ); _insertClass( objVec, className ); row[_mColumns._colObj] = _getObjVec( selector ); for (auto& obj: objVec) { // Add child row Gtk::TreeModel::Row childrow = *(_store->append(row->children())); childrow[_mColumns._colSelector] = "#" + Glib::ustring(obj->getId()); childrow[_mColumns._colIsSelector] = false; childrow[_mColumns._colObj] = std::vector(1, obj); childrow[_mColumns._colProperties] = ""; // Unused } } else { // Do nothing for element selectors. // std::cout << " Element selector... doing nothing!" << std::endl; } } _writeStyleElement(); } /** * @brief StyleDialog::_removeFromSelector * @param row * Remove the object corresponding to 'row' from the parent selector. */ void StyleDialog::_removeFromSelector(Gtk::TreeModel::Row row) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_removeFromSelector: Entrance" << std::endl; #endif if (*row) { Glib::ustring objectLabel = row[_mColumns._colSelector]; Gtk::TreeModel::iterator iter = row->parent(); if (iter) { Gtk::TreeModel::Row parent = *iter; Glib::ustring selector = parent[_mColumns._colSelector]; if (selector[0] == '#') { // 'id' selector... remove selected object's id's to list. // Erase from selector label. auto i = selector.find(objectLabel); if (i != Glib::ustring::npos) { selector.erase(i, objectLabel.length()); } // Erase any comma/space if (i != Glib::ustring::npos && selector[i] == ',') { selector.erase(i, 1); } if (i != Glib::ustring::npos && selector[i] == ' ') { selector.erase(i, 1); } // Update store if (selector.empty()) { _store->erase(parent); } else { // Save new selector and update object vector. parent[_mColumns._colSelector] = selector; parent[_mColumns._colObj] = _getObjVec( selector ); _store->erase(row); } } else if (selector[0] == '.') { // 'class' selector... remove value to class attribute of selected objects. std::vector objVec = row[_mColumns._colObj]; // Just one // Get first class (split on white space or comma) std::vector tokens = Glib::Regex::split_simple("[,\\s]+", selector); Glib::ustring className = tokens[0]; className.erase(0,1); // Erase '.' // Erase class name from 'class' attribute. Glib::ustring classAttr = objVec[0]->getRepr()->attribute("class"); auto i = classAttr.find( className ); if (i != Glib::ustring::npos) { classAttr.erase(i, className.length()); } if (i != Glib::ustring::npos && classAttr[i] == ' ') { classAttr.erase(i, 1); } objVec[0]->getRepr()->setAttribute("class", classAttr); parent[_mColumns._colObj] = _getObjVec( selector ); _store->erase(row); } else { // Do nothing for element selectors. // std::cout << " Element selector... doing nothing!" << std::endl; } } } _writeStyleElement(); } /** * @brief StyleDialog::_getIdList * @param sel * @return This function returns a comma separated list of ids for objects in input vector. * It is used in creating an 'id' selector. It relies on objects having 'id's. */ Glib::ustring StyleDialog::_getIdList(std::vector sel) { Glib::ustring str; for (auto& obj: sel) { str += "#" + Glib::ustring(obj->getId()) + ", "; } if (!str.empty()) { str.erase(str.size()-1); // Remove space at end. c++11 has pop_back() but not ustring. str.erase(str.size()-1); // Remove comma at end. } return str; } /** * @brief StyleDialog::_getObjVec * @param selector: a valid CSS selector string. * @return objVec: a vector of pointers to SPObject's the selector matches. * Return a vector of all objects that selector matches. */ std::vector StyleDialog::_getObjVec(Glib::ustring selector) { std::vector objVec = SP_ACTIVE_DOCUMENT->getObjectsBySelector( selector ); #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_getObjVec: |" << selector << "|" << std::endl; for (auto& obj: objVec) { std::cout << " " << (obj->getId()?obj->getId():"null") << std::endl; } #endif return objVec; } /** * @brief StyleDialog::_insertClass * @param objs: list of objects to insert class * @param class: class to insert * Insert a class name into objects' 'class' attribute. */ void StyleDialog::_insertClass(const std::vector& objVec, const Glib::ustring& className) { for (auto& obj: objVec) { if (!obj->getRepr()->attribute("class")) { // 'class' attribute does not exist, create it. obj->getRepr()->setAttribute("class", className); } else { // 'class' attribute exists, append. Glib::ustring classAttr = obj->getRepr()->attribute("class"); // Split on white space. std::vector tokens = Glib::Regex::split_simple("\\s+", classAttr); bool add = true; for (auto& token: tokens) { if (token == className) { add = false; // Might be useful to still add... break; } } if (add) { obj->getRepr()->setAttribute("class", classAttr + " " + className ); } } } } /** * @brief StyleDialog::_selectObjects * @param eventX * @param eventY * This function selects objects in the drawing corresponding to the selector * selected in the treeview. */ void StyleDialog::_selectObjects(int eventX, int eventY) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_selectObjects: " << eventX << ", " << eventY << std::endl; #endif getDesktop()->selection->clear(); Gtk::TreeViewColumn *col = _treeView.get_column(1); Gtk::TreeModel::Path path; int x2 = 0; int y2 = 0; // To do: We should be able to do this via passing in row. if (_treeView.get_path_at_pos(eventX, eventY, path, col, x2, y2)) { if (col == _treeView.get_column(1)) { Gtk::TreeModel::iterator iter = _store->get_iter(path); if (iter) { Gtk::TreeModel::Row row = *iter; Gtk::TreeModel::Children children = row.children(); std::vector objVec = row[_mColumns._colObj]; for (unsigned i = 0; i < objVec.size(); ++i) { SPObject *obj = objVec[i]; getDesktop()->selection->add(obj); } } } } } /** * @brief StyleDialog::_addSelector * * This function opens a dialog to add a selector. The dialog is prefilled * with an 'id' selector containing a list of the id's of selected objects * or with a 'class' selector if no objects are selected. */ void StyleDialog::_addSelector() { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_addSelector: Entrance" << std::endl; #endif // Store list of selected elements on desktop (not to be confused with selector). Inkscape::Selection* selection = getDesktop()->getSelection(); std::vector objVec( selection->objects().begin(), selection->objects().end() ); // ==== Create popup dialog ==== Gtk::Dialog *textDialogPtr = new Gtk::Dialog(); textDialogPtr->add_button(_("Cancel"), Gtk::RESPONSE_CANCEL); textDialogPtr->add_button(_("Add"), Gtk::RESPONSE_OK); Gtk::Entry *textEditPtr = manage ( new Gtk::Entry() ); textDialogPtr->get_content_area()->pack_start(*textEditPtr, Gtk::PACK_SHRINK); Gtk::Label *textLabelPtr = manage ( new Gtk::Label( _("Invalid entry: Not an id (#), class (.), or element CSS selector.") ) ); textDialogPtr->get_content_area()->pack_start(*textLabelPtr, Gtk::PACK_SHRINK); /** * By default, the entrybox contains 'Class1' as text. However, if object(s) * is(are) selected and user clicks '+' at the bottom of dialog, the * entrybox will have the id(s) of the selected objects as text. */ if (getDesktop()->getSelection()->isEmpty()) { textEditPtr->set_text(".Class1"); } else { textEditPtr->set_text(_getIdList(objVec)); } Gtk::Requisition sreq1, sreq2; textDialogPtr->get_preferred_size(sreq1, sreq2); int minWidth = 200; int minHeight = 100; minWidth = (sreq2.width > minWidth ? sreq2.width : minWidth ); minHeight = (sreq2.height > minHeight ? sreq2.height : minHeight); textDialogPtr->set_size_request(minWidth, minHeight); textEditPtr->show(); textLabelPtr->hide(); textDialogPtr->show(); // ==== Get response ==== int result = -1; bool invalid = true; Glib::ustring selectorValue; while (invalid) { result = textDialogPtr->run(); if (result != Gtk::RESPONSE_OK) { // Cancel, close dialog, etc. textDialogPtr->hide(); return; } /** * @brief selectorName * This string stores selector name. The text from entrybox is saved as name * for selector. If the entrybox is empty, the text (thus selectorName) is * set to ".Class1" */ selectorValue = textEditPtr->get_text(); Glib::ustring firstWord = selectorValue.substr(0, selectorValue.find_first_of(" >+~")); del->set_sensitive(true); if (selectorValue[0] == '.' || selectorValue[0] == '#' || selectorValue[0] == '*' || SPAttributeRelSVG::isSVGElement( firstWord ) ) { invalid = false; } else { textLabelPtr->show(); } } delete textDialogPtr; // ==== Handle response ==== // If class selector, add selector name to class attribute for each object if (selectorValue[0] == '.') { Glib::ustring className = selectorValue; className.erase(0,1); _insertClass(objVec, className); } // Generate a new object vector (we could have an element selector, // the user could have edited the id selector list, etc.). objVec = _getObjVec( selectorValue ); // Add entry to GUI tree Gtk::TreeModel::Row row = *(_store->append()); row[_mColumns._colSelector] = selectorValue; row[_mColumns._colIsSelector] = true; row[_mColumns._colObj] = objVec; // Add as children objects that match selector. for (auto& obj: objVec) { Gtk::TreeModel::Row childrow = *(_store->append(row->children())); childrow[_mColumns._colSelector] = "#" + Glib::ustring(obj->getId()); childrow[_mColumns._colIsSelector] = false; childrow[_mColumns._colObj] = std::vector(1, obj); } // Add entry to style element _writeStyleElement(); } /** * @brief StyleDialog::_delSelector * This function deletes selector when '-' at the bottom is clicked. * Note: If deleting a class selector, class attributes are NOT changed. */ void StyleDialog::_delSelector() { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_delSelector" << std::endl; #endif Glib::RefPtr refTreeSelection = _treeView.get_selection(); Gtk::TreeModel::iterator iter = refTreeSelection->get_selected(); if (iter) { Gtk::TreeModel::Row row = *iter; _updating = true; _store->erase(iter); _updating = false; _writeStyleElement(); } } /** * @brief StyleDialog::_handleButtonEvent * @param event * @return * Handles the event when '+' button in front of a selector name is clicked or when a '-' button in * front of a child object is clicked. In the first case, the selected objects on the desktop (if * any) are added as children of the selector in the treeview. In the latter case, the object * corresponding to the row is removed from the selector. */ bool StyleDialog::_handleButtonEvent(GdkEventButton *event) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_handleButtonEvent: Entrance" << std::endl; #endif if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { Gtk::TreeViewColumn *col = nullptr; Gtk::TreeModel::Path path; int x = static_cast(event->x); int y = static_cast(event->y); int x2 = 0; int y2 = 0; if (_treeView.get_path_at_pos(x, y, path, col, x2, y2)) { if (col == _treeView.get_column(0)) { Gtk::TreeModel::iterator iter = _store->get_iter(path); Gtk::TreeModel::Row row = *iter; // Add or remove objects from a if (!row.parent()) { // Add selected objects to selector. _addToSelector(row); } else { // Remove object from selector _removeFromSelector(row); } } } } return false; } // ------------------------------------------------------------------- class PropertyData { public: PropertyData() = default;; PropertyData(Glib::ustring name) : _name(std::move(name)) {}; void _setSheetValue(Glib::ustring value) { _sheetValue = value; }; void _setAttrValue(Glib::ustring value) { _attrValue = value; }; Glib::ustring _getName() { return _name; }; Glib::ustring _getSheetValue() { return _sheetValue; }; Glib::ustring _getAttrValue() { return _attrValue; }; private: Glib::ustring _name; Glib::ustring _sheetValue; Glib::ustring _attrValue; }; // ------------------------------------------------------------------- /** * @brief StyleDialog::_updateCSSPanel * Updates CSS panel according to row in Style panel. */ void StyleDialog::_updateCSSPanel() { // This should probably be in a member function of CSSDialog. #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_updateCSSPanel" << std::endl; #endif _updating = true; _cssPane->_store->clear(); Glib::RefPtr refTreeSelection = _treeView.get_selection(); Gtk::TreeModel::iterator iter = refTreeSelection->get_selected(); if (iter) { Gtk::TreeModel::Row row = *iter; Glib::ustring properties; Glib::ustring sheet; Glib::ustring attr; if (row[_mColumns._colIsSelector]) { _cssPane->_propRenderer->property_editable() = true; _cssPane->_sheetRenderer->property_editable() = true; _cssPane->_sheetRenderer->property_foreground_rgba() = Gdk::RGBA("black"); _cssPane->_attrRenderer->property_editable() = false; _cssPane->_buttonAddProperty.set_sensitive(true); properties = row[_mColumns._colProperties]; sheet = row[_mColumns._colProperties]; _objObserver.set( nullptr ); } else { _cssPane->_propRenderer->property_editable() = false; _cssPane->_sheetRenderer->property_editable() = false; _cssPane->_sheetRenderer->property_foreground_rgba() = Gdk::RGBA("gray"); _cssPane->_attrRenderer->property_editable() = false; // false for now... _cssPane->_buttonAddProperty.set_sensitive(false); std::vector objects = row[_mColumns._colObj]; Gtk::TreeModel::iterator piter = row.parent(); if (piter) { Gtk::TreeModel::Row prow = *piter; sheet = prow[_mColumns._colProperties]; } _objObserver.set( objects[0] ); if (objects[0] && objects[0]->getAttribute("style") != nullptr) { properties = objects[0]->getAttribute("style"); attr = objects[0]->getAttribute("style"); } } REMOVE_SPACES(properties); // Remove leading/trailing spaces std::map propMap; std::vector sheetList = Glib::Regex::split_simple("\\s*;\\s*", sheet); for (auto& token: sheetList) { if (token.empty()) break; std::vector pair = Glib::Regex::split_simple("\\s*:\\s*", token); if( pair.size() > 1) { PropertyData temp( pair[0] ); temp._setSheetValue( pair[1] ); propMap[pair[0]] = temp; } } std::vector attrList = Glib::Regex::split_simple("\\s*;\\s*", attr); for (auto& token: attrList) { if (token.empty()) break; std::vector pair = Glib::Regex::split_simple("\\s*:\\s*", token); if( pair.size() > 1) { auto it = propMap.find(pair[0]); if (it != propMap.end()) { (*it).second._setAttrValue( pair[1] ); } else { PropertyData temp(pair[0]); temp._setAttrValue( pair[1] ); propMap[pair[0]] = temp; } } } for (auto it : propMap) { // std::cout << " " << it.first // << " " << it.second._getName() // << " " << it.second._getSheetValue() // << " " << it.second._getAttrValue() // << std::endl; _cssPane->_propRow = *(_cssPane->_store->append()); _cssPane->_propRow[_cssPane->_cssColumns._colUnsetProp] = false; _cssPane->_propRow[_cssPane->_cssColumns._propertyLabel] = it.second._getName(); _cssPane->_propRow[_cssPane->_cssColumns._styleSheetVal] = it.second._getSheetValue(); _cssPane->_propRow[_cssPane->_cssColumns._styleAttrVal ] = it.second._getAttrValue(); } } _updating = false; } /** * Handle document replaced. (Happens when a default document is immediately replaced by another * document in a new window.) */ void StyleDialog::_handleDocumentReplaced(SPDesktop *desktop, SPDocument * /* document */) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::handleDocumentReplaced()" << std::endl; #endif _selection_changed_connection.disconnect(); _selection_changed_connection = desktop->getSelection()->connectChanged( sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged))); _updateWatchers(); _readStyleElement(); _selectRow(); } /* * When a dialog is floating, it is connected to the active desktop. */ void StyleDialog::_handleDesktopChanged(SPDesktop* desktop) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::handleDesktopReplaced()" << std::endl; #endif if (getDesktop() == desktop) { // This will happen after construction of dialog. We've already // set up signals so just return. return; } _selection_changed_connection.disconnect(); _document_replaced_connection.disconnect(); setDesktop( desktop ); _selection_changed_connection = desktop->getSelection()->connectChanged( sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged))); _document_replaced_connection = desktop->connectDocumentReplaced( sigc::mem_fun(this, &StyleDialog::_handleDocumentReplaced)); _updateWatchers(); _readStyleElement(); _selectRow(); } /* * Handle a change in which objects are selected in a document. */ void StyleDialog::_handleSelectionChanged() { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_handleSelectionChanged()" << std::endl; #endif _selectRow(); } /** * @brief StyleDialog::_buttonEventsSelectObjs * @param event * This function detects single or double click on a selector in any row. Clicking * on a selector selects the matching objects on the desktop. A double click will * in addition open the CSS dialog. */ void StyleDialog::_buttonEventsSelectObjs(GdkEventButton* event ) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_buttonEventsSelectObjs" << std::endl; #endif _updating = true; if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { int x = static_cast(event->x); int y = static_cast(event->y); _selectObjects(x, y); //} //else if (event->type == GDK_2BUTTON_PRESS && event->button == 1) { //int x = static_cast(event->x); //int y = static_cast(event->y); //_selectObjects(x, y); _updateCSSPanel(); } _updating = false; } /** * @brief StyleDialog::_selectRow * This function selects the row in treeview corresponding to an object selected * in the drawing. If more than one row matches, the first is chosen. */ void StyleDialog::_selectRow() { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_selectRow: updating: " << (_updating?"true":"false") << std::endl; #endif if (_updating || !getDesktop()) return; // Avoid updating if we have set row via dialog. if (SP_ACTIVE_DESKTOP != getDesktop()) { std::cerr << "StyleDialog::_selectRow: SP_ACTIVE_DESKTOP != getDesktop()" << std::endl; return; } Inkscape::Selection* selection = getDesktop()->getSelection(); if (!selection->isEmpty()) { SPObject *obj = selection->objects().back(); Gtk::TreeModel::Children children = _store->children(); for(Gtk::TreeModel::Children::iterator iter = children.begin(); iter != children.end(); ++iter) { Gtk::TreeModel::Row row = *iter; std::vector objVec = row[_mColumns._colObj]; for (unsigned i = 0; i < objVec.size(); ++i) { if (obj->getId() == objVec[i]->getId()) { _treeView.get_selection()->select(row); _updateCSSPanel(); return; } } } } // Selection empty or no row matches. _treeView.get_selection()->unselect_all(); _updateCSSPanel(); } void StyleDialog::_objChanged() { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_objChanged" << std::endl; #endif if (_updating) return; _updateCSSPanel(); } /** * @brief StyleDialog::_handleProp * @param path * @param new_text * Called when new text is entered into a "prop" cell.. */ void StyleDialog::_handleProp(const Glib::ustring& path, const Glib::ustring& new_text) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_handleProp: path: " << path << " new_text: " << new_text << std::endl; #endif Gtk::TreeModel::iterator iterCss = _cssPane->_treeView.get_model()->get_iter(path); if (iterCss) { Gtk::TreeModel::Row row = *iterCss; row[_cssPane->_cssColumns._propertyLabel] = new_text; } // To do: validate. } /** * @brief StyleDialog::_handleSheet * @param path * @param new_text * Called when new text is entered into a "sheet" cell.. */ void StyleDialog::_handleSheet(const Glib::ustring& path, const Glib::ustring& new_text) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_handleSheet: path: " << path << " new_text: " << new_text << std::endl; #endif Gtk::TreeModel::iterator iterCss = _cssPane->_treeView.get_model()->get_iter(path); if (iterCss) { Gtk::TreeModel::Row row = *iterCss; row[_cssPane->_cssColumns._styleSheetVal] = new_text; } // To do: validate (run through style.read()/style.write()?). Glib::ustring properties; for (auto& crow: _cssPane->_store->children()) { properties = properties + crow[_cssPane->_cssColumns._propertyLabel] + ": " + crow[_cssPane->_cssColumns._styleSheetVal] + "; "; } // Update selector data. Glib::RefPtr refTreeSelection = _treeView.get_selection(); Gtk::TreeModel::iterator iter = refTreeSelection->get_selected(); if (iter) { Gtk::TreeModel::Row row = *iter; row[_mColumns._colProperties] = properties; _writeStyleElement(); } } /** * @brief StyleDialog::_handleAttr * @param path * @param new_text * Called when new text is entered into an "attr" cell.. */ void StyleDialog::_handleAttr(const Glib::ustring& path, const Glib::ustring& new_text) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_handleAttr: path: " << path << " new_text: " << new_text << std::endl; #endif Gtk::TreeModel::iterator iterCss = _cssPane->_treeView.get_model()->get_iter(path); if (iterCss) { Gtk::TreeModel::Row row = *iterCss; row[_cssPane->_cssColumns._styleAttrVal] = new_text; } // To do: validate (run through style.read()/style.write()?). Glib::ustring properties; for (auto& crow: _cssPane->_store->children()) { properties = properties + crow[_cssPane->_cssColumns._propertyLabel] + ": "; crow[_cssPane->_cssColumns._styleAttrVal] + "; "; } std::cout << "StyleDialog::_handlerAttr(): Unimplemented write." << std::endl; } /** * @brief StyleDialog::_delProperty * @param event * @return * Delete a property from the CSS dialog and then update trees. */ bool StyleDialog::_delProperty(GdkEventButton *event) { #ifdef DEBUG_STYLEDIALOG std::cout << "StyleDialog::_delProperty" << std::endl; #endif if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { Gtk::TreeViewColumn *col = nullptr; Gtk::TreeModel::Path path; int x = static_cast(event->x); int y = static_cast(event->y); int x2 = 0; int y2 = 0; Gtk::TreeModel::Row cssRow; Glib::ustring toDelProperty; if (_cssPane->_treeView.get_path_at_pos(x, y, path, col, x2, y2)) { if (col == _cssPane->_treeView.get_column(0)) { Gtk::TreeModel::iterator cssIter = _cssPane->_treeView.get_selection()->get_selected(); if (cssIter) { Gtk::TreeModel::Row cssRow = *cssIter; // Update selector data. Glib::RefPtr refTreeSelection = _treeView.get_selection(); Gtk::TreeModel::iterator iter = refTreeSelection->get_selected(); if (iter) { Gtk::TreeModel::Row row = *iter; if ( row[_mColumns._colIsSelector]) { // We only care about style sheet for selectors so erase row in CSS pane. _cssPane->_store->erase(cssIter); // Update style sheet Glib::ustring properties; for (auto& crow: _cssPane->_store->children()) { Glib::ustring sheetVal = crow[_cssPane->_cssColumns._styleSheetVal]; if (!sheetVal.empty()) { properties = properties + crow[_cssPane->_cssColumns._propertyLabel] + ": " + crow[_cssPane->_cssColumns._styleSheetVal] + "; "; } } row[_mColumns._colProperties] = properties; _writeStyleElement(); } else { // We only erase row if style sheet does not contain deleted property. // Otherwise we set style attr value to empty string. Gtk::TreeModel::Row cssRow = *cssIter; Glib::ustring val = cssRow[_cssPane->_cssColumns._styleSheetVal]; if (val.empty()) { _cssPane->_store->erase(cssIter); } else { cssRow[_cssPane->_cssColumns._styleAttrVal] = Glib::ustring(); } // Update style attribute std::vector objects = row[_mColumns._colObj]; Glib::ustring properties; for (auto& crow: _cssPane->_store->children()) { Glib::ustring attrVal = crow[_cssPane->_cssColumns._styleAttrVal]; if (!attrVal.empty()) { properties = properties + crow[_cssPane->_cssColumns._propertyLabel] + ": " + crow[_cssPane->_cssColumns._styleAttrVal] + "; "; } } if (objects[0]) { if (properties.empty()) { objects[0]->setAttribute("style", nullptr); } else { objects[0]->setAttribute("style", properties); } DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_STYLE, _("Deleted property from style attribute.")); } } } } } } } return false; } /** * @brief StyleDialog::_styleButton * @param btn * @param iconName * @param tooltip * Set the style of '+' and '-' buttons at the bottom of dialog. */ void StyleDialog::_styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip) { GtkWidget *child = gtk_image_new_from_icon_name(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_widget_show(child); btn.add(*manage(Glib::wrap(child))); btn.set_relief(Gtk::RELIEF_NONE); btn.set_tooltip_text (tooltip); } } // namespace Dialog } // namespace UI } // namespace Inkscape /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :