Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
selectorsdialog.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
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
18#include <algorithm>
19#include <glibmm/i18n.h>
20#include <glibmm/regex.h>
21#include <gtkmm/adjustment.h>
22#include <gtkmm/dialog.h>
23#include <gtkmm/entry.h>
24#include <gtkmm/gestureclick.h>
25#include <gtkmm/label.h>
26#include <gtkmm/togglebutton.h>
27#include <gtkmm/treemodelfilter.h>
28#include <map>
29#include <string>
30#include <utility>
31
32#include "attribute-rel-svg.h"
34#include "desktop.h"
35#include "document-undo.h"
36#include "document.h"
37#include "inkscape.h"
38#include "preferences.h"
39#include "selection.h"
40#include "style.h"
41#include "ui/dialog-run.h"
43#include "ui/icon-names.h"
44#include "ui/pack.h"
46#include "util/trim.h"
48#include "xml/sp-css-attr.h"
49
50// G_MESSAGES_DEBUG=DEBUG_SELECTORSDIALOG gdb ./inkscape
51// #define DEBUG_SELECTORSDIALOG
52// #define G_LOG_DOMAIN "SELECTORSDIALOG"
53
55
56namespace Inkscape::UI::Dialog {
57
58// Keeps a watch on style element
59class SelectorsDialog::NodeObserver : public Inkscape::XML::NodeObserver
60{
61public:
62 NodeObserver(SelectorsDialog *selectorsdialog)
63 : _selectorsdialog(selectorsdialog)
64 {
65 g_debug("SelectorsDialog::NodeObserver: Constructor");
66 };
67
68 void notifyContentChanged(Inkscape::XML::Node &node,
70 Inkscape::Util::ptr_shared new_content) override;
71
72 SelectorsDialog *_selectorsdialog;
73};
74
75void SelectorsDialog::NodeObserver::notifyContentChanged(Inkscape::XML::Node &,
78{
79 g_debug("SelectorsDialog::NodeObserver::notifyContentChanged");
80 _selectorsdialog->_scrollock = true;
81 _selectorsdialog->_updating = false;
82 _selectorsdialog->_readStyleElement();
83 _selectorsdialog->_selectRow();
84}
85
86// Keeps a watch for new/removed/changed nodes
87// (Must update objects that selectors match.)
88class SelectorsDialog::NodeWatcher : public Inkscape::XML::NodeObserver
89{
90public:
91 NodeWatcher(SelectorsDialog *selectorsdialog)
92 : _selectorsdialog(selectorsdialog)
93 {
94 g_debug("SelectorsDialog::NodeWatcher: Constructor");
95 };
96
97 void notifyChildAdded(Inkscape::XML::Node &,
99 Inkscape::XML::Node *) override
100 {
101 _selectorsdialog->_nodeAdded(child);
102 }
103
104 void notifyChildRemoved(Inkscape::XML::Node &,
106 Inkscape::XML::Node *) override
107 {
108 _selectorsdialog->_nodeRemoved(child);
109 }
110
111 void notifyAttributeChanged(Inkscape::XML::Node &node,
112 GQuark qname,
114 Util::ptr_shared) override
115 {
116 static GQuark const CODE_id = g_quark_from_static_string("id");
117 static GQuark const CODE_class = g_quark_from_static_string("class");
118
119 if (qname == CODE_id || qname == CODE_class) {
120 _selectorsdialog->_nodeChanged(node);
121 }
122 }
123
124 SelectorsDialog *_selectorsdialog;
125};
126
127namespace {
128
130auto extract_label(CSS::RuleStatement const &rule)
131{
132 return rule.selectors;
133}
134auto extract_label(CSS::BlockAtStatement const &block_at)
135{
136 return block_at.at_statement;
137}
138auto extract_label(CSS::OtherStatement const &other)
139{
140 return other;
141}
142
143constexpr int FONT_WEIGHT_NORMAL = 400;
144constexpr int FONT_WEIGHT_BOLD = 700;
145} // namespace
146
152
154{
155 if (_textNode == &repr) {
156 _textNode = nullptr;
157 }
158
160 _selectRow();
161}
162
164{
165 g_debug("SelectorsDialog::NodeChanged");
166
167 _scrollock = true;
168
170 _selectRow();
171}
172
174
178bool SelectorsDialog::TreeStore::row_draggable_vfunc(const Gtk::TreeModel::Path &path) const
179{
180 g_debug("SelectorsDialog::TreeStore::row_draggable_vfunc");
181
182 auto unconstThis = const_cast<SelectorsDialog::TreeStore *>(this);
183 const_iterator iter = unconstThis->get_iter(path);
184 if (iter) {
185 auto const &row = *iter;
186 bool is_draggable = row[_selectorsdialog->_mColumns._colType] == SELECTOR;
187 return is_draggable;
188 }
189 return Gtk::TreeStore::row_draggable_vfunc(path);
190}
191
195bool SelectorsDialog::TreeStore::row_drop_possible_vfunc(const Gtk::TreeModel::Path &dest,
196 const Glib::ValueBase &) const
197{
198 g_debug("SelectorsDialog::TreeStore::row_drop_possible_vfunc");
199
200 Gtk::TreeModel::Path dest_parent = dest;
201 dest_parent.up();
202 return dest_parent.empty();
203}
204
205// This is only here to handle updating style element after a drag and drop.
206void SelectorsDialog::TreeStore::on_row_deleted(const TreeModel::Path &path)
207{
208 if (_selectorsdialog->_updating)
209 return; // Don't write if we deleted row (other than from DND)
210
211 g_debug("on_row_deleted");
212 _selectorsdialog->_writeStyleElement();
213 _selectorsdialog->_readStyleElement();
214}
215
216Glib::RefPtr<SelectorsDialog::TreeStore> SelectorsDialog::TreeStore::create(SelectorsDialog *selectorsdialog)
217{
218 g_debug("SelectorsDialog::TreeStore::create");
219
221 store->_selectorsdialog = selectorsdialog;
222 store->set_column_types(store->_selectorsdialog->_mColumns);
223 return Glib::RefPtr<SelectorsDialog::TreeStore>(store);
224}
225
233 : DialogBase("/dialogs/selectors", "Selectors")
234{
235 g_debug("SelectorsDialog::SelectorsDialog");
236
237 m_nodewatcher = std::make_unique<NodeWatcher>(this);
238 m_styletextwatcher = std::make_unique<NodeObserver>(this);
239
240 // Tree
242 _treeView.set_model(_store);
243
244 auto const addRenderer = Gtk::make_managed<UI::Widget::IconRenderer>();
245 addRenderer->add_icon("edit-delete");
246 addRenderer->add_icon("list-add");
247 addRenderer->add_icon("empty-icon");
248 addRenderer->signal_activated().connect([this](Glib::ustring const &path) {
249 _vscroll();
250 Gtk::TreeModel::iterator iter = _store->get_iter(path);
251 Gtk::TreeModel::Row row = *iter;
252 if (!row.parent()) {
253 _addToSelector(row);
254 } else {
256 }
257 _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
258 _updating = true;
259 _del.set_visible(true);
260 _updating = false;
261 _selectRow();
262 });
263
264 // ALWAYS be a single selection widget
265 _treeView.get_selection()->set_mode(Gtk::SelectionMode::SINGLE);
266
267 _treeView.set_headers_visible(false);
268 _treeView.enable_model_drag_source();
269 _treeView.enable_model_drag_dest( Gdk::DragAction::MOVE );
270 int addCol = _treeView.append_column("", *addRenderer) - 1;
271 Gtk::TreeViewColumn *col = _treeView.get_column(addCol);
272 if ( col ) {
273 col->add_attribute(addRenderer->property_icon(), _mColumns._colType);
274 }
275
276 auto const label = Gtk::make_managed<Gtk::CellRendererText>();
277 addCol = _treeView.append_column("CSS Selector", *label) - 1;
278 col = _treeView.get_column(addCol);
279 if (col) {
280 col->add_attribute(label->property_text(), _mColumns._colSelector);
281 col->add_attribute(label->property_weight(), _mColumns._fontWeight);
282 }
283 _treeView.set_expander_column(*(_treeView.get_column(1)));
284
285 _treeView.signal_row_expanded().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowExpand));
286 _treeView.signal_row_collapsed().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowCollapse));
287
288 _showWidgets();
289 set_visible(true);
290}
291
293{
294 if (!_scrollock) {
295 _scrollpos = _vadj->get_value();
296 } else {
297 _vadj->set_value(_scrollpos);
298 _scrollock = false;
299 }
300}
301
303{
304 // Pack widgets
305 g_debug("SelectorsDialog::_showWidgets");
306
308 bool dir = prefs->getBool("/dialogs/selectors/vertical", true);
309
310 _paned.set_orientation(dir ? Gtk::Orientation::VERTICAL : Gtk::Orientation::HORIZONTAL);
311
312 _selectors_box.set_orientation(Gtk::Orientation::VERTICAL);
313 _selectors_box.set_name("SelectorsDialog");
314
316 _scrolled_window_selectors.set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC);
317 _scrolled_window_selectors.set_overlay_scrolling(false);
318
319 _vadj = _scrolled_window_selectors.get_vadjustment();
320 _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_vscroll));
322
323 _styleButton(_create, "list-add", "Add a new CSS Selector");
324 _create.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_addSelector));
325 _styleButton(_del, "list-remove", "Remove a CSS Selector");
326
329
330 auto const _horizontal = Gtk::make_managed<Gtk::ToggleButton>();
331 auto const _vertical = Gtk::make_managed<Gtk::ToggleButton>();
332 _horizontal->set_image_from_icon_name(INKSCAPE_ICON("horizontal"));
333 _vertical->set_image_from_icon_name(INKSCAPE_ICON("vertical"));
334 _vertical->set_group(*_horizontal);
335 _vertical->set_active(dir);
336 _vertical->signal_toggled().connect(
337 sigc::bind(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection), _vertical));
338 UI::pack_end(_button_box, *_horizontal, false, false);
339 UI::pack_end(_button_box, *_vertical, false, false);
340
341 _del.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_delSelector));
342 _del.set_visible(false);
343
344 _style_dialog = Gtk::make_managed<StyleDialog>();
345 _style_dialog->set_name("StyleDialog");
346
347 _paned.set_start_child(*_style_dialog);
348 _paned.set_shrink_start_child();
349 _paned.set_end_child(_selectors_box);
350 _paned.set_shrink_end_child();
351 _paned.set_resize_end_child();
352 _paned.set_wide_handle(true);
353 _paned.set_size_request(320, -1);
354
355 auto const contents = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL);
357 UI::pack_start(*contents, _button_box, false, false);
358 contents->set_valign(Gtk::Align::FILL);
360
361 _updating = true;
362 _paned.property_position() = 200;
363 _updating = false;
364
365 set_name("SelectorsAndStyleDialog");
366}
367
368void SelectorsDialog::_toggleDirection(Gtk::ToggleButton *vertical)
369{
370 g_debug("SelectorsDialog::_toggleDirection");
372 bool dir = vertical->get_active();
373 prefs->setBool("/dialogs/selectors/vertical", dir);
374 _paned.set_orientation(dir ? Gtk::Orientation::VERTICAL : Gtk::Orientation::HORIZONTAL);
375 // _paned.check_resize(); // No longer needed?
376 int widthpos = _paned.property_max_position() - _paned.property_min_position();
377 prefs->setInt("/dialogs/selectors/panedpos", widthpos / 2);
378 _paned.property_position() = widthpos / 2;
379}
380
387{
388 g_debug("SelectorsDialog::_getStyleTextNode");
389
390 auto const textNode = get_first_style_text_node(m_root, create_if_missing);
391
392 if (_textNode != textNode) {
393 if (_textNode) {
395 }
396
397 _textNode = textNode;
398
399 if (_textNode) {
401 }
402 }
403
404 return textNode;
405}
406
418 Gtk::TreeIter<Gtk::TreeRow> where)
419{
420 auto row = *where;
421 row[_mColumns._colSelector] = rule.selectors;
422 row[_mColumns._colExpand] = expand;
424 row[_mColumns._colObj] = nullptr;
425 row[_mColumns._colProperties] = rule.rules;
427
428 // Add objects that match the selector as children
429 for (auto const &obj : _getObjVec(rule.selectors)) {
430 auto const id = obj->getId();
431 if (!id) {
432 continue;
433 }
434 auto childrow = *_store->append(row.children());
435 childrow[_mColumns._colSelector] = "#" + Glib::ustring(id);
436 childrow[_mColumns._colExpand] = false;
437 childrow[_mColumns._colType] = OBJECT;
438 childrow[_mColumns._colObj] = obj;
440 }
441}
442
458 Gtk::TreeIter<Gtk::TreeRow> where)
459{
460 auto row = *where;
461 row[_mColumns._colSelector] = block_at.at_statement;
462 row[_mColumns._colExpand] = expand;
463 row[_mColumns._colType] = OTHER;
464 row[_mColumns._colObj] = nullptr;
466
467 if (block_at.block_content) {
468 block_at.block_content->for_each([this, expand, subtree = row.children()](auto const &element) {
469 _insertSyntacticElement(element, expand, _store->append(subtree));
470 });
471 }
472}
473
482void SelectorsDialog::_insertSyntacticElement(CSS::OtherStatement const &other, bool, Gtk::TreeIter<Gtk::TreeRow> where)
483{
484 auto row = *where;
485 row[_mColumns._colSelector] = other;
486 row[_mColumns._colExpand] = false;
487 row[_mColumns._colType] = OTHER;
488 row[_mColumns._colObj] = nullptr;
490}
491
496{
497 g_debug("SelectorsDialog::_readStyleElement(): updating %s", (_updating ? "true" : "false"));
498
499 if (_updating) return; // Don't read if we wrote style element.
500 _updating = true;
501 _scrollock = true;
502 auto const *textNode = _getStyleTextNode();
503
504 // Get content from style text node.
505 std::string const content = (textNode && textNode->content()) ? textNode->content() : "";
506
507 CSS::SyntacticDecomposition const syntactic_decomposition{content};
508 if (syntactic_decomposition.empty()) {
509 _store->clear();
510 _updating = false;
511 return;
512 }
513
514 // Remember the old expanded status before clearing the store
515 std::map<std::string, bool> expanded_status;
516
517 syntactic_decomposition.for_each([&, rows = _store->children(), this](auto const &element) {
518 auto const label = extract_label(element);
519 auto const row_with_matching_label = std::find_if(rows.begin(), rows.end(), [&label, this](auto const &row) {
520 return label == Glib::ustring{row[_mColumns._colSelector]}.raw();
521 });
522 if (row_with_matching_label != rows.end()) {
523 expanded_status.emplace(label, (*row_with_matching_label)[_mColumns._colExpand]);
524 }
525 });
526
527 _store->clear();
528
529 // Populate the tree store with representations of the CSS syntactic decomposition elements
530 syntactic_decomposition.for_each([&, this](auto const &element) {
531 _insertSyntacticElement(element, expanded_status[extract_label(element)], _store->append());
532 });
533
534 _updating = false;
535 _scrollock = false;
536 _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
537
538}
539
540void SelectorsDialog::_rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path)
541{
542 g_debug("SelectorsDialog::_row_expand()");
543 Gtk::TreeModel::Row row = *iter;
544 row[_mColumns._colExpand] = true;
545}
546
547void SelectorsDialog::_rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path)
548{
549 g_debug("SelectorsDialog::_row_collapse()");
550 Gtk::TreeModel::Row row = *iter;
551 row[_mColumns._colExpand] = false;
552}
553
555Glib::ustring SelectorsDialog::_formatRowAsCSS(Gtk::TreeConstRow const &row) const
556{
557 if (row[_mColumns._colType] == SELECTOR) {
558 return row[_mColumns._colSelector] + " { " + row[_mColumns._colProperties] + " }\n";
559 } else if (row[_mColumns._colType] == OTHER) {
560 Glib::ustring result = row[_mColumns._colSelector];
561
562 if (!row.children().empty()) {
563 result += " { ";
564 for (auto const &child : row.children()) {
565 result += _formatRowAsCSS(child);
566 }
567 result += " }";
568 }
569 return result + '\n';
570 }
571 return {};
572}
573
577void SelectorsDialog::_writeStyleElement()
578{
579 if (_updating) {
580 return;
581 }
582 _scrollock = true;
583 _updating = true;
584
585 Glib::ustring style_content;
586 for (auto const &row : _store->children()) {
587 style_content += _formatRowAsCSS(row);
588 }
589
590 Inkscape::XML::Node *text_node = _getStyleTextNode(true);
591 g_assert(text_node);
592 text_node->setContent(style_content.c_str());
593 DocumentUndo::done(SP_ACTIVE_DOCUMENT, _("Edited style element."), INKSCAPE_ICON("dialog-selectors"));
594
595 _updating = false;
596 _scrollock = false;
597 _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
598 g_debug("SelectorsDialog::_writeStyleElement(): | %s |", style_content.c_str());
599}
600
601Glib::ustring SelectorsDialog::_getSelectorClasses(Glib::ustring selector)
602{
603 g_debug("SelectorsDialog::_getSelectorClasses");
604
605 if (std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[ ]+", selector);
606 !tokensplus.empty())
607 {
608 selector = std::move(tokensplus.back());
609 } else {
610 g_assert(!tokensplus.empty());
611 }
612
613 // Erase any comma/space
614 Util::trim(selector, ",");
615 Glib::ustring toparse = Glib::ustring(selector);
616 selector = Glib::ustring("");
617 auto i = toparse.find(".");
618 if (i == std::string::npos) {
619 return "";
620 }
621 if (toparse[0] != '.' && toparse[0] != '#') {
622 i = std::min(toparse.find("#"), toparse.find("."));
623 Glib::ustring tag = toparse.substr(0, i);
625 return selector;
626 }
627 if (i != std::string::npos) {
628 toparse.erase(0, i);
629 }
630 }
631
632 i = toparse.find("#");
633 if (i != std::string::npos) {
634 toparse.erase(i, 1);
635 }
636
637 auto j = toparse.find("#");
638 if (j != std::string::npos) {
639 return selector;
640 }
641
642 if (i != std::string::npos) {
643 toparse.insert(i, "#");
644 if (i) {
645 Glib::ustring post = toparse.substr(0, i);
646 Glib::ustring pre = toparse.substr(i, toparse.size() - i);
647 toparse = pre + post;
648 }
649
650 auto k = toparse.find(".");
651 if (k != std::string::npos) {
652 toparse = toparse.substr(k, toparse.size() - k);
653 }
654 }
655
656 return toparse;
657}
658
659std::vector<SPObject *> SelectorsDialog::getSelectedObjects()
660{
661 auto const objects = getDesktop()->getSelection()->objects();
662 return std::vector<SPObject *>(objects.begin(), objects.end());
663}
664
669void SelectorsDialog::_addToSelector(Gtk::TreeModel::Row row)
670{
671 g_debug("SelectorsDialog::_addToSelector: Entrance");
672
673 if (!row) return;
674
675 // Store list of selected elements on desktop (not to be confused with selector).
676 _updating = true;
677 if (row[_mColumns._colType] == OTHER) {
678 return;
679 }
680
681 auto const toAddObjVec = getSelectedObjects();
682
683 Glib::ustring multiselector = row[_mColumns._colSelector];
684 row[_mColumns._colExpand] = true;
685
686 std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", multiselector);
687
688 for (auto const &obj : toAddObjVec) {
689 auto const id = obj->getId();
690 if (!id)
691 continue;
692
693 for (auto const &tok : tokens) {
694 Glib::ustring clases = _getSelectorClasses(tok);
695 if (!clases.empty()) {
696 _insertClass(obj, clases);
697
698 std::vector<SPObject *> currentobjs = _getObjVec(multiselector);
699 bool removeclass = true;
700 for (auto currentobj : currentobjs) {
701 if (g_strcmp0(currentobj->getId(), id) == 0) {
702 removeclass = false;
703 }
704 }
705 if (removeclass) {
706 _removeClass(obj, clases);
707 }
708 }
709 }
710
711 std::vector<SPObject *> currentobjs = _getObjVec(multiselector);
712
713 bool insertid = true;
714 for (auto currentobj : currentobjs) {
715 if (g_strcmp0(currentobj->getId(), id) == 0) {
716 insertid = false;
717 }
718 }
719 if (insertid) {
720 multiselector = multiselector + ",#" + id;
721 }
722
723 auto childrow = *_store->prepend(row.children());
724 childrow[_mColumns._colSelector] = "#" + Glib::ustring(id);
725 childrow[_mColumns._colExpand] = false;
726 childrow[_mColumns._colType] = OBJECT;
727 childrow[_mColumns._colObj] = obj;
728 childrow[_mColumns._fontWeight] = FONT_WEIGHT_NORMAL;
729 }
730
731 row[_mColumns._colSelector] = multiselector;
732 _updating = false;
733
734 // Add entry to style element
735 for (auto const &obj : toAddObjVec) {
737 SPCSSAttr *css_selector = sp_repr_css_attr_new();
738
739 sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style"));
740
741 Glib::ustring selprops = row[_mColumns._colProperties];
742
743 sp_repr_css_attr_add_from_string(css_selector, selprops.c_str());
744
745 for (const auto & iter : css_selector->attributeList()) {
746 auto const key = g_quark_to_string(iter.key);
747 css->removeAttribute(key);
748 }
749
750 Glib::ustring css_str;
752
754 sp_repr_css_attr_unref(css_selector);
755
756 obj->getRepr()->setAttribute("style", css_str);
757 obj->style->readFromObject(obj);
758 obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
759 }
760
761 _writeStyleElement();
762}
763
768void SelectorsDialog::_removeFromSelector(Gtk::TreeModel::Row row)
769{
770 g_debug("SelectorsDialog::_removeFromSelector: Entrance");
771
772 if (!row) return;
773
774 _scrollock = true;
775 _updating = true;
776 SPObject *obj = nullptr;
777 Glib::ustring objectLabel = row[_mColumns._colSelector];
778
779 if (auto const iter = row.parent()) {
780 Gtk::TreeModel::Row parent = *iter;
781 Glib::ustring multiselector = parent[_mColumns._colSelector];
782 Util::trim(multiselector, ",");
783
784 obj = _getObjVec(objectLabel)[0];
785 Glib::ustring selector;
786
787 for (auto const &tok : Glib::Regex::split_simple("[,]+", multiselector)) {
788 if (tok.empty()) {
789 continue;
790 }
791
792 // TODO: handle when other selectors has the removed class applied to maybe not remove
793 Glib::ustring clases = _getSelectorClasses(tok);
794 if (!clases.empty()) {
795 _removeClass(obj, tok, true);
796 }
797
798 auto i = tok.find(row[_mColumns._colSelector]);
799 if (i == std::string::npos) {
800 selector = selector.empty() ? tok : selector + "," + tok;
801 }
802 }
803
804 Util::trim(selector);
805
806 if (selector.empty()) {
807 _store->erase(parent.get_iter());
808 } else {
809 _store->erase(row.get_iter());
810 parent[_mColumns._colSelector] = selector;
811 parent[_mColumns._colExpand] = true;
812 parent[_mColumns._colObj] = nullptr;
813 }
814 }
815
816 _updating = false;
817
818 // Add entry to style element
819 _writeStyleElement();
820 obj->style->readFromObject(obj);
821 obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
822 _scrollock = false;
823 _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
824}
825
831Glib::ustring SelectorsDialog::_getIdList(std::vector<SPObject *> sel)
832{
833 g_debug("SelectorsDialog::_getIdList");
834
835 Glib::ustring str;
836 for (auto const &obj: sel) {
837 if (auto const id = obj->getId()) {
838 if (!str.empty()) {
839 str.append(", ");
840 }
841 str.append("#").append(id);
842 }
843 }
844 return str;
845}
846
852std::vector<SPObject *> SelectorsDialog::_getObjVec(Glib::ustring const &selector)
853{
854 g_debug("SelectorsDialog::_getObjVec: | %s |", selector.c_str());
855
856 g_assert(selector.find(";") == Glib::ustring::npos);
857
858 return getDesktop()->getDocument()->getObjectsBySelector(selector);
859}
860
866void SelectorsDialog::_insertClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className)
867{
868 g_debug("SelectorsDialog::_insertClass");
869
870 for (auto const &obj: objVec) {
871 _insertClass(obj, className);
872 }
873}
874
875template <typename T, typename U>
876[[nodiscard]] bool vector_contains(std::vector<T> const &haystack, U const &needle)
877{
878 auto const end = haystack.end();
879 return std::find(haystack.begin(), end, needle) != end;
880}
881
887void SelectorsDialog::_insertClass(SPObject *obj, const Glib::ustring &className)
888{
889 g_debug("SelectorsDialog::_insertClass");
890
891 Glib::ustring classAttr;
892 if (obj->getRepr()->attribute("class")) {
893 classAttr = obj->getRepr()->attribute("class");
894 }
895
896 std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[.]+", className);
897 std::sort(tokens.begin(), tokens.end());
898 tokens.erase(std::unique(tokens.begin(), tokens.end()), tokens.end());
899
900 std::vector<Glib::ustring> const tokensplus = Glib::Regex::split_simple("[\\s]+", classAttr);
901 for (auto const &tok : tokens) {
902 bool const exist = vector_contains(tokensplus, tok);
903 if (!exist) {
904 classAttr = classAttr.empty() ? tok : classAttr + " " + tok;
905 }
906 }
907
908 obj->getRepr()->setAttribute("class", classAttr);
909}
910
916void SelectorsDialog::_removeClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className, bool all)
917{
918 g_debug("SelectorsDialog::_removeClass");
919
920 for (auto const &obj : objVec) {
921 _removeClass(obj, className, all);
922 }
923}
924
930void SelectorsDialog::_removeClass(SPObject *obj, const Glib::ustring &className, bool all) // without "."
931{
932 g_debug("SelectorsDialog::_removeClass");
933
934 if (obj->getRepr()->attribute("class")) {
935 Glib::ustring classAttr = obj->getRepr()->attribute("class");
936 Glib::ustring classAttrRestore = classAttr;
937 bool notfound = false;
938
939 for (auto const &tok : Glib::Regex::split_simple("[.]+", className)) {
940 auto i = classAttr.find(tok);
941 if (i != std::string::npos) {
942 classAttr.erase(i, tok.length());
943 } else {
944 notfound = true;
945 }
946 }
947
948 if (all && notfound) {
949 classAttr = classAttrRestore;
950 }
951
952 Util::trim(classAttr, ",");
953
954 if (classAttr.empty()) {
955 obj->getRepr()->removeAttribute("class");
956 } else {
957 obj->getRepr()->setAttribute("class", classAttr);
958 }
959 }
960}
961
967void SelectorsDialog::_addSelector()
968{
969 g_debug("SelectorsDialog::_addSelector: Entrance");
970 _scrollock = true;
971
972 // Store list of selected elements on desktop (not to be confused with selector).
973 auto const objVec = getSelectedObjects();
974
975 // ==== Create popup dialog ====
976 auto textDialogPtr = std::make_unique<Gtk::Dialog>();
977 textDialogPtr->property_modal() = true;
978 textDialogPtr->property_title() = _("CSS selector");
979 textDialogPtr->add_button(_("Cancel"), Gtk::ResponseType::CANCEL);
980 textDialogPtr->add_button(_("Add"), Gtk::ResponseType::OK);
981
982 auto const textEditPtr = Gtk::make_managed<Gtk::Entry>();
983 textEditPtr->signal_activate().connect(
984 sigc::bind(sigc::mem_fun(*this, &SelectorsDialog::_closeDialog), textDialogPtr.get()));
985 UI::pack_start(*textDialogPtr->get_content_area(), *textEditPtr, UI::PackOptions::shrink);
986
987 auto const textLabelPtr = Gtk::make_managed<Gtk::Label>(_("Invalid CSS selector."));
988 UI::pack_start(*textDialogPtr->get_content_area(), *textLabelPtr, UI::PackOptions::shrink);
989
995 if (getDesktop()->getSelection()->isEmpty()) {
996 textEditPtr->set_text(".Class1");
997 } else {
998 textEditPtr->set_text(_getIdList(objVec));
999 }
1000
1001 Gtk::Requisition sreq1, sreq2;
1002 textDialogPtr->get_preferred_size(sreq1, sreq2);
1003 int const minWidth = std::max(200, sreq2.get_width());
1004 int const minHeight = std::max(100, sreq2.get_height());
1005 textDialogPtr->set_size_request(minWidth, minHeight);
1006
1007 textEditPtr->set_visible(true);
1008 textLabelPtr->set_visible(false);
1009 textDialogPtr->set_visible(true);
1010
1011 // ==== Get response ====
1012 int result = -1;
1013 bool invalid = true;
1014 Glib::ustring selectorValue;
1015 Glib::ustring originalValue;
1016 while (invalid) {
1017 result = dialog_run(*textDialogPtr);
1018 if (result != Gtk::ResponseType::OK) { // Cancel, close dialog, etc.
1019 return;
1020 }
1027 originalValue = textEditPtr->get_text();
1028 {
1029 std::unique_ptr<CRSelector, decltype(&cr_selector_destroy)> selector{
1030 cr_selector_parse_from_buf(reinterpret_cast<guchar const *>(originalValue.c_str()), CR_UTF_8),
1032 if (!selector) {
1033 continue; // Try again on parse error
1034 }
1035 selectorValue = CSS::selector_to_validated_string(*selector);
1036 }
1037 _del.set_visible(true);
1038 if (originalValue.find("@import ") == std::string::npos && selectorValue.empty()) {
1039 textLabelPtr->set_visible(true);
1040 } else {
1041 invalid = false;
1042 }
1043 }
1044
1045 // ==== Handle response ====
1046 // If class selector, add selector name to class attribute for each object
1047 Util::trim(selectorValue, ",");
1048 if (originalValue.find("@import ") != std::string::npos) {
1049 Gtk::TreeModel::Row row = *_store->prepend();
1050 row[_mColumns._colSelector] = originalValue;
1051 row[_mColumns._colExpand] = false;
1052 row[_mColumns._colType] = OTHER;
1053 row[_mColumns._colObj] = nullptr;
1054 row[_mColumns._fontWeight] = FONT_WEIGHT_NORMAL;
1055 } else {
1056 auto const tokens = Glib::Regex::split_simple("[,]+", selectorValue);
1057 for (auto const &obj : objVec) {
1058 for (auto const &tok : tokens) {
1059 Glib::ustring clases = _getSelectorClasses(tok);
1060 if (clases.empty()) {
1061 continue;
1062 }
1063
1064 _insertClass(obj, clases);
1065
1066 auto const currentobjs = _getObjVec(selectorValue);
1067 bool const removeclass = !vector_contains(currentobjs, obj);
1068 if (removeclass) {
1069 _removeClass(obj, clases);
1070 }
1071 }
1072 }
1073
1074 Gtk::TreeModel::Row row = *(_store->prepend());
1075 row[_mColumns._colExpand] = true;
1076 row[_mColumns._colType] = SELECTOR;
1077 row[_mColumns._colSelector] = selectorValue;
1078 row[_mColumns._colObj] = nullptr;
1079 row[_mColumns._fontWeight] = FONT_WEIGHT_NORMAL;
1080
1081 for (auto const &obj : _getObjVec(selectorValue)) {
1082 auto const id = obj->getId();
1083 if (!id)
1084 continue;
1085
1086 auto childrow = *(_store->prepend(row.children()));
1087 childrow[_mColumns._colSelector] = "#" + Glib::ustring(id);
1088 childrow[_mColumns._colExpand] = false;
1089 childrow[_mColumns._colType] = OBJECT;
1090 childrow[_mColumns._colObj] = obj;
1091 childrow[_mColumns._fontWeight] = FONT_WEIGHT_NORMAL;
1092 }
1093 }
1094
1095 // Add entry to style element
1096 _writeStyleElement();
1097 _scrollock = false;
1098 _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
1099}
1100
1101void SelectorsDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::ResponseType::OK); }
1102
1107void SelectorsDialog::_delSelector()
1108{
1109 g_debug("SelectorsDialog::_delSelector");
1110
1111 _scrollock = true;
1112 Glib::RefPtr<Gtk::TreeSelection> refTreeSelection = _treeView.get_selection();
1113 Gtk::TreeModel::iterator iter = refTreeSelection->get_selected();
1114 if (!iter) return;
1115
1116 _vscroll();
1117
1118 if (iter->children().size() > 2) {
1119 return;
1120 }
1121
1122 _updating = true;
1123 _store->erase(iter);
1124 _updating = false;
1125 _writeStyleElement();
1126 _del.set_visible(false);
1127 _scrollock = false;
1128 _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
1129}
1130
1131// -------------------------------------------------------------------
1132
1133class PropertyData
1134{
1135public:
1136 PropertyData() = default;;
1137 PropertyData(Glib::ustring name) : _name(std::move(name)) {};
1138
1139 void _setSheetValue(Glib::ustring value) { _sheetValue = value; };
1140 void _setAttrValue(Glib::ustring value) { _attrValue = value; };
1141 Glib::ustring _getName() { return _name; };
1142 Glib::ustring _getSheetValue() { return _sheetValue; };
1143 Glib::ustring _getAttrValue() { return _attrValue; };
1144
1145private:
1146 Glib::ustring _name;
1147 Glib::ustring _sheetValue;
1148 Glib::ustring _attrValue;
1149};
1150
1151// -------------------------------------------------------------------
1152
1153SelectorsDialog::~SelectorsDialog()
1154{
1155 removeObservers();
1156 _style_dialog->setDesktop(nullptr);
1157}
1158
1159void SelectorsDialog::update()
1160{
1161 _style_dialog->update();
1162}
1163
1164void SelectorsDialog::desktopReplaced()
1165{
1166 _style_dialog->setDesktop(getDesktop());
1167}
1168
1169void SelectorsDialog::removeObservers()
1170{
1171 if (_textNode) {
1172 _textNode->removeObserver(*m_styletextwatcher);
1173 _textNode = nullptr;
1174 }
1175 if (m_root) {
1176 m_root->removeSubtreeObserver(*m_nodewatcher);
1177 m_root = nullptr;
1178 }
1179}
1180
1181void SelectorsDialog::documentReplaced()
1182{
1183 removeObservers();
1184 if (auto document = getDocument()) {
1185 m_root = document->getReprRoot();
1186 m_root->addSubtreeObserver(*m_nodewatcher);
1187 }
1188 selectionChanged(getSelection());
1189}
1190
1191void SelectorsDialog::selectionChanged(Selection *selection)
1192{
1193 _lastpath.clear();
1194 _readStyleElement();
1195 _selectRow();
1196}
1197
1202void SelectorsDialog::_selectRow()
1203{
1204 g_debug("SelectorsDialog::_selectRow: updating: %s", (_updating ? "true" : "false"));
1205
1206 _scrollock = true;
1207
1208 _del.set_visible(false);
1209
1210 std::vector<Gtk::TreeModel::Path> selectedrows = _treeView.get_selection()->get_selected_rows();
1211 if (selectedrows.size() == 1) {
1212 Gtk::TreeModel::Row row = *_store->get_iter(selectedrows[0]);
1213 if (!row.parent() && row.children().size() < 2) {
1214 _del.set_visible(true);
1215 }
1216 if (row) {
1217 _style_dialog->setCurrentSelector(row[_mColumns._colSelector]);
1218 }
1219 } else if (selectedrows.size() == 0) {
1220 _del.set_visible(true);
1221 }
1222
1223 if (_updating || !getDesktop()) return; // Avoid updating if we have set row via dialog.
1224
1225 Gtk::TreeModel::Children children = _store->children();
1226
1227 Inkscape::Selection* selection = getDesktop()->getSelection();
1228 if (selection->isEmpty()) {
1229 _style_dialog->setCurrentSelector("");
1230 }
1231
1232 for (auto &&row : children) {
1233 row[_mColumns._fontWeight] = FONT_WEIGHT_NORMAL;
1234 for (auto &&subrow : row.children()) {
1235 subrow[_mColumns._fontWeight] = FONT_WEIGHT_NORMAL;
1236 }
1237 }
1238
1239 // Sort selection for matching.
1240 auto selected_objs = getSelectedObjects();
1241 std::sort(selected_objs.begin(), selected_objs.end());
1242
1243 for (auto &&row : children) {
1244 if (row[_mColumns._colType] != SELECTOR) {
1245 continue;
1246 }
1247 // Recalculate the selector, in real time.
1248 auto row_children = _getObjVec(row[_mColumns._colSelector]);
1249 std::sort(row_children.begin(), row_children.end());
1250
1251 // If all selected objects are in the css-selector, select it.
1252 if (row_children == selected_objs) {
1253 row[_mColumns._fontWeight] = FONT_WEIGHT_BOLD;
1254 }
1255
1256 for (auto &&subrow : row.children()) {
1257 if (subrow[_mColumns._colObj] && selection->includes(subrow[_mColumns._colObj])) {
1258 subrow[_mColumns._fontWeight] = FONT_WEIGHT_BOLD;
1259 }
1260 }
1261
1262 if (row[_mColumns._colExpand]) {
1263 _treeView.expand_to_path(_treeView.get_model()->get_path(row.get_iter()));
1264 }
1265 }
1266
1267 _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
1268}
1269
1276void SelectorsDialog::_styleButton(Gtk::Button &btn, char const *iconName, char const *tooltip)
1277{
1278 g_debug("SelectorsDialog::_styleButton");
1279
1280 btn.set_image_from_icon_name(iconName, Gtk::IconSize::NORMAL); // Previously GTK_ICON_SIZE_SMALL_TOOLBAR
1281 btn.set_has_frame(false);
1282 btn.set_tooltip_text (tooltip);
1283}
1284
1285} // namespace Inkscape::UI::Dialog
1286
1287/*
1288 Local Variables:
1289 mode:c++
1290 c-file-style:"stroustrup"
1291 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1292 indent-tabs-mode:nil
1293 fill-column:99
1294 End:
1295*/
1296// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
TODO: insert short description here.
TODO: insert short description here.
Fragment store
Definition canvas.cpp:155
A partial syntactic decomposition of a CSS stylesheet into syntactic elements.
void for_each(SyntacticElementHandler auto &&handler) const
Execute an operation for each syntactic element, in the order of their occurrence.
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
bool isEmpty()
Returns true if no items are selected.
Preference storage class.
Definition preferences.h:61
bool getBool(Glib::ustring const &pref_path, bool def=false)
Retrieve a Boolean value.
static Preferences * get()
Access the singleton Preferences object.
void setInt(Glib::ustring const &pref_path, int value)
Set an integer value.
void setBool(Glib::ustring const &pref_path, bool value)
Set a Boolean value.
The set of selected SPObjects for a given document and layer model.
Definition selection.h:80
bool includes(XML::Node *repr, bool anyAncestor=false)
Returns true if the given item is selected.
Definition selection.h:140
DialogBase is the base class for the dialog system.
Definition dialog-base.h:42
Gtk::TreeModelColumn< Glib::ustring > _colSelector
Gtk::TreeModelColumn< Glib::ustring > _colProperties
void on_row_deleted(const TreeModel::Path &path) final
bool row_drop_possible_vfunc(const Gtk::TreeModel::Path &path, const Glib::ValueBase &selection_data) const final
Allow dropping only in between other selectors.
bool row_draggable_vfunc(const Gtk::TreeModel::Path &path) const final
Allow dragging only selectors.
static Glib::RefPtr< SelectorsDialog::TreeStore > create(SelectorsDialog *styledialog)
The SelectorsDialog class A list of CSS selectors will show up in this dialog.
void _readStyleElement()
Fill the internal Gtk::TreeStore from the svg:style element.
SelectorsDialog()
Constructor A treeview and a set of two buttons are added to the dialog.
Inkscape::XML::Node * _getStyleTextNode(bool create_if_missing=false)
void _selectRow()
This function selects the row in treeview corresponding to an object selected in the drawing.
void _insertSyntacticElement(CSS::RuleStatement const &rule, bool expand, Gtk::TreeIter< Gtk::TreeRow > where)
Populate a tree row with a representation of a CSS rule statement.
void _styleButton(Gtk::Button &btn, char const *iconName, char const *tooltip)
void _addToSelector(Gtk::TreeModel::Row row)
Glib::RefPtr< Gtk::Adjustment > _vadj
void _nodeChanged(Inkscape::XML::Node &repr)
void _rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path)
void _rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path)
void _nodeAdded(Inkscape::XML::Node &repr)
void _removeFromSelector(Gtk::TreeModel::Row row)
void _addSelector()
This function opens a dialog to add a selector.
void _nodeRemoved(Inkscape::XML::Node &repr)
std::unique_ptr< Inkscape::XML::NodeObserver > m_styletextwatcher
std::vector< SPObject * > _getObjVec(Glib::ustring const &selector)
void _delSelector()
This function deletes selector when '-' at the bottom is clicked.
void _toggleDirection(Gtk::ToggleButton *vertical)
std::unique_ptr< Inkscape::XML::NodeObserver > m_nodewatcher
Interface for XML node observers.
Interface for refcounted XML nodes.
Definition node.h:80
virtual const AttributeVector & attributeList() const =0
Get a list of the node's attributes.
void setAttribute(Util::const_char_ptr key, Util::const_char_ptr value)
Change an attribute of this node.
Definition node.cpp:25
virtual void setContent(char const *value)=0
Set the content of a text or comment node.
virtual char const * attribute(char const *key) const =0
Get the string representation of a node's attribute.
void removeAttribute(Inkscape::Util::const_char_ptr key)
Remove an attribute of this node.
Definition node.h:280
virtual void addObserver(NodeObserver &observer)=0
Add an object that will be notified of the changes to this node.
virtual void removeObserver(NodeObserver &observer)=0
Remove an object from the list of observers.
static bool isSVGElement(Glib::ustring const &element)
SPObject is an abstract base class of all of the document nodes at the SVG document level.
Definition sp-object.h:160
SPStyle * style
Represents the style properties, whether from presentation attributes, the style attribute,...
Definition sp-object.h:248
Inkscape::XML::Node * getRepr()
Returns the XML representation of tree.
void requestDisplayUpdate(unsigned int flags)
Queues an deferred update of this object's display.
void readFromObject(SPObject *object)
Read style properties from object's repr.
Definition style.cpp:608
@ FONT_WEIGHT_BOLD
Definition cr-fonts.h:195
@ FONT_WEIGHT_NORMAL
Definition cr-fonts.h:194
typedefG_BEGIN_DECLS struct _CRSelector CRSelector
Definition cr-selector.h:40
CRSelector * cr_selector_parse_from_buf(const guchar *a_char_buf, enum CREncoding a_enc)
void cr_selector_destroy(CRSelector *a_this)
@ CR_UTF_8
Definition cr-utils.h:89
std::shared_ptr< Css const > css
Css & result
Editable view implementation.
static char const *const parent
Definition dir-util.cpp:70
TODO: insert short description here.
Macro for icon names used in Inkscape.
Inkscape::XML::Node * node
Glib::ustring label
std::string OtherStatement
Another CSS statement, currently not handled in a special way; for example @charset.
std::string selector_to_validated_string(CRSelector const &croco_selector)
Convert a CSS selector to a string, performing a fix-up if needed.
Dialog code.
Definition desktop.h:117
bool vector_contains(std::vector< T > const &haystack, U const &needle)
XML::Node * get_first_style_text_node(XML::Node *root, bool create_if_missing)
Get the first <style> element's first text node.
void pack_end(Gtk::Box &box, Gtk::Widget &child, bool const expand, bool const fill, unsigned const padding)
Adds child to box, packed with reference to the end of box.
Definition pack.cpp:153
void pack_start(Gtk::Box &box, Gtk::Widget &child, bool const expand, bool const fill, unsigned const padding)
Adds child to box, packed with reference to the start of box.
Definition pack.cpp:141
int dialog_run(Gtk::Dialog &dialog)
This is a GTK4 porting aid meant to replace the removal of the Gtk::Dialog synchronous API.
void trim(Glib::ustring &input, Glib::ustring const &also_remove="")
Modifies a string in place, removing leading and trailing whitespace characters.
Definition trim.h:34
STL namespace.
static cairo_user_data_key_t key
Helpers for using Gtk::Boxes, encapsulating large changes between GTK3 & GTK4.
Singleton class to access the preferences file in a convenient way.
Ocnode * child[8]
Definition quantize.cpp:33
SPCSSAttr * sp_repr_css_attr_new()
Creates an empty SPCSSAttr (a class for manipulating CSS style properties).
Definition repr-css.cpp:67
void sp_repr_css_write_string(SPCSSAttr *css, Glib::ustring &str)
Write a style attribute string from a list of properties stored in an SPCSAttr object.
Definition repr-css.cpp:242
void sp_repr_css_attr_unref(SPCSSAttr *css)
Unreferences an SPCSSAttr (will be garbage collected if no references remain).
Definition repr-css.cpp:76
void sp_repr_css_attr_add_from_string(SPCSSAttr *css, gchar const *p)
Use libcroco to parse a string for CSS properties and then merge them into an existing SPCSSAttr.
Definition repr-css.cpp:340
A dialog for CSS selectors.
SPCSSAttr - interface for CSS Attributes.
guint32 GQuark
A decomposed block -statement, consisting of the statement and the contents of the associated block.
std::unique_ptr< SyntacticDecomposition > block_content
A decomposed CSS rule statement, consisting of a selector (which can be complex), and the associated ...
std::string selectors
Selectors for a rule set statement.
std::string rules
Semicolon-separated rules.
SPStyle - a style object for SPItem objects.
A dialog for CSS selectors.
Parsing utils capable of producing a rudimentary syntactic decomposition of a CSS stylesheet.
Glib::ustring name
Definition toolbars.cpp:55
Inkscape::Util::trim - trim whitespace and other characters.