1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /** @file
3  * @brief A dialog for CSS selectors
4  */
5 /* Authors:
6  *   Kamalpreet Kaur Grewal
7  *   Tavmjong Bah
8  *   Jabiertxof
9  *
10  * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com>
11  * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr>
12  *
13  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
14  */
15 
16 #include "selectorsdialog.h"
17 #include "attribute-rel-svg.h"
18 #include "document-undo.h"
19 #include "inkscape.h"
20 #include "selection.h"
21 #include "style.h"
22 #include "ui/icon-loader.h"
23 #include "ui/icon-names.h"
24 #include "ui/widget/iconrenderer.h"
25 #include "verbs.h"
26 
27 #include "xml/attribute-record.h"
28 #include "xml/node-observer.h"
29 #include "xml/sp-css-attr.h"
30 
31 #include <glibmm/i18n.h>
32 #include <glibmm/regex.h>
33 
34 #include <map>
35 #include <regex>
36 #include <utility>
37 
38 // G_MESSAGES_DEBUG=DEBUG_SELECTORSDIALOG  gdb ./inkscape
39 // #define DEBUG_SELECTORSDIALOG
40 // #define G_LOG_DOMAIN "SELECTORSDIALOG"
41 
42 using Inkscape::DocumentUndo;
43 
44 /**
45  * This macro is used to remove spaces around selectors or any strings when
46  * parsing is done to update XML style element or row labels in this dialog.
47  */
48 #define REMOVE_SPACES(x)                                                                                               \
49     x.erase(0, x.find_first_not_of(' '));                                                                              \
50     if (x.size() && x[0] == ',')                                                                                       \
51         x.erase(0, 1);                                                                                                 \
52     if (x.size() && x[x.size() - 1] == ',')                                                                            \
53         x.erase(x.size() - 1, 1);                                                                                      \
54     x.erase(x.find_last_not_of(' ') + 1);
55 
56 namespace Inkscape {
57 namespace UI {
58 namespace Dialog {
59 
60 // Keeps a watch on style element
61 class SelectorsDialog::NodeObserver : public Inkscape::XML::NodeObserver {
62   public:
NodeObserver(SelectorsDialog * selectorsdialog)63     NodeObserver(SelectorsDialog *selectorsdialog)
64         : _selectorsdialog(selectorsdialog)
65     {
66         g_debug("SelectorsDialog::NodeObserver: Constructor");
67     };
68 
69     void notifyContentChanged(Inkscape::XML::Node &node,
70                                       Inkscape::Util::ptr_shared old_content,
71                                       Inkscape::Util::ptr_shared new_content) override;
72 
73     SelectorsDialog *_selectorsdialog;
74 };
75 
76 
notifyContentChanged(Inkscape::XML::Node &,Inkscape::Util::ptr_shared,Inkscape::Util::ptr_shared)77 void SelectorsDialog::NodeObserver::notifyContentChanged(Inkscape::XML::Node & /*node*/,
78                                                          Inkscape::Util::ptr_shared /*old_content*/,
79                                                          Inkscape::Util::ptr_shared /*new_content*/)
80 {
81 
82     g_debug("SelectorsDialog::NodeObserver::notifyContentChanged");
83     _selectorsdialog->_scroollock = true;
84     _selectorsdialog->_updating = false;
85     _selectorsdialog->_readStyleElement();
86     _selectorsdialog->_selectRow();
87 }
88 
89 
90 // Keeps a watch for new/removed/changed nodes
91 // (Must update objects that selectors match.)
92 class SelectorsDialog::NodeWatcher : public Inkscape::XML::NodeObserver {
93   public:
NodeWatcher(SelectorsDialog * selectorsdialog)94     NodeWatcher(SelectorsDialog *selectorsdialog)
95         : _selectorsdialog(selectorsdialog)
96     {
97         g_debug("SelectorsDialog::NodeWatcher: Constructor");
98     };
99 
notifyChildAdded(Inkscape::XML::Node &,Inkscape::XML::Node & child,Inkscape::XML::Node *)100     void notifyChildAdded( Inkscape::XML::Node &/*node*/,
101                                    Inkscape::XML::Node &child,
102                                    Inkscape::XML::Node */*prev*/ ) override
103     {
104             _selectorsdialog->_nodeAdded(child);
105     }
106 
notifyChildRemoved(Inkscape::XML::Node &,Inkscape::XML::Node & child,Inkscape::XML::Node *)107     void notifyChildRemoved( Inkscape::XML::Node &/*node*/,
108                                      Inkscape::XML::Node &child,
109                                      Inkscape::XML::Node */*prev*/ ) override
110     {
111             _selectorsdialog->_nodeRemoved(child);
112     }
113 
notifyAttributeChanged(Inkscape::XML::Node & node,GQuark qname,Util::ptr_shared,Util::ptr_shared)114     void notifyAttributeChanged( Inkscape::XML::Node &node,
115                                          GQuark qname,
116                                          Util::ptr_shared /*old_value*/,
117                                          Util::ptr_shared /*new_value*/ ) override {
118 
119         static GQuark const CODE_id = g_quark_from_static_string("id");
120         static GQuark const CODE_class = g_quark_from_static_string("class");
121 
122         if (qname == CODE_id || qname == CODE_class) {
123             _selectorsdialog->_nodeChanged(node);
124         }
125     }
126 
127     SelectorsDialog *_selectorsdialog;
128 };
129 
_nodeAdded(Inkscape::XML::Node & node)130 void SelectorsDialog::_nodeAdded(Inkscape::XML::Node &node)
131 {
132     _readStyleElement();
133     _selectRow();
134 }
135 
_nodeRemoved(Inkscape::XML::Node & repr)136 void SelectorsDialog::_nodeRemoved(Inkscape::XML::Node &repr)
137 {
138     if (_textNode == &repr) {
139         _textNode = nullptr;
140     }
141 
142     _readStyleElement();
143     _selectRow();
144 }
145 
_nodeChanged(Inkscape::XML::Node & object)146 void SelectorsDialog::_nodeChanged(Inkscape::XML::Node &object)
147 {
148 
149     g_debug("SelectorsDialog::NodeChanged");
150 
151     _scroollock = true;
152 
153     _readStyleElement();
154     _selectRow();
155 }
156 
157 SelectorsDialog::TreeStore::TreeStore() = default;
158 
159 
160 /**
161  * Allow dragging only selectors.
162  */
row_draggable_vfunc(const Gtk::TreeModel::Path & path) const163 bool SelectorsDialog::TreeStore::row_draggable_vfunc(const Gtk::TreeModel::Path &path) const
164 {
165     g_debug("SelectorsDialog::TreeStore::row_draggable_vfunc");
166 
167     auto unconstThis = const_cast<SelectorsDialog::TreeStore *>(this);
168     const_iterator iter = unconstThis->get_iter(path);
169     if (iter) {
170         Gtk::TreeModel::Row row = *iter;
171         bool is_draggable = row[_selectorsdialog->_mColumns._colType] == SELECTOR;
172         return is_draggable;
173     }
174     return Gtk::TreeStore::row_draggable_vfunc(path);
175 }
176 
177 /**
178  * Allow dropping only in between other selectors.
179  */
row_drop_possible_vfunc(const Gtk::TreeModel::Path & dest,const Gtk::SelectionData & selection_data) const180 bool SelectorsDialog::TreeStore::row_drop_possible_vfunc(const Gtk::TreeModel::Path &dest,
181                                                          const Gtk::SelectionData &selection_data) const
182 {
183     g_debug("SelectorsDialog::TreeStore::row_drop_possible_vfunc");
184 
185     Gtk::TreeModel::Path dest_parent = dest;
186     dest_parent.up();
187     return dest_parent.empty();
188 }
189 
190 
191 // This is only here to handle updating style element after a drag and drop.
on_row_deleted(const TreeModel::Path & path)192 void SelectorsDialog::TreeStore::on_row_deleted(const TreeModel::Path &path)
193 {
194     if (_selectorsdialog->_updating)
195         return; // Don't write if we deleted row (other than from DND)
196 
197     g_debug("on_row_deleted");
198     _selectorsdialog->_writeStyleElement();
199     _selectorsdialog->_readStyleElement();
200 }
201 
202 
create(SelectorsDialog * selectorsdialog)203 Glib::RefPtr<SelectorsDialog::TreeStore> SelectorsDialog::TreeStore::create(SelectorsDialog *selectorsdialog)
204 {
205     g_debug("SelectorsDialog::TreeStore::create");
206 
207     SelectorsDialog::TreeStore *store = new SelectorsDialog::TreeStore();
208     store->_selectorsdialog = selectorsdialog;
209     store->set_column_types(store->_selectorsdialog->_mColumns);
210     return Glib::RefPtr<SelectorsDialog::TreeStore>(store);
211 }
212 
213 /**
214  * Constructor
215  * A treeview and a set of two buttons are added to the dialog. _addSelector
216  * adds selectors to treeview. _delSelector deletes the selector from the dialog.
217  * Any addition/deletion of the selectors updates XML style element accordingly.
218  */
SelectorsDialog()219 SelectorsDialog::SelectorsDialog()
220     : DialogBase("/dialogs/selectors", SP_VERB_DIALOG_SELECTORS)
221     , _updating(false)
222     , _textNode(nullptr)
223     , _scroolpos(0)
224     , _scroollock(false)
225 {
226     g_debug("SelectorsDialog::SelectorsDialog");
227 
228     m_nodewatcher.reset(new SelectorsDialog::NodeWatcher(this));
229     m_styletextwatcher.reset(new SelectorsDialog::NodeObserver(this));
230 
231     // Tree
232     Inkscape::UI::Widget::IconRenderer * addRenderer = manage(
233                 new Inkscape::UI::Widget::IconRenderer() );
234     addRenderer->add_icon("edit-delete");
235     addRenderer->add_icon("list-add");
236     addRenderer->add_icon("empty-icon");
237     _store = TreeStore::create(this);
238     _treeView.set_model(_store);
239 
240     // ALWAYS be a single selection widget
241     _treeView.get_selection()->set_mode(Gtk::SELECTION_SINGLE);
242 
243     _treeView.set_headers_visible(false);
244     _treeView.enable_model_drag_source();
245     _treeView.enable_model_drag_dest( Gdk::ACTION_MOVE );
246     int addCol = _treeView.append_column("", *addRenderer) - 1;
247     Gtk::TreeViewColumn *col = _treeView.get_column(addCol);
248     if ( col ) {
249         col->add_attribute(addRenderer->property_icon(), _mColumns._colType);
250     }
251 
252     Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText());
253     addCol = _treeView.append_column("CSS Selector", *label) - 1;
254     col = _treeView.get_column(addCol);
255     if (col) {
256         col->add_attribute(label->property_text(), _mColumns._colSelector);
257         col->add_attribute(label->property_weight(), _mColumns._colSelected);
258     }
259     _treeView.set_expander_column(*(_treeView.get_column(1)));
260 
261 
262     // Signal handlers
263     _treeView.signal_button_release_event().connect( // Needs to be release, not press.
264         sigc::mem_fun(*this, &SelectorsDialog::_handleButtonEvent), false);
265 
266     _treeView.signal_button_release_event().connect_notify(
267         sigc::mem_fun(*this, &SelectorsDialog::_buttonEventsSelectObjs), false);
268 
269     _treeView.signal_row_expanded().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowExpand));
270 
271     _treeView.signal_row_collapsed().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowCollapse));
272 
273     _showWidgets();
274 
275     show_all();
276 }
277 
278 
_vscrool()279 void SelectorsDialog::_vscrool()
280 {
281     if (!_scroollock) {
282         _scroolpos = _vadj->get_value();
283     } else {
284         _vadj->set_value(_scroolpos);
285         _scroollock = false;
286     }
287 }
288 
_showWidgets()289 void SelectorsDialog::_showWidgets()
290 {
291     // Pack widgets
292     g_debug("SelectorsDialog::_showWidgets");
293 
294     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
295     bool dir = prefs->getBool("/dialogs/selectors/vertical", true);
296     _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL);
297     _selectors_box.set_orientation(Gtk::ORIENTATION_VERTICAL);
298     _selectors_box.set_name("SelectorsDialog");
299     _scrolled_window_selectors.add(_treeView);
300     _scrolled_window_selectors.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
301     _vadj = _scrolled_window_selectors.get_vadjustment();
302     _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_vscrool));
303     _selectors_box.pack_start(_scrolled_window_selectors, Gtk::PACK_EXPAND_WIDGET);
304     /* Gtk::Label *dirtogglerlabel = Gtk::manage(new Gtk::Label(_("Paned vertical")));
305     dirtogglerlabel->get_style_context()->add_class("inksmall");
306     _direction.property_active() = dir;
307     _direction.property_active().signal_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection));
308     _direction.get_style_context()->add_class("inkswitch"); */
309     _styleButton(_create, "list-add", "Add a new CSS Selector");
310     _create.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_addSelector));
311     _styleButton(_del, "list-remove", "Remove a CSS Selector");
312     _button_box.pack_start(_create, Gtk::PACK_SHRINK);
313     _button_box.pack_start(_del, Gtk::PACK_SHRINK);
314     Gtk::RadioButton::Group group;
315     Gtk::RadioButton *_horizontal = Gtk::manage(new Gtk::RadioButton());
316     Gtk::RadioButton *_vertical = Gtk::manage(new Gtk::RadioButton());
317     _horizontal->set_image_from_icon_name(INKSCAPE_ICON("horizontal"));
318     _vertical->set_image_from_icon_name(INKSCAPE_ICON("vertical"));
319     _horizontal->set_group(group);
320     _vertical->set_group(group);
321     _vertical->set_active(dir);
322     _vertical->signal_toggled().connect(
323         sigc::bind(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection), _vertical));
324     _horizontal->property_draw_indicator() = false;
325     _vertical->property_draw_indicator() = false;
326     _button_box.pack_end(*_horizontal, false, false, 0);
327     _button_box.pack_end(*_vertical, false, false, 0);
328     _del.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_delSelector));
329     _del.hide();
330     _style_dialog = new StyleDialog;
331     _style_dialog->set_name("StyleDialog");
332     _paned.pack1(*_style_dialog, Gtk::SHRINK);
333     _paned.pack2(_selectors_box, true, true);
334     _paned.set_wide_handle(true);
335     Gtk::Box *contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
336     contents->pack_start(_paned, Gtk::PACK_EXPAND_WIDGET);
337     contents->pack_start(_button_box, false, false, 0);
338     contents->set_valign(Gtk::ALIGN_FILL);
339     contents->child_property_fill(_paned);
340     Gtk::ScrolledWindow *dialog_scroller = new Gtk::ScrolledWindow();
341     dialog_scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
342     dialog_scroller->set_shadow_type(Gtk::SHADOW_IN);
343     dialog_scroller->add(*Gtk::manage(contents));
344     pack_start(*dialog_scroller, Gtk::PACK_EXPAND_WIDGET);
345     show_all();
346     int widthpos = _paned.property_max_position() - _paned.property_min_position();
347     int panedpos = prefs->getInt("/dialogs/selectors/panedpos", widthpos / 2);
348     _paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_childresized));
349     _paned.signal_size_allocate().connect(sigc::mem_fun(*this, &SelectorsDialog::_panedresized));
350     _updating = true;
351     _paned.property_position() = panedpos;
352     _updating = false;
353     set_size_request(320, 260);
354     set_name("SelectorsAndStyleDialog");
355 }
356 
_panedresized(Gtk::Allocation allocation)357 void SelectorsDialog::_panedresized(Gtk::Allocation allocation)
358 {
359     g_debug("SelectorsDialog::_panedresized");
360     _resized();
361 }
362 
_childresized()363 void SelectorsDialog::_childresized()
364 {
365     g_debug("SelectorsDialog::_childresized");
366     _resized();
367 }
368 
_resized()369 void SelectorsDialog::_resized()
370 {
371     g_debug("SelectorsDialog::_resized");
372     _scroollock = true;
373     if (_updating) {
374         return;
375     }
376     _updating = true;
377     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
378     int max = int(_paned.property_max_position() * 0.95);
379     int min = int(_paned.property_max_position() * 0.05);
380     if (_paned.property_position() > max) {
381         _paned.property_position() = max;
382     }
383     if (_paned.property_position() < min) {
384         _paned.property_position() = min;
385     }
386 
387     prefs->setInt("/dialogs/selectors/panedpos", _paned.property_position());
388     _updating = false;
389 }
390 
_toggleDirection(Gtk::RadioButton * vertical)391 void SelectorsDialog::_toggleDirection(Gtk::RadioButton *vertical)
392 {
393     g_debug("SelectorsDialog::_toggleDirection");
394     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
395     bool dir = vertical->get_active();
396     prefs->setBool("/dialogs/selectors/vertical", dir);
397     _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL);
398     _paned.check_resize();
399     int widthpos = _paned.property_max_position() - _paned.property_min_position();
400     prefs->setInt("/dialogs/selectors/panedpos", widthpos / 2);
401     _paned.property_position() = widthpos / 2;
402 }
403 
404 /**
405  * Class destructor
406  */
~SelectorsDialog()407 SelectorsDialog::~SelectorsDialog()
408 {
409     g_debug("SelectorsDialog::~SelectorsDialog");
410 
411     // Detatch watchers to prevent crashes.
412     _updateWatchers(nullptr);
413 }
414 
415 
416 /**
417  * @return Inkscape::XML::Node* pointing to a style element's text node.
418  * Returns the style element's text node. If there is no style element, one is created.
419  * Ditto for text node.
420  */
_getStyleTextNode(bool create_if_missing)421 Inkscape::XML::Node *SelectorsDialog::_getStyleTextNode(bool create_if_missing)
422 {
423     g_debug("SelectorsDialog::_getStyleTextNode");
424 
425     auto textNode = Inkscape::get_first_style_text_node(m_root, create_if_missing);
426 
427     if (_textNode != textNode) {
428         if (_textNode) {
429             _textNode->removeObserver(*m_styletextwatcher);
430         }
431 
432         _textNode = textNode;
433 
434         if (_textNode) {
435             _textNode->addObserver(*m_styletextwatcher);
436         }
437     }
438 
439     return textNode;
440 }
441 
442 /**
443  * Fill the Gtk::TreeStore from the svg:style element.
444  */
_readStyleElement()445 void SelectorsDialog::_readStyleElement()
446 {
447     g_debug("SelectorsDialog::_readStyleElement(): updating %s", (_updating ? "true" : "false"));
448 
449     if (_updating) return; // Don't read if we wrote style element.
450     _updating = true;
451     _scroollock = true;
452     Inkscape::XML::Node * textNode = _getStyleTextNode();
453 
454     // Get content from style text node.
455     std::string content = (textNode && textNode->content()) ? textNode->content() : "";
456 
457     // Remove end-of-lines (check it works on Windoze).
458     content.erase(std::remove(content.begin(), content.end(), '\n'), content.end());
459 
460     // Remove comments (/* xxx */)
461 #if 0
462         while(content.find("/*") != std::string::npos) {
463             size_t start = content.find("/*");
464             content.erase(start, (content.find("*\/", start) - start) +2);
465         }
466 #endif
467 
468     // First split into selector/value chunks.
469     // An attempt to use Glib::Regex failed. A C++11 version worked but
470     // reportedly has problems on Windows. Using split_simple() is simpler
471     // and probably faster.
472     //
473     // Glib::RefPtr<Glib::Regex> regex1 =
474     //   Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}");
475     //
476     // Glib::MatchInfo minfo;
477     // regex1->match(content, minfo);
478 
479     // Split on curly brackets. Even tokens are selectors, odd are values.
480     std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[}{]", content);
481 
482     // If text node is empty, return (avoids problem with negative below).
483     if (tokens.size() == 0) {
484         _store->clear();
485         _updating = false;
486         return;
487     }
488     _treeView.show_all();
489     std::vector<std::pair<Glib::ustring, bool>> expanderstatus;
490     for (unsigned i = 0; i < tokens.size() - 1; i += 2) {
491         Glib::ustring selector = tokens[i];
492         REMOVE_SPACES(selector); // Remove leading/trailing spaces
493         std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector);
494         if (!selectordata.empty()) {
495             selector = selectordata.back();
496         }
497         selector = _style_dialog->fixCSSSelectors(selector);
498         for (auto &row : _store->children()) {
499             Glib::ustring selectorold = row[_mColumns._colSelector];
500             if (selectorold == selector) {
501                 expanderstatus.emplace_back(selector, row[_mColumns._colExpand]);
502             }
503         }
504     }
505     _store->clear();
506     bool rewrite = false;
507 
508 
509     for (unsigned i = 0; i < tokens.size()-1; i += 2) {
510         Glib::ustring selector = tokens[i];
511         REMOVE_SPACES(selector); // Remove leading/trailing spaces
512         std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector);
513         for (auto selectoritem : selectordata) {
514             if (selectordata[selectordata.size() - 1] == selectoritem) {
515                 selector = selectoritem;
516             } else {
517                 Gtk::TreeModel::Row row = *(_store->append());
518                 row[_mColumns._colSelector] = selectoritem + ";";
519                 row[_mColumns._colExpand] = false;
520                 row[_mColumns._colType] = OTHER;
521                 row[_mColumns._colObj] = nullptr;
522                 row[_mColumns._colProperties] = "";
523                 row[_mColumns._colVisible] = true;
524                 row[_mColumns._colSelected] = 400;
525             }
526         }
527         Glib::ustring selector_old = selector;
528         selector = _style_dialog->fixCSSSelectors(selector);
529         if (selector_old != selector) {
530             rewrite = true;
531         }
532 
533         if (selector.empty() || selector == "* > .inkscapehacktmp") {
534             continue;
535         }
536         std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[,]+", selector);
537         coltype colType = SELECTOR;
538 
539         Glib::ustring properties;
540         // Check to make sure we do have a value to match selector.
541         if ((i+1) < tokens.size()) {
542             properties = tokens[i+1];
543         } else {
544             std::cerr << "SelectorsDialog::_readStyleElement(): Missing values "
545                          "for last selector!"
546                       << std::endl;
547         }
548         REMOVE_SPACES(properties);
549         bool colExpand = false;
550         for (auto rowstatus : expanderstatus) {
551             if (selector == rowstatus.first) {
552                 colExpand = rowstatus.second;
553             }
554         }
555         std::vector<Glib::ustring> properties_data = Glib::Regex::split_simple(";", properties);
556         Gtk::TreeModel::Row row = *(_store->append());
557         row[_mColumns._colSelector] = selector;
558         row[_mColumns._colExpand] = colExpand;
559         row[_mColumns._colType] = colType;
560         row[_mColumns._colObj] = nullptr;
561         row[_mColumns._colProperties] = properties;
562         row[_mColumns._colVisible] = true;
563         row[_mColumns._colSelected] = 400;
564         // Add as children, objects that match selector.
565         for (auto &obj : _getObjVec(selector)) {
566             auto *id = obj->getId();
567             if (!id)
568                 continue;
569             Gtk::TreeModel::Row childrow = *(_store->append(row->children()));
570             childrow[_mColumns._colSelector] = "#" + Glib::ustring(id);
571             childrow[_mColumns._colExpand] = false;
572             childrow[_mColumns._colType] = colType == OBJECT;
573             childrow[_mColumns._colObj] = obj;
574             childrow[_mColumns._colProperties] = ""; // Unused
575             childrow[_mColumns._colVisible] = true;  // Unused
576             childrow[_mColumns._colSelected] = 400;
577         }
578     }
579 
580 
581     _updating = false;
582     if (rewrite) {
583         _writeStyleElement();
584     }
585     _scroollock = false;
586     _vadj->set_value(std::min(_scroolpos, _vadj->get_upper()));
587 }
588 
_rowExpand(const Gtk::TreeModel::iterator & iter,const Gtk::TreeModel::Path & path)589 void SelectorsDialog::_rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path)
590 {
591     g_debug("SelectorsDialog::_row_expand()");
592     Gtk::TreeModel::Row row = *iter;
593     row[_mColumns._colExpand] = true;
594 }
595 
_rowCollapse(const Gtk::TreeModel::iterator & iter,const Gtk::TreeModel::Path & path)596 void SelectorsDialog::_rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path)
597 {
598     g_debug("SelectorsDialog::_row_collapse()");
599     Gtk::TreeModel::Row row = *iter;
600     row[_mColumns._colExpand] = false;
601 }
602 /**
603  * Update the content of the style element as selectors (or objects) are added/removed.
604  */
_writeStyleElement()605 void SelectorsDialog::_writeStyleElement()
606 {
607 
608     if (_updating) {
609         return;
610     }
611 
612     g_debug("SelectorsDialog::_writeStyleElement");
613 
614     _scroollock = true;
615     _updating = true;
616     Glib::ustring styleContent = "";
617     for (auto& row: _store->children()) {
618         Glib::ustring selector = row[_mColumns._colSelector];
619 #if 0
620                 REMOVE_SPACES(selector);
621                 size_t len = selector.size();
622                 if(selector[len-1] == ','){
623                     selector.erase(len-1);
624                 }
625                 row[_mColumns._colSelector] =  selector;
626 #endif
627         if (row[_mColumns._colType] == OTHER) {
628             styleContent = selector + styleContent;
629         } else {
630             styleContent = styleContent + selector + " { " + row[_mColumns._colProperties] + " }\n";
631         }
632     }
633     // We could test if styleContent is empty and then delete the style node here but there is no
634     // harm in keeping it around ...
635     Inkscape::XML::Node *textNode = _getStyleTextNode(true);
636     bool empty = false;
637     if (styleContent.empty()) {
638         empty = true;
639         styleContent = "* > .inkscapehacktmp{}";
640     }
641     textNode->setContent(styleContent.c_str());
642     if (empty) {
643         styleContent = "";
644         textNode->setContent(styleContent.c_str());
645     }
646     textNode->setContent(styleContent.c_str());
647     DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_SELECTORS, _("Edited style element."));
648 
649     _updating = false;
650     _scroollock = false;
651     _vadj->set_value(std::min(_scroolpos, _vadj->get_upper()));
652     g_debug("SelectorsDialog::_writeStyleElement(): | %s |", styleContent.c_str());
653 }
654 
655 /**
656  * Update the watchers on objects.
657  */
_updateWatchers(SPDesktop * desktop)658 void SelectorsDialog::_updateWatchers(SPDesktop *desktop)
659 {
660     g_debug("SelectorsDialog::_updateWatchers");
661 
662     if (_textNode) {
663         _textNode->removeObserver(*m_styletextwatcher);
664         _textNode = nullptr;
665     }
666 
667     if (m_root) {
668         m_root->removeSubtreeObserver(*m_nodewatcher);
669         m_root = nullptr;
670     }
671 
672     if (desktop) {
673         m_root = desktop->getDocument()->getReprRoot();
674         m_root->addSubtreeObserver(*m_nodewatcher);
675     }
676 }
677 /*
678 void sp_get_selector_active(Glib::ustring &selector)
679 {
680     std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[ ]+", selector);
681     selector = tokensplus[tokensplus.size() - 1];
682     // Erase any comma/space
683     REMOVE_SPACES(selector);
684     Glib::ustring toadd = Glib::ustring(selector);
685     Glib::ustring toparse = Glib::ustring(selector);
686     Glib::ustring tag = "";
687     if (toadd[0] != '.' || toadd[0] != '#') {
688         auto i = std::min(toadd.find("#"), toadd.find("."));
689         tag = toadd.substr(0,i-1);
690         toparse.erase(0, i-1);
691     }
692     auto i = toparse.find("#");
693     toparse.erase(i, 1);
694     auto j = toparse.find("#");
695     if (j == std::string::npos) {
696         selector = "";
697     } else if (i != std::string::npos) {
698         Glib::ustring post = toadd.substr(0,i-1);
699         Glib::ustring pre = toadd.substr(i, (toadd.size()-1)-i);
700         selector = tag + pre + post;
701     }
702 } */
703 
sp_get_selector_classes(Glib::ustring selector)704 Glib::ustring sp_get_selector_classes(Glib::ustring selector) //, SelectorType selectortype,  Glib::ustring id = "")
705 {
706     g_debug("SelectorsDialog::sp_get_selector_classes");
707 
708     std::pair<Glib::ustring, Glib::ustring> result;
709     std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[ ]+", selector);
710     selector = tokensplus[tokensplus.size() - 1];
711     // Erase any comma/space
712     REMOVE_SPACES(selector);
713     Glib::ustring toparse = Glib::ustring(selector);
714     selector = Glib::ustring("");
715     auto i = toparse.find(".");
716     if (i == std::string::npos) {
717         return "";
718     }
719     if (toparse[0] != '.' && toparse[0] != '#') {
720         i = std::min(toparse.find("#"), toparse.find("."));
721         Glib::ustring tag = toparse.substr(0, i);
722         if (!SPAttributeRelSVG::isSVGElement(tag)) {
723             return selector;
724         }
725         if (i != std::string::npos) {
726             toparse.erase(0, i);
727         }
728     }
729     i = toparse.find("#");
730     if (i != std::string::npos) {
731         toparse.erase(i, 1);
732     }
733     auto j = toparse.find("#");
734     if (j != std::string::npos) {
735         return selector;
736     }
737     if (i != std::string::npos) {
738         toparse.insert(i, "#");
739         if (i) {
740             Glib::ustring post = toparse.substr(0, i);
741             Glib::ustring pre = toparse.substr(i, toparse.size() - i);
742             toparse = pre + post;
743         }
744         auto k = toparse.find(".");
745         if (k != std::string::npos) {
746             toparse = toparse.substr(k, toparse.size() - k);
747         }
748     }
749     return toparse;
750 }
751 
752 /**
753  * @param row
754  * Add selected objects on the desktop to the selector corresponding to 'row'.
755  */
_addToSelector(Gtk::TreeModel::Row row)756 void SelectorsDialog::_addToSelector(Gtk::TreeModel::Row row)
757 {
758     g_debug("SelectorsDialog::_addToSelector: Entrance");
759     if (*row) {
760         // Store list of selected elements on desktop (not to be confused with selector).
761         _updating = true;
762         if (row[_mColumns._colType] == OTHER) {
763             return;
764         }
765         Inkscape::Selection *selection = getDesktop()->getSelection();
766         std::vector<SPObject *> toAddObjVec(selection->objects().begin(), selection->objects().end());
767         Glib::ustring multiselector = row[_mColumns._colSelector];
768         row[_mColumns._colExpand] = true;
769         std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", multiselector);
770         for (auto &obj : toAddObjVec) {
771             auto *id = obj->getId();
772             if (!id)
773                 continue;
774             for (auto tok : tokens) {
775                 Glib::ustring clases = sp_get_selector_classes(tok);
776                 if (!clases.empty()) {
777                     _insertClass(obj, clases);
778                     std::vector<SPObject *> currentobjs = _getObjVec(multiselector);
779                     bool removeclass = true;
780                     for (auto currentobj : currentobjs) {
781                         if (g_strcmp0(currentobj->getId(), id) == 0) {
782                             removeclass = false;
783                         }
784                     }
785                     if (removeclass) {
786                         _removeClass(obj, clases);
787                     }
788                 }
789             }
790             std::vector<SPObject *> currentobjs = _getObjVec(multiselector);
791             bool insertid = true;
792             for (auto currentobj : currentobjs) {
793                 if (g_strcmp0(currentobj->getId(), id) == 0) {
794                     insertid = false;
795                 }
796             }
797             if (insertid) {
798                 multiselector = multiselector + ",#" + id;
799             }
800             Gtk::TreeModel::Row childrow = *(_store->prepend(row->children()));
801             childrow[_mColumns._colSelector] = "#" + Glib::ustring(id);
802             childrow[_mColumns._colExpand] = false;
803             childrow[_mColumns._colType] = OBJECT;
804             childrow[_mColumns._colObj] = obj;
805             childrow[_mColumns._colProperties] = ""; // Unused
806             childrow[_mColumns._colVisible] = true;  // Unused
807             childrow[_mColumns._colSelected] = 400;
808         }
809         row[_mColumns._colSelector] = multiselector;
810         _updating = false;
811 
812         // Add entry to style element
813         for (auto &obj : toAddObjVec) {
814             Glib::ustring css_str = "";
815             SPCSSAttr *css = sp_repr_css_attr_new();
816             SPCSSAttr *css_selector = sp_repr_css_attr_new();
817             sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style"));
818             Glib::ustring selprops = row[_mColumns._colProperties];
819             sp_repr_css_attr_add_from_string(css_selector, selprops.c_str());
820             for (const auto & iter : css_selector->attributeList()) {
821                 gchar const *key = g_quark_to_string(iter.key);
822                 css->removeAttribute(key);
823             }
824             sp_repr_css_write_string(css, css_str);
825             sp_repr_css_attr_unref(css);
826             sp_repr_css_attr_unref(css_selector);
827             obj->getRepr()->setAttribute("style", css_str);
828             obj->style->readFromObject(obj);
829             obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
830         }
831         _writeStyleElement();
832     }
833 }
834 
835 /**
836  * @param row
837  * Remove the object corresponding to 'row' from the parent selector.
838  */
_removeFromSelector(Gtk::TreeModel::Row row)839 void SelectorsDialog::_removeFromSelector(Gtk::TreeModel::Row row)
840 {
841     g_debug("SelectorsDialog::_removeFromSelector: Entrance");
842     if (*row) {
843         _scroollock = true;
844         _updating = true;
845         SPObject *obj = nullptr;
846         Glib::ustring objectLabel = row[_mColumns._colSelector];
847         Gtk::TreeModel::iterator iter = row->parent();
848         if (iter) {
849             Gtk::TreeModel::Row parent = *iter;
850             Glib::ustring multiselector = parent[_mColumns._colSelector];
851             REMOVE_SPACES(multiselector);
852             obj = _getObjVec(objectLabel)[0];
853             std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", multiselector);
854             Glib::ustring selector = "";
855             for (auto tok : tokens) {
856                 if (tok.empty()) {
857                     continue;
858                 }
859                 // TODO: handle when other selectors has the removed class applied to maybe not remove
860                 Glib::ustring clases = sp_get_selector_classes(tok);
861                 if (!clases.empty()) {
862                     _removeClass(obj, tok, true);
863                 }
864                 auto i = tok.find(row[_mColumns._colSelector]);
865                 if (i == std::string::npos) {
866                     selector = selector.empty() ? tok : selector + "," + tok;
867                 }
868             }
869             REMOVE_SPACES(selector);
870             if (selector.empty()) {
871                 _store->erase(parent);
872 
873             } else {
874                 _store->erase(row);
875                 parent[_mColumns._colSelector] = selector;
876                 parent[_mColumns._colExpand] = true;
877                 parent[_mColumns._colObj] = nullptr;
878             }
879         }
880         _updating = false;
881 
882         // Add entry to style element
883         _writeStyleElement();
884         obj->style->readFromObject(obj);
885         obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
886         _scroollock = false;
887         _vadj->set_value(std::min(_scroolpos, _vadj->get_upper()));
888     }
889 }
890 
891 
892 /**
893  * @param sel
894  * @return This function returns a comma separated list of ids for objects in input vector.
895  * It is used in creating an 'id' selector. It relies on objects having 'id's.
896  */
_getIdList(std::vector<SPObject * > sel)897 Glib::ustring SelectorsDialog::_getIdList(std::vector<SPObject *> sel)
898 {
899     g_debug("SelectorsDialog::_getIdList");
900 
901     Glib::ustring str;
902     for (auto& obj: sel) {
903         char const *id = obj->getId();
904         if (id) {
905             if (!str.empty()) {
906                 str.append(", ");
907             }
908             str.append("#").append(id);
909         }
910     }
911     return str;
912 }
913 
914 /**
915  * @param selector: a valid CSS selector string.
916  * @return objVec: a vector of pointers to SPObject's the selector matches.
917  * Return a vector of all objects that selector matches.
918  */
_getObjVec(Glib::ustring selector)919 std::vector<SPObject *> SelectorsDialog::_getObjVec(Glib::ustring selector)
920 {
921 
922     g_debug("SelectorsDialog::_getObjVec: | %s |", selector.c_str());
923 
924     g_assert(selector.find(";") == Glib::ustring::npos);
925 
926     return getDesktop()->getDocument()->getObjectsBySelector(selector);
927 }
928 
929 
930 /**
931  * @param objs: list of objects to insert class
932  * @param class: class to insert
933  * Insert a class name into objects' 'class' attribute.
934  */
_insertClass(const std::vector<SPObject * > & objVec,const Glib::ustring & className)935 void SelectorsDialog::_insertClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className)
936 {
937     g_debug("SelectorsDialog::_insertClass");
938 
939     for (auto& obj: objVec) {
940         _insertClass(obj, className);
941     }
942 }
943 
944 /**
945  * @param objs: list of objects to insert class
946  * @param class: class to insert
947  * Insert a class name into objects' 'class' attribute.
948  */
_insertClass(SPObject * obj,const Glib::ustring & className)949 void SelectorsDialog::_insertClass(SPObject *obj, const Glib::ustring &className)
950 {
951     g_debug("SelectorsDialog::_insertClass");
952 
953     Glib::ustring classAttr = Glib::ustring("");
954     if (obj->getRepr()->attribute("class")) {
955         classAttr = obj->getRepr()->attribute("class");
956     }
957     std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[.]+", className);
958     std::sort(tokens.begin(), tokens.end());
959     tokens.erase(std::unique(tokens.begin(), tokens.end()), tokens.end());
960     std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[\\s]+", classAttr);
961     for (auto tok : tokens) {
962         bool exist = false;
963         for (auto &tokenplus : tokensplus) {
964             if (tokenplus == tok) {
965                 exist = true;
966             }
967         }
968         if (!exist) {
969             classAttr = classAttr.empty() ? tok : classAttr + " " + tok;
970         }
971     }
972     obj->getRepr()->setAttribute("class", classAttr);
973 }
974 
975 /**
976  * @param objs: list of objects to insert class
977  * @param class: class to insert
978  * Insert a class name into objects' 'class' attribute.
979  */
_removeClass(const std::vector<SPObject * > & objVec,const Glib::ustring & className,bool all)980 void SelectorsDialog::_removeClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className, bool all)
981 {
982     g_debug("SelectorsDialog::_removeClass");
983 
984     for (auto &obj : objVec) {
985         _removeClass(obj, className, all);
986     }
987 }
988 
989 /**
990  * @param objs: list of objects to insert class
991  * @param class: class to insert
992  * Insert a class name into objects' 'class' attribute.
993  */
_removeClass(SPObject * obj,const Glib::ustring & className,bool all)994 void SelectorsDialog::_removeClass(SPObject *obj, const Glib::ustring &className, bool all) // without "."
995 {
996     g_debug("SelectorsDialog::_removeClass");
997 
998     if (obj->getRepr()->attribute("class")) {
999         std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[.]+", className);
1000         Glib::ustring classAttr = obj->getRepr()->attribute("class");
1001         Glib::ustring classAttrRestore = classAttr;
1002         bool notfound = false;
1003         for (auto tok : tokens) {
1004             auto i = classAttr.find(tok);
1005             if (i != std::string::npos) {
1006                 classAttr.erase(i, tok.length());
1007             } else {
1008                 notfound = true;
1009             }
1010         }
1011         if (all && notfound) {
1012             classAttr = classAttrRestore;
1013         }
1014         REMOVE_SPACES(classAttr);
1015         if (classAttr.empty()) {
1016             obj->getRepr()->removeAttribute("class");
1017         } else {
1018             obj->getRepr()->setAttribute("class", classAttr);
1019         }
1020     }
1021 }
1022 
1023 
1024 /**
1025  * @param eventX
1026  * @param eventY
1027  * This function selects objects in the drawing corresponding to the selector
1028  * selected in the treeview.
1029  */
_selectObjects(int eventX,int eventY)1030 void SelectorsDialog::_selectObjects(int eventX, int eventY)
1031 {
1032     g_debug("SelectorsDialog::_selectObjects: %d, %d", eventX, eventY);
1033     Gtk::TreeViewColumn *col = _treeView.get_column(1);
1034     Gtk::TreeModel::Path path;
1035     int x2 = 0;
1036     int y2 = 0;
1037     // To do: We should be able to do this via passing in row.
1038     if (_treeView.get_path_at_pos(eventX, eventY, path, col, x2, y2)) {
1039         if (_lastpath.size() && _lastpath == path) {
1040             return;
1041         }
1042         if (col == _treeView.get_column(1) && x2 > 25) {
1043             getDesktop()->selection->clear();
1044             Gtk::TreeModel::iterator iter = _store->get_iter(path);
1045             if (iter) {
1046                 Gtk::TreeModel::Row row = *iter;
1047                 if (row[_mColumns._colObj]) {
1048                     getDesktop()->selection->add(row[_mColumns._colObj]);
1049                 }
1050                 Gtk::TreeModel::Children children = row.children();
1051                 if (children.empty() || children.size() == 1) {
1052                     _del.show();
1053                 }
1054                 for (auto child : row.children()) {
1055                     Gtk::TreeModel::Row child_row = *child;
1056                     if (child[_mColumns._colObj]) {
1057                         getDesktop()->selection->add(child[_mColumns._colObj]);
1058                     }
1059                 }
1060             }
1061             _lastpath = path;
1062         }
1063     }
1064 }
1065 
1066 /**
1067  * This function opens a dialog to add a selector. The dialog is prefilled
1068  * with an 'id' selector containing a list of the id's of selected objects
1069  * or with a 'class' selector if no objects are selected.
1070  */
_addSelector()1071 void SelectorsDialog::_addSelector()
1072 {
1073     g_debug("SelectorsDialog::_addSelector: Entrance");
1074     _scroollock = true;
1075     // Store list of selected elements on desktop (not to be confused with selector).
1076     Inkscape::Selection* selection = getDesktop()->getSelection();
1077     std::vector<SPObject *> objVec( selection->objects().begin(),
1078                                     selection->objects().end() );
1079 
1080     // ==== Create popup dialog ====
1081     Gtk::Dialog *textDialogPtr =  new Gtk::Dialog();
1082     textDialogPtr->property_modal() = true;
1083     textDialogPtr->property_title() = _("CSS selector");
1084     textDialogPtr->property_window_position() = Gtk::WIN_POS_CENTER_ON_PARENT;
1085     textDialogPtr->add_button(_("Cancel"), Gtk::RESPONSE_CANCEL);
1086     textDialogPtr->add_button(_("Add"),    Gtk::RESPONSE_OK);
1087 
1088     Gtk::Entry *textEditPtr = manage ( new Gtk::Entry() );
1089     textEditPtr->signal_activate().connect(
1090         sigc::bind<Gtk::Dialog *>(sigc::mem_fun(*this, &SelectorsDialog::_closeDialog), textDialogPtr));
1091     textDialogPtr->get_content_area()->pack_start(*textEditPtr, Gtk::PACK_SHRINK);
1092 
1093     Gtk::Label *textLabelPtr = manage(new Gtk::Label(_("Invalid CSS selector.")));
1094     textDialogPtr->get_content_area()->pack_start(*textLabelPtr, Gtk::PACK_SHRINK);
1095 
1096     /**
1097      * By default, the entrybox contains 'Class1' as text. However, if object(s)
1098      * is(are) selected and user clicks '+' at the bottom of dialog, the
1099      * entrybox will have the id(s) of the selected objects as text.
1100      */
1101     if (getDesktop()->getSelection()->isEmpty()) {
1102         textEditPtr->set_text(".Class1");
1103     } else {
1104         textEditPtr->set_text(_getIdList(objVec));
1105     }
1106 
1107     Gtk::Requisition sreq1, sreq2;
1108     textDialogPtr->get_preferred_size(sreq1, sreq2);
1109     int minWidth = 200;
1110     int minHeight = 100;
1111     minWidth  = (sreq2.width  > minWidth  ? sreq2.width  : minWidth );
1112     minHeight = (sreq2.height > minHeight ? sreq2.height : minHeight);
1113     textDialogPtr->set_size_request(minWidth, minHeight);
1114     textEditPtr->show();
1115     textLabelPtr->hide();
1116     textDialogPtr->show();
1117 
1118 
1119     // ==== Get response ====
1120     int result = -1;
1121     bool invalid = true;
1122     Glib::ustring selectorValue;
1123     Glib::ustring originalValue;
1124     while (invalid) {
1125         result = textDialogPtr->run();
1126         if (result != Gtk::RESPONSE_OK) { // Cancel, close dialog, etc.
1127             textDialogPtr->hide();
1128             delete textDialogPtr;
1129             return;
1130         }
1131         /**
1132          * @brief selectorName
1133          * This string stores selector name. The text from entrybox is saved as name
1134          * for selector. If the entrybox is empty, the text (thus selectorName) is
1135          * set to ".Class1"
1136          */
1137         originalValue = Glib::ustring(textEditPtr->get_text());
1138         selectorValue = _style_dialog->fixCSSSelectors(originalValue);
1139         _del.show();
1140         if (originalValue.find("@import ") == std::string::npos && selectorValue.empty()) {
1141             textLabelPtr->show();
1142         } else {
1143             invalid = false;
1144         }
1145     }
1146     delete textDialogPtr;
1147     // ==== Handle response ====
1148     // If class selector, add selector name to class attribute for each object
1149     REMOVE_SPACES(selectorValue);
1150     if (originalValue.find("@import ") != std::string::npos) {
1151         Gtk::TreeModel::Row row = *(_store->prepend());
1152         row[_mColumns._colSelector] = originalValue;
1153         row[_mColumns._colExpand] = false;
1154         row[_mColumns._colType] = OTHER;
1155         row[_mColumns._colObj] = nullptr;
1156         row[_mColumns._colProperties] = "";
1157         row[_mColumns._colVisible] = true;
1158         row[_mColumns._colSelected] = 400;
1159     } else {
1160         std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", selectorValue);
1161         for (auto &obj : objVec) {
1162             for (auto tok : tokens) {
1163                 Glib::ustring clases = sp_get_selector_classes(tok);
1164                 if (clases.empty()) {
1165                     continue;
1166                 }
1167                 _insertClass(obj, clases);
1168                 std::vector<SPObject *> currentobjs = _getObjVec(selectorValue);
1169                 bool removeclass = true;
1170                 for (auto currentobj : currentobjs) {
1171                     if (currentobj == obj) {
1172                         removeclass = false;
1173                     }
1174                 }
1175                 if (removeclass) {
1176                     _removeClass(obj, clases);
1177                 }
1178             }
1179         }
1180         Gtk::TreeModel::Row row = *(_store->prepend());
1181         row[_mColumns._colExpand] = true;
1182         row[_mColumns._colType] = SELECTOR;
1183         row[_mColumns._colSelector] = selectorValue;
1184         row[_mColumns._colObj] = nullptr;
1185         row[_mColumns._colProperties] = "";
1186         row[_mColumns._colVisible] = true;
1187         row[_mColumns._colSelected] = 400;
1188         for (auto &obj : _getObjVec(selectorValue)) {
1189             auto *id = obj->getId();
1190             if (!id)
1191                 continue;
1192             Gtk::TreeModel::Row childrow = *(_store->prepend(row->children()));
1193             childrow[_mColumns._colSelector] = "#" + Glib::ustring(id);
1194             childrow[_mColumns._colExpand] = false;
1195             childrow[_mColumns._colType] = OBJECT;
1196             childrow[_mColumns._colObj] = obj;
1197             childrow[_mColumns._colProperties] = ""; // Unused
1198             childrow[_mColumns._colVisible] = true;  // Unused
1199             childrow[_mColumns._colSelected] = 400;
1200         }
1201     }
1202     // Add entry to style element
1203     _writeStyleElement();
1204     _scroollock = false;
1205     _vadj->set_value(std::min(_scroolpos, _vadj->get_upper()));
1206 }
1207 
_closeDialog(Gtk::Dialog * textDialogPtr)1208 void SelectorsDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); }
1209 
1210 /**
1211  * This function deletes selector when '-' at the bottom is clicked.
1212  * Note: If deleting a class selector, class attributes are NOT changed.
1213  */
_delSelector()1214 void SelectorsDialog::_delSelector()
1215 {
1216     g_debug("SelectorsDialog::_delSelector");
1217 
1218     _scroollock = true;
1219     Glib::RefPtr<Gtk::TreeSelection> refTreeSelection = _treeView.get_selection();
1220     Gtk::TreeModel::iterator iter = refTreeSelection->get_selected();
1221     if (iter) {
1222         _vscrool();
1223         Gtk::TreeModel::Row row = *iter;
1224         if (row.children().size() > 2) {
1225             return;
1226         }
1227         _updating = true;
1228         _store->erase(iter);
1229         _updating = false;
1230         _writeStyleElement();
1231         _del.hide();
1232         _scroollock = false;
1233         _vadj->set_value(std::min(_scroolpos, _vadj->get_upper()));
1234     }
1235 }
1236 
1237 /**
1238  * @param event
1239  * @return
1240  * Handles the event when '+' button in front of a selector name is clicked or when a '-' button in
1241  * front of a child object is clicked. In the first case, the selected objects on the desktop (if
1242  * any) are added as children of the selector in the treeview. In the latter case, the object
1243  * corresponding to the row is removed from the selector.
1244  */
_handleButtonEvent(GdkEventButton * event)1245 bool SelectorsDialog::_handleButtonEvent(GdkEventButton *event)
1246 {
1247     g_debug("SelectorsDialog::_handleButtonEvent: Entrance");
1248     if (event->type == GDK_BUTTON_RELEASE && event->button == 1) {
1249         _scroollock = true;
1250         Gtk::TreeViewColumn *col = nullptr;
1251         Gtk::TreeModel::Path path;
1252         int x = static_cast<int>(event->x);
1253         int y = static_cast<int>(event->y);
1254         int x2 = 0;
1255         int y2 = 0;
1256 
1257         if (_treeView.get_path_at_pos(x, y, path, col, x2, y2)) {
1258             if (col == _treeView.get_column(0)) {
1259                 _vscrool();
1260                 Gtk::TreeModel::iterator iter = _store->get_iter(path);
1261                 Gtk::TreeModel::Row row = *iter;
1262                 if (!row.parent()) {
1263                     _addToSelector(row);
1264                 } else {
1265                     _removeFromSelector(row);
1266                 }
1267                 _vadj->set_value(std::min(_scroolpos, _vadj->get_upper()));
1268             }
1269         }
1270     }
1271     return false;
1272 }
1273 
1274 // -------------------------------------------------------------------
1275 
1276 class PropertyData
1277 {
1278 public:
1279     PropertyData() = default;;
PropertyData(Glib::ustring name)1280     PropertyData(Glib::ustring name) : _name(std::move(name)) {};
1281 
_setSheetValue(Glib::ustring value)1282     void _setSheetValue(Glib::ustring value) { _sheetValue = value; };
_setAttrValue(Glib::ustring value)1283     void _setAttrValue(Glib::ustring value)  { _attrValue  = value; };
_getName()1284     Glib::ustring _getName()       { return _name;       };
_getSheetValue()1285     Glib::ustring _getSheetValue() { return _sheetValue; };
_getAttrValue()1286     Glib::ustring _getAttrValue()  { return _attrValue;  };
1287 
1288 private:
1289     Glib::ustring _name;
1290     Glib::ustring _sheetValue;
1291     Glib::ustring _attrValue;
1292 };
1293 
1294 // -------------------------------------------------------------------
1295 
1296 /*
1297  * When a dialog is floating, it is connected to the active desktop.
1298  */
update()1299 void SelectorsDialog::update()
1300 {
1301     if (!_app) {
1302         std::cerr << "SelectorsDialog::update(): _app is null" << std::endl;
1303         return;
1304     }
1305 
1306     SPDesktop *desktop = getDesktop();
1307 
1308     _updateWatchers(desktop);
1309 
1310     if (!desktop)
1311         return;
1312 
1313     _style_dialog->update();
1314 
1315     _handleSelectionChanged();
1316     _selectRow();
1317 }
1318 
1319 /*
1320  * Handle a change in which objects are selected in a document.
1321  */
_handleSelectionChanged()1322 void SelectorsDialog::_handleSelectionChanged()
1323 {
1324     g_debug("SelectorsDialog::_handleSelectionChanged()");
1325     _lastpath.clear();
1326     _readStyleElement();
1327     _selectRow();
1328 }
1329 
1330 
1331 /**
1332  * @param event
1333  * This function detects single or double click on a selector in any row. Clicking
1334  * on a selector selects the matching objects on the desktop. A double click will
1335  * in addition open the CSS dialog.
1336  */
_buttonEventsSelectObjs(GdkEventButton * event)1337 void SelectorsDialog::_buttonEventsSelectObjs(GdkEventButton *event)
1338 {
1339     g_debug("SelectorsDialog::_buttonEventsSelectObjs");
1340     if (event->type == GDK_BUTTON_RELEASE && event->button == 1) {
1341         _updating = true;
1342         _del.show();
1343         int x = static_cast<int>(event->x);
1344         int y = static_cast<int>(event->y);
1345         _selectObjects(x, y);
1346         _updating = false;
1347         _selectRow();
1348     }
1349 }
1350 
1351 
1352 /**
1353  * This function selects the row in treeview corresponding to an object selected
1354  * in the drawing. If more than one row matches, the first is chosen.
1355  */
_selectRow()1356 void SelectorsDialog::_selectRow()
1357 {
1358     _scroollock = true;
1359     g_debug("SelectorsDialog::_selectRow: updating: %s", (_updating ? "true" : "false"));
1360     _del.hide();
1361     std::vector<Gtk::TreeModel::Path> selectedrows = _treeView.get_selection()->get_selected_rows();
1362     if (selectedrows.size() == 1) {
1363         Gtk::TreeModel::Row row = *_store->get_iter(selectedrows[0]);
1364         if (!row->parent() && row->children().size() < 2) {
1365             _del.show();
1366         }
1367         if (row) {
1368             _style_dialog->setCurrentSelector(row[_mColumns._colSelector]);
1369         }
1370     } else if (selectedrows.size() == 0) {
1371         _del.show();
1372     }
1373     if (_updating || !getDesktop()) return; // Avoid updating if we have set row via dialog.
1374 
1375     Gtk::TreeModel::Children children = _store->children();
1376     Inkscape::Selection* selection = getDesktop()->getSelection();
1377     SPObject *obj = nullptr;
1378     if (!selection->isEmpty()) {
1379         obj = selection->objects().back();
1380     } else {
1381         _style_dialog->setCurrentSelector("");
1382     }
1383     for (auto row : children) {
1384         row[_mColumns._colSelected] = 400;
1385         Gtk::TreeModel::Children subchildren = row->children();
1386         for (auto subrow : subchildren) {
1387             subrow[_mColumns._colSelected] = 400;
1388         }
1389     }
1390 
1391     // Sort selection for matching.
1392     std::vector<SPObject *> selected_objs(
1393         selection->objects().begin(), selection->objects().end());
1394     std::sort(selected_objs.begin(), selected_objs.end());
1395 
1396     for (auto row : children) {
1397         // Recalculate the selector, in real time.
1398         auto row_children = _getObjVec(row[_mColumns._colSelector]);
1399         std::sort(row_children.begin(), row_children.end());
1400 
1401         // If all selected objects are in the css-selector, select it.
1402         if (row_children == selected_objs) {
1403             row[_mColumns._colSelected] = 700;
1404         }
1405 
1406         Gtk::TreeModel::Children subchildren = row->children();
1407 
1408         for (auto subrow : subchildren) {
1409             if (subrow[_mColumns._colObj] && selection->includes(subrow[_mColumns._colObj])) {
1410                 subrow[_mColumns._colSelected] = 700;
1411             }
1412             if (row[_mColumns._colExpand]) {
1413                 _treeView.expand_to_path(Gtk::TreePath(row));
1414             }
1415         }
1416         if (row[_mColumns._colExpand]) {
1417             _treeView.expand_to_path(Gtk::TreePath(row));
1418         }
1419     }
1420     _vadj->set_value(std::min(_scroolpos, _vadj->get_upper()));
1421 }
1422 
1423 /**
1424  * @param btn
1425  * @param iconName
1426  * @param tooltip
1427  * Set the style of '+' and '-' buttons at the bottom of dialog.
1428  */
_styleButton(Gtk::Button & btn,char const * iconName,char const * tooltip)1429 void SelectorsDialog::_styleButton(Gtk::Button &btn, char const *iconName, char const *tooltip)
1430 {
1431     g_debug("SelectorsDialog::_styleButton");
1432 
1433     GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR);
1434     gtk_widget_show(child);
1435     btn.add(*manage(Glib::wrap(child)));
1436     btn.set_relief(Gtk::RELIEF_NONE);
1437     btn.set_tooltip_text (tooltip);
1438 }
1439 
1440 
1441 } // namespace Dialog
1442 } // namespace UI
1443 } // namespace Inkscape
1444 
1445 /*
1446   Local Variables:
1447   mode:c++
1448   c-file-style:"stroustrup"
1449   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1450   indent-tabs-mode:nil
1451   fill-column:99
1452   End:
1453 */
1454 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
1455