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