Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
objects.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * A panel for listing objects in a document.
4 *
5 * Authors:
6 * Martin Owens
7 * Mike Kowalski
8 * Adam Belis (UX/Design)
9 *
10 * Copyright (C) Authors 2020-2022
11 *
12 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
13 */
14
15#include <cmath>
16#include <string>
17#include <glibmm/i18n.h>
18#include <glibmm/main.h>
19#include <glibmm/ustring.h>
20#include <pango/pango-utils.h>
21#include <gtkmm/builder.h>
22#include <gtkmm/button.h>
23#include <gtkmm/cellrenderer.h>
24#include <gtkmm/checkbutton.h>
25#include <gtkmm/dragsource.h>
26#include <gtkmm/droptarget.h>
27#include <gtkmm/enums.h>
28#include <gtkmm/eventcontrollerkey.h>
29#include <gtkmm/eventcontrollermotion.h>
30#include <gtkmm/gestureclick.h>
31#include <gtkmm/icontheme.h>
32#include <gtkmm/popover.h>
33#include <gtkmm/scale.h>
34#include <gtkmm/searchentry2.h>
35#include <gtkmm/separator.h>
36#include <gtkmm/togglebutton.h>
37#include <gtkmm/treestore.h>
38#include <gtkmm/treeview.h>
39
40#include "objects.h"
42#include "desktop.h"
43#include "desktop-style.h"
45#include "document.h"
46#include "document-undo.h"
47#include "filter-chemistry.h"
48#include "inkscape.h"
49#include "inkscape-window.h"
50#include "layer-manager.h"
51#include "message-stack.h"
52#include "object/sp-root.h"
53#include "object/sp-shape.h"
54#include "style-enums.h"
55#include "style.h"
57#include "ui/builder-utils.h"
58#include "ui/contextmenu.h"
59#include "ui/controller.h"
60#include "ui/icon-loader.h"
61#include "ui/icon-names.h"
62#include "ui/pack.h"
63#include "ui/popup-menu.h"
64#include "ui/shortcuts.h"
65#include "ui/tools/node-tool.h"
66#include "ui/util.h"
67#include "ui/widget/canvas.h"
72#include "ui/widget/shapeicon.h"
74
75// alpha (transparency) multipliers corresponding to item selection state combinations (SelectionState)
76// when 0 - do not color item's background
77static double const SELECTED_ALPHA[16] = {
78 0.00, //00 not selected
79 0.90, //01 selected
80 0.50, //02 layer focused
81 0.20, //03 layer focused & selected
82 0.00, //04 child of focused layer
83 0.90, //05 selected child of focused layer
84 0.50, //06 2 and 4
85 0.90, //07 1, 2 and 4
86 0.40, //08 child of selected group
87 0.90, //09 1 and 8
88 0.40, //10 2 and 8
89 0.90, //11 1, 2 and 8
90 0.40, //12 4 and 8
91 0.90, //13 1, 4 and 8
92 0.40, //14 2, 4 and 8
93 0.90, //15 1, 2 , 4 and 8
94};
95
96static double const HOVER_ALPHA = 0.10;
97
98namespace Inkscape::UI::Dialog {
99
100namespace {
101
102void connect_on_window_when_mapped(Glib::RefPtr<Gtk::EventController> controller, Gtk::Widget &widget)
103{
104 auto const on_map = [controller, &widget] {
105 auto& window = dynamic_cast<Gtk::Window &>(*widget.get_root());
106 window.add_controller(controller);
107 };
108 auto const on_unmap = [controller, &widget] {
109 auto& window = dynamic_cast<Gtk::Window &>(*widget.get_root());
110 window.remove_controller(controller);
111 };
112 widget.signal_map().connect(on_map);
113 widget.signal_unmap().connect(on_unmap);
114}
115
116} // namespace
117
119using namespace Inkscape::UI::Widget;
120
121// This was the 1 widget where we used signal_style_updated(), so just hack together a replacement!
122class ObjectsPanel::TreeViewWithCssChanged final
123 : public WidgetVfuncsClassInit
124 , public Gtk::TreeView
125{
126public:
127 TreeViewWithCssChanged()
128 : Glib::ObjectBase{"TreeViewWithCssChanged"}
130 , Gtk::TreeView{}
131 {
132 }
133
134 auto connect_css_changed(sigc::slot<void (GtkCssStyleChange *)> slot)
135 {
136 return _signal.connect(std::move(slot));
137 }
138
139private:
140 sigc::signal<void (GtkCssStyleChange *)> _signal;
141
142 void css_changed(GtkCssStyleChange * const change) final
143 {
144 _signal.emit(change);
145 }
146};
147
148class ObjectWatcher : public Inkscape::XML::NodeObserver
149{
150public:
151 ObjectWatcher(ObjectsPanel *panel, SPItem *, Gtk::TreeRow *row, bool is_filtered);
152 ~ObjectWatcher() override;
153
154 void initRowInfo();
155 void updateRowInfo();
156 void updateRowHighlight();
157 void updateRowAncestorState(bool invisible, bool locked);
158 void updateRowBg(guint32 rgba = 0.0);
159
160 ObjectWatcher *findChild(Node *node);
161 void addDummyChild();
162 bool addChild(SPItem *, bool dummy = true);
163 void addChildren(SPItem *, bool dummy = false);
164 void setSelectedBit(SelectionState mask, bool enabled);
165 void setSelectedBitRecursive(SelectionState mask, bool enabled);
166 void setSelectedBitChildren(SelectionState mask, bool enabled);
167 void rememberExtendedItems();
168 void moveChild(Node &child, Node *sibling);
169 bool isFiltered() const { return is_filtered; }
170
171 Gtk::TreeNodeChildren getChildren() const;
172 Gtk::TreeModel::iterator getChildIter(Node *) const;
173
174 void notifyChildRemoved(Node &, Node &, Node *) final;
175 void notifyChildOrderChanged(Node &, Node &child, Node *, Node *) final;
176 void notifyChildAdded(Node &, Node &, Node *) final;
177 void notifyAttributeChanged(Node &, GQuark, Util::ptr_shared, Util::ptr_shared) final;
178
180 void setRow(const Gtk::TreeModel::Path &path)
181 {
182 assert(path);
183 row_ref = Gtk::TreeModel::RowReference(panel->_store, path);
184 }
185 void setRow(const Gtk::TreeModel::Row &row)
186 {
187 setRow(panel->_store->get_path(row.get_iter()));
188 }
189
190 // Get the path out of this watcher
191 Gtk::TreeModel::Path getTreePath() const {
192 if (!row_ref)
193 return {};
194 return row_ref.get_path();
195 }
196
198 bool hasRow() const { return bool(row_ref); }
199
201 void transferChild(Node *childnode)
202 {
203 auto *target = panel->getWatcher(childnode->parent());
204 assert(target != this);
205 auto nh = child_watchers.extract(childnode);
206 assert(nh);
207 bool inserted = target->child_watchers.insert(std::move(nh)).inserted;
208 assert(inserted);
209 }
210
212 Node *getRepr() const { return node; }
213 std::optional<Gtk::TreeRow> getRow() const {
214 if (auto path = row_ref.get_path()) {
215 if(auto iter = panel->_store->get_iter(path)) {
216 return *iter;
217 }
218 }
219 return std::nullopt;
220 }
221
222 std::unordered_map<Node const *, std::unique_ptr<ObjectWatcher>> child_watchers;
223
224private:
225 Node *node;
226 Gtk::TreeModel::RowReference row_ref;
227 ObjectsPanel *panel;
228 SelectionState selection_state;
229 bool is_filtered;
230};
231
232class ObjectsPanel::ModelColumns final : public Gtk::TreeModel::ColumnRecord
233{
234public:
235 ModelColumns()
236 {
237 add(_colNode);
238 add(_colLabel);
239 add(_colType);
240 add(_colIconColor);
241 add(_colClipMask);
242 add(_colBgColor);
243 add(_colInvisible);
244 add(_colLocked);
245 add(_colAncestorInvisible);
246 add(_colAncestorLocked);
247 add(_colHover);
248 add(_colItemStateSet);
249 add(_colBlendMode);
250 add(_colOpacity);
251 add(_colItemState);
252 add(_colHoverColor);
253 }
254
255 Gtk::TreeModelColumn<Node*> _colNode;
256 Gtk::TreeModelColumn<Glib::ustring> _colLabel;
257 Gtk::TreeModelColumn<Glib::ustring> _colType;
258 Gtk::TreeModelColumn<unsigned int> _colIconColor;
259 Gtk::TreeModelColumn<unsigned int> _colClipMask;
260 Gtk::TreeModelColumn<Gdk::RGBA> _colBgColor;
261 Gtk::TreeModelColumn<bool> _colInvisible;
262 Gtk::TreeModelColumn<bool> _colLocked;
263 Gtk::TreeModelColumn<bool> _colAncestorInvisible;
264 Gtk::TreeModelColumn<bool> _colAncestorLocked;
265 Gtk::TreeModelColumn<bool> _colHover;
266 Gtk::TreeModelColumn<bool> _colItemStateSet;
267 Gtk::TreeModelColumn<SPBlendMode> _colBlendMode;
268 Gtk::TreeModelColumn<double> _colOpacity;
269 Gtk::TreeModelColumn<Glib::ustring> _colItemState;
270 // Set when hovering over the color tag cell
271 Gtk::TreeModelColumn<bool> _colHoverColor;
272};
273
283ObjectWatcher::ObjectWatcher(ObjectsPanel* panel, SPItem* obj, Gtk::TreeRow *row, bool filtered)
284 : panel(panel)
285 , row_ref()
286 , selection_state(0)
287 , is_filtered(filtered)
288 , node(obj->getRepr())
289{
290 if(row != nullptr) {
291 assert(row->children().empty());
292 setRow(*row);
293 initRowInfo();
294 updateRowInfo();
295 }
296 node->addObserver(*this);
297
298 // Only show children for groups (and their subclasses like SPAnchor or SPRoot)
299 if (!is<SPGroup>(obj)) {
300 return;
301 }
302
303 // Add children as a dummy row to avoid excensive execution when
304 // the tree is really large, but not in layers mode.
305 addChildren(obj, (bool)row && !obj->isExpanded());
306}
307
308ObjectWatcher::~ObjectWatcher()
309{
310 node->removeObserver(*this);
311 Gtk::TreeModel::Path path;
312 if (bool(row_ref) && (path = row_ref.get_path())) {
313 if (auto iter = panel->_store->get_iter(path)) {
314 panel->_store->erase(iter);
315 }
316 }
317 child_watchers.clear();
318}
319
320void ObjectWatcher::initRowInfo()
321{
322 auto const _model = panel->_model.get();
323 auto row = *panel->_store->get_iter(row_ref.get_path());
324 row[_model->_colHover] = false;
325}
326
330void ObjectWatcher::updateRowInfo()
331{
332 if (auto item = cast<SPItem>(panel->getObject(node))) {
333 assert(row_ref);
334 assert(row_ref.get_path());
335
336 auto const _model = panel->_model.get();
337 auto row = *panel->_store->get_iter(row_ref.get_path());
338 row[_model->_colNode] = node;
339
340 // show ids without "#"
341 char const *id = item->getId();
342 row[_model->_colLabel] = (id && !item->label()) ? id : item->defaultLabel();
343
344 row[_model->_colType] = item->typeName();
345 row[_model->_colClipMask] =
348 row[_model->_colInvisible] = item->isHidden();
349 row[_model->_colLocked] = !item->isSensitive();
351 row[_model->_colBlendMode] = blend;
352 auto opacity = 1.0;
353 if (item->style && item->style->opacity.set) {
354 opacity = SP_SCALE24_TO_FLOAT(item->style->opacity.value);
355 }
356 row[_model->_colOpacity] = opacity;
357 std::string item_state;
358 if (opacity == 0.0) {
359 item_state = "object-transparent";
360 }
361 else if (blend != SP_CSS_BLEND_NORMAL) {
362 item_state = opacity == 1.0 ? "object-blend-mode" : "object-translucent-blend-mode";
363 }
364 else if (opacity < 1.0) {
365 item_state = "object-translucent";
366 }
367 row[_model->_colItemState] = item_state;
368 row[_model->_colItemStateSet] = !item_state.empty();
369
370 updateRowHighlight();
371 updateRowAncestorState(row[_model->_colAncestorInvisible], row[_model->_colAncestorLocked]);
372 }
373}
374
378void ObjectWatcher::updateRowHighlight() {
379
380 if (!hasRow()) {
381 std::cerr << "ObjectWatcher::updateRowHighlight: no row_ref: " << node->name() << std::endl;
382 return;
383 }
384
385 if (auto item = cast<SPItem>(panel->getObject(node))) {
386 auto row = *panel->_store->get_iter(row_ref.get_path());
387 auto new_color = item->highlight_color().toRGBA();
388 if (new_color != row[panel->_model->_colIconColor]) {
389 row[panel->_model->_colIconColor] = new_color;
390 updateRowBg(new_color);
391 for (auto &watcher : child_watchers) {
392 watcher.second->updateRowHighlight();
393 }
394 }
395 }
396}
397
401void ObjectWatcher::updateRowAncestorState(bool invisible, bool locked) {
402 auto const _model = panel->_model.get();
403 auto row = *panel->_store->get_iter(row_ref.get_path());
404 row[_model->_colAncestorInvisible] = invisible;
405 row[_model->_colAncestorLocked] = locked;
406 for (auto &watcher : child_watchers) {
407 watcher.second->updateRowAncestorState(
408 invisible || row[_model->_colInvisible],
409 locked || row[_model->_colLocked]);
410 }
411}
412
414
418void ObjectWatcher::updateRowBg(guint32 rgba)
419{
420 assert(row_ref);
421 if (auto row = *panel->_store->get_iter(row_ref.get_path())) {
422 auto alpha = SELECTED_ALPHA[selection_state];
423 if (alpha == 0.0) {
424 row[panel->_model->_colBgColor] = Gdk::RGBA();
425 return;
426 }
427
428 const auto& sel = selection_color;
429 const auto gdk_color = change_alpha(sel, sel.get_alpha() * alpha);
430 row[panel->_model->_colBgColor] = gdk_color;
431 }
432}
433
440void ObjectWatcher::setSelectedBit(SelectionState mask, bool enabled) {
441 if (!row_ref) return;
442 SelectionState value = selection_state;
443 SelectionState original = value;
444 if (enabled) {
445 value |= mask;
446 } else {
447 value &= ~mask;
448 }
449 if (value != original) {
450 selection_state = value;
451 updateRowBg();
452 }
453}
454
459void ObjectWatcher::setSelectedBitRecursive(SelectionState mask, bool enabled)
460{
461 setSelectedBit(mask, enabled);
462 setSelectedBitChildren(mask, enabled);
463}
464void ObjectWatcher::setSelectedBitChildren(SelectionState mask, bool enabled)
465{
466 for (auto &pair : child_watchers) {
467 pair.second->setSelectedBitRecursive(mask, enabled);
468 }
469}
470
474void ObjectWatcher::rememberExtendedItems()
475{
476 if (auto item = cast<SPItem>(panel->getObject(node))) {
477 if (item->isExpanded())
478 panel->_tree.expand_row(row_ref.get_path(), false);
479 }
480 for (auto &pair : child_watchers) {
481 pair.second->rememberExtendedItems();
482 }
483}
484
488ObjectWatcher *ObjectWatcher::findChild(Node *node)
489{
490 auto it = child_watchers.find(node);
491 if (it != child_watchers.end()) {
492 return it->second.get();
493 }
494 return nullptr;
495}
496
505bool ObjectWatcher::addChild(SPItem *child, bool dummy)
506{
507 if (is_filtered && !panel->showChildInTree(child)) {
508 return false;
509 }
510
511 auto children = getChildren();
512 if (!is_filtered && dummy && row_ref) {
513 if (children.empty()) {
514 auto const iter = panel->_store->append(children);
515 assert(panel->isDummy(*iter));
516 return true;
517 } else if (panel->isDummy(children[0])) {
518 return false;
519 }
520 }
521
522 auto *node = child->getRepr();
523 assert(node);
524 Gtk::TreeModel::Row row = *(panel->_store->prepend(children));
525
526 // Ancestor states are handled inside the list store (so we don't have to re-ask every update)
527 auto const _model = panel->_model.get();
528 if (row_ref) {
529 auto parent_row = *panel->_store->get_iter(row_ref.get_path());
530 row[_model->_colAncestorInvisible] = parent_row[_model->_colAncestorInvisible] || parent_row[_model->_colInvisible];
531 row[_model->_colAncestorLocked] = parent_row[_model->_colAncestorLocked] || parent_row[_model->_colLocked];
532 } else {
533 row[_model->_colAncestorInvisible] = false;
534 row[_model->_colAncestorLocked] = false;
535 }
536
537 auto &watcher = child_watchers[node];
538 assert(!watcher);
539 watcher.reset(new ObjectWatcher(panel, child, &row, is_filtered));
540
541 // Make sure new children have the right focus set.
542 if ((selection_state & LAYER_FOCUSED) != 0) {
543 watcher->setSelectedBit(LAYER_FOCUS_CHILD, true);
544 }
545 return false;
546}
547
551void ObjectWatcher::addChildren(SPItem *obj, bool dummy)
552{
553 assert(child_watchers.empty());
554
555 for (auto &child : obj->children) {
556 if (auto item = cast<SPItem>(&child)) {
557 if (addChild(item, dummy) && dummy) {
558 // one dummy child is enough to make the group expandable
559 break;
560 }
561 }
562 }
563}
564
572void ObjectWatcher::moveChild(Node &child, Node *sibling)
573{
574 auto child_iter = getChildIter(&child);
575 if (!child_iter)
576 return; // This means the child was never added, probably not an SPItem.
577
578 // sibling might not be an SPItem and thus not be represented in the
579 // TreeView. Find the closest SPItem and use that for the reordering.
580 while (sibling && !is<SPItem>(panel->getObject(sibling))) {
581 sibling = sibling->prev();
582 }
583
584 auto sibling_iter = getChildIter(sibling);
585 panel->_store->move(child_iter, sibling_iter);
586}
587
593Gtk::TreeNodeChildren ObjectWatcher::getChildren() const
594{
595 Gtk::TreeModel::Path path;
596 if (row_ref && (path = row_ref.get_path())) {
597 return panel->_store->get_iter(path)->children();
598 }
599 assert(!row_ref);
600 return panel->_store->children();
601}
602
609Gtk::TreeModel::iterator ObjectWatcher::getChildIter(Node *node) const
610{
611 auto childrows = getChildren();
612
613 if (!node) {
614 return childrows.end();
615 }
616
617 for (auto &row : childrows) {
618 if (panel->getRepr(row) == node) {
619 return row.get_iter();
620 }
621 }
622 // In layer mode, we will come here for all non-layers
623 return childrows.begin();
624}
625
626void ObjectWatcher::notifyChildAdded( Node &node, Node &child, Node *prev )
627{
628 assert(this->node == &node);
629 // Ignore XML nodes which are not displayable items
630 if (auto item = cast<SPItem>(panel->getObject(&child))) {
631 addChild(item);
632 moveChild(child, prev);
633 }
634}
635void ObjectWatcher::notifyChildRemoved( Node &node, Node &child, Node* /*prev*/ )
636{
637 assert(this->node == &node);
638
639 if (child_watchers.erase(&child) > 0) {
640 return;
641 }
642
643 if (node.firstChild() == nullptr) {
644 assert(row_ref);
645 auto iter = panel->_store->get_iter(row_ref.get_path());
646 panel->removeDummyChildren(*iter);
647 }
648}
649void ObjectWatcher::notifyChildOrderChanged( Node &parent, Node &child, Node */*old_prev*/, Node *new_prev )
650{
651 assert(this->node == &parent);
652
653 moveChild(child, new_prev);
654}
655void ObjectWatcher::notifyAttributeChanged( Node &node, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ )
656{
657 assert(this->node == &node);
658
659 // The root <svg> node doesn't have a row
660 if (this == panel->getRootWatcher()) {
661 return;
662 }
663
664 // Almost anything could change the icon, so update upon any change, defer for lots of updates.
665
666 // examples of not-so-obvious cases:
667 // - width/height: Can change type "circle" to an "ellipse"
668
669 static std::set<GQuark> const excluded{
670 g_quark_from_static_string("transform"),
671 g_quark_from_static_string("x"),
672 g_quark_from_static_string("y"),
673 g_quark_from_static_string("d"),
674 g_quark_from_static_string("sodipodi:nodetypes"),
675 };
676
677 if (excluded.count(name)) {
678 return;
679 }
680
681 updateRowInfo();
682}
683
691 if (node != nullptr && getDocument())
693 return nullptr;
694}
695
704{
705 assert(node);
706
707 if (root_watcher->getRepr() == node) {
708 return root_watcher.get();
709 }
710
711 if (node->parent()) {
712 if (auto parent_watcher = getWatcher(node->parent())) {
713 return parent_watcher->findChild(node);
714 }
715 }
716
717 return nullptr;
718}
719
724 : DialogBase("/dialogs/objects", "Objects")
725 , _model{std::make_unique<ModelColumns>()}
726 , _layer(nullptr)
727 , _is_editing(false)
728 , _page(Gtk::Orientation::VERTICAL)
729 , _builder(create_builder("dialog-objects.glade"))
730 , _settings_menu(get_widget<Gtk::Popover>(_builder, "settings-menu"))
731 , _object_menu(get_widget<Gtk::Popover>(_builder, "object-menu"))
732 , _colors(std::make_shared<Colors::ColorSet>(nullptr, false))
733 , _searchBox(get_widget<Gtk::SearchEntry2>(_builder, "search"))
734 , _opacity_slider(get_widget<Gtk::Scale>(_builder, "opacity-slider"))
735 , _setting_layers(get_derived_widget<PrefCheckButton, Glib::ustring, bool>(_builder, "setting-layers", "/dialogs/objects/layers_only", false))
736 , _setting_track(get_derived_widget<PrefCheckButton, Glib::ustring, bool>(_builder, "setting-track", "/dialogs/objects/expand_to_layer", true))
737 , _tree{*Gtk::make_managed<TreeViewWithCssChanged>()}
738{
739 _store = Gtk::TreeStore::create(*_model);
740
741 //Set up the tree
742 _tree.set_model(_store);
743 _tree.set_headers_visible(false);
744 _tree.set_reorderable(false); // Don't interfere with D&D via controllers!
745 _tree.set_name("ObjectsTreeView");
746
747 auto& header = get_widget<Gtk::Box>(_builder, "header");
748 // Search
749 _searchBox.signal_search_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_searchActivated));
750
751 // Buttons
752 auto& _move_up_button = get_widget<Gtk::Button>(_builder, "move-up");
753 auto& _move_down_button = get_widget<Gtk::Button>(_builder, "move-down");
754 auto& _object_delete_button = get_widget<Gtk::Button>(_builder, "remove-object");
755 _move_up_button.signal_clicked().connect([this]() {
756 _activateAction("win.layer-raise", "selection-stack-up");
757 });
758 _move_down_button.signal_clicked().connect([this]() {
759 _activateAction("win.layer-lower", "selection-stack-down");
760 });
761 _object_delete_button.signal_clicked().connect([this]() {
762 _activateAction("win.layer-delete", "delete-selection");
763 });
764
765 //Label
766 _name_column = Gtk::make_managed<Gtk::TreeViewColumn>();
767 _text_renderer = Gtk::make_managed<Gtk::CellRendererText>();
768 _text_renderer->property_editable() = true;
769 _text_renderer->property_ellipsize().set_value(Pango::EllipsizeMode::END);
770 _text_renderer->signal_editing_started().connect([this](Gtk::CellEditable*,const Glib::ustring&){
771 _is_editing = true;
772 });
773 _text_renderer->signal_editing_canceled().connect([this](){
774 _is_editing = false;
775 });
776 _text_renderer->signal_edited().connect([this](const Glib::ustring&,const Glib::ustring&){
777 _is_editing = false;
778 });
779
780 const int icon_col_width = 24;
781 auto const icon_renderer = Gtk::make_managed<Inkscape::UI::Widget::CellRendererItemIcon>();
782 icon_renderer->property_xpad() = 2;
783 icon_renderer->property_width() = icon_col_width;
784 _tree.append_column(*_name_column);
785 _name_column->set_expand(true);
786 _name_column->pack_start(*icon_renderer, false);
787 _name_column->pack_start(*_text_renderer, true);
788 _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel);
789 _name_column->add_attribute(_text_renderer->property_cell_background_rgba(), _model->_colBgColor);
790 _name_column->add_attribute(icon_renderer->property_shape_type(), _model->_colType);
791 _name_column->add_attribute(icon_renderer->property_color(), _model->_colIconColor);
792 _name_column->add_attribute(icon_renderer->property_clipmask(), _model->_colClipMask);
793 _name_column->add_attribute(icon_renderer->property_cell_background_rgba(), _model->_colBgColor);
794
795 // blend mode and opacity icon(s)
796 _item_state_toggler = Gtk::make_managed<UI::Widget::ImageToggler>(
797 INKSCAPE_ICON("object-blend-mode"), INKSCAPE_ICON("object-opaque"));
798 int modeColNum = _tree.append_column("mode", *_item_state_toggler) - 1;
799 if (auto col = _tree.get_column(modeColNum)) {
800 col->add_attribute(_item_state_toggler->property_active(), _model->_colItemStateSet);
801 col->add_attribute(_item_state_toggler->property_active_icon(), _model->_colItemState);
802 col->add_attribute(_item_state_toggler->property_cell_background_rgba(), _model->_colBgColor);
803 col->add_attribute(_item_state_toggler->property_activatable(), _model->_colHover);
804 col->set_fixed_width(icon_col_width);
805 _blend_mode_column = col;
806 }
807
808 _tree.signal_query_tooltip().connect([this](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltip){
809 Gtk::TreeModel::iterator iter;
810 if (!_tree.get_tooltip_context_iter(x, y, kbd, iter) || !iter) {
811 return false;
812 }
813 auto blend = (*iter)[_model->_colBlendMode];
814 auto opacity = (*iter)[_model->_colOpacity];
815 auto templt = !pango_version_check(1, 50, 0) ?
816 "<span>%1 %2%%\n</span><span line_height=\"0.5\">\n</span><span>%3\n<i>%4</i></span>" :
817 "<span>%1 %2%%\n</span><span>\n</span><span>%3\n<i>%4</i></span>";
818 auto label = Glib::ustring::compose(templt,
819 _("Opacity:"), Util::format_number(opacity * 100.0, 1),
820 _("Blend mode:"), _blend_mode_names[blend]
821 );
822 tooltip->set_markup(label);
823 _tree.set_tooltip_cell(tooltip, nullptr, _blend_mode_column, _item_state_toggler);
824 return true;
825 }, false); // before
826
827 _object_menu.signal_closed().connect([this]{
829 _tree.queue_draw();
830 });
831
832 auto& modes = get_widget<Gtk::Grid>(_builder, "modes");
833 _opacity_slider.set_format_value_func([](double const val){
834 return Util::format_number(val, 1) + "%";
835 });
836 const int min = 0, max = 100;
837 for (int i = min; i <= max; i += 50) {
838 _opacity_slider.add_mark(i, Gtk::PositionType::BOTTOM, "");
839 }
840 _opacity_slider.signal_value_changed().connect([this](){
841 if (current_item) {
842 auto value = _opacity_slider.get_value() / 100.0;
844 os << CLAMP(value, 0.0, 1.0);
845 auto css = sp_repr_css_attr_new();
846 sp_repr_css_set_property(css, "opacity", os.str().c_str());
847 current_item->changeCSS(css, "style");
849 DocumentUndo::maybeDone(current_item->document, ":opacity", _("Change opacity"), INKSCAPE_ICON("dialog-object-properties"));
850 }
851 });
852
853 // object blend mode and opacity popup
854 Gtk::CheckButton *group = nullptr;
855 int top = 0;
856 int left = 0;
857 int width = 2;
858 for (size_t i = 0; i < Inkscape::SPBlendModeConverter._length; ++i) {
860 auto label = _blend_mode_names[data.id] = g_dpgettext2(nullptr, "BlendMode", data.label.c_str());
862 if (top >= (Inkscape::SPBlendModeConverter._length + 1) / 2) {
863 ++left;
864 top = 2;
865 } else if (!left) {
866 auto const sep = Gtk::make_managed<Gtk::Separator>();
867 sep->set_visible(true);
868 modes.attach(*sep, left, top, 2, 1);
869 }
870 } else {
871 // Manual correction that indicates this should all be done in glade
872 if (left == 1 && top == 9)
873 top++;
874
875 auto const check = Gtk::make_managed<Gtk::CheckButton>(label);
876 if (!group) group = check;
877 else check->set_group(*group);
878 check->set_halign(Gtk::Align::START);
879 check->signal_toggled().connect([=, this]{
880 if (!check->get_active()) return;
881 // set blending mode
883 for (auto const &btn : _blend_items) {
884 btn.second->property_active().set_value(btn.first == data.id);
885 }
886 DocumentUndo::done(getDocument(), "set-blend-mode", _("Change blend mode"));
887 }
888 });
889 _blend_items[data.id] = check;
891 check->set_visible(true);
892 modes.attach(*check, left, top, width, 1);
893 width = 1; // First element takes whole width
894 }
895 top++;
896 }
897
898 // Visible icon
899 auto const eyeRenderer = Gtk::make_managed<UI::Widget::ImageToggler>(
900 INKSCAPE_ICON("object-hidden"), INKSCAPE_ICON("object-visible"));
901 int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1;
902 if (auto eye = _tree.get_column(visibleColNum)) {
903 eye->add_attribute(eyeRenderer->property_active(), _model->_colInvisible);
904 eye->add_attribute(eyeRenderer->property_cell_background_rgba(), _model->_colBgColor);
905 eye->add_attribute(eyeRenderer->property_activatable(), _model->_colHover);
906 eye->add_attribute(eyeRenderer->property_gossamer(), _model->_colAncestorInvisible);
907 eye->set_fixed_width(icon_col_width);
908 _eye_column = eye;
909 }
910
911 // Unlocked icon
912 auto const lockRenderer = Gtk::make_managed<UI::Widget::ImageToggler>(
913 INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked"));
914 int lockedColNum = _tree.append_column("lock", *lockRenderer) - 1;
915 if (auto lock = _tree.get_column(lockedColNum)) {
916 lock->add_attribute(lockRenderer->property_active(), _model->_colLocked);
917 lock->add_attribute(lockRenderer->property_cell_background_rgba(), _model->_colBgColor);
918 lock->add_attribute(lockRenderer->property_activatable(), _model->_colHover);
919 lock->add_attribute(lockRenderer->property_gossamer(), _model->_colAncestorLocked);
920 lock->set_fixed_width(icon_col_width);
921 _lock_column = lock;
922 }
923
924 // hierarchy indicator - using item's layer highlight color
925 auto const tag_renderer = Gtk::make_managed<Inkscape::UI::Widget::ColorTagRenderer>();
926 int tag_column = _tree.append_column("tag", *tag_renderer) - 1;
927 if (auto tag = _tree.get_column(tag_column)) {
928 tag->add_attribute(tag_renderer->property_color(), _model->_colIconColor);
929 tag->add_attribute(tag_renderer->property_hover(), _model->_colHoverColor);
930 tag->set_fixed_width(tag_renderer->get_width());
931 _color_tag_column = tag;
932 }
933
934 //Set the expander columns and search columns
935 _tree.set_expander_column(*_name_column);
936 _tree.set_search_column(-1);
937 _tree.set_enable_search(false);
938 _tree.get_selection()->set_mode(Gtk::SelectionMode::NONE);
939
940 //Set up tree signals
941 auto const click = Gtk::GestureClick::create();
942 click->set_button(0); // any
943 click->set_propagation_phase(Gtk::PropagationPhase::TARGET);
944 click->signal_pressed().connect(Controller::use_state([this](auto &&...args) { return on_click(args..., EventType::pressed); }, *click));
945 click->signal_released().connect(Controller::use_state([this](auto &&...args) { return on_click(args..., EventType::released); }, *click));
946 _tree.add_controller(click);
947
948 auto const key = Gtk::EventControllerKey::create();
949 key->signal_key_pressed().connect([this, &key = *key](auto &&...args) { return on_tree_key_pressed(key, args...); }, true);
950 _tree.add_controller(key);
951
952 auto const motion = Gtk::EventControllerMotion::create();
953 motion->set_propagation_phase(Gtk::PropagationPhase::TARGET);
954 motion->signal_enter().connect(sigc::mem_fun(*this, &ObjectsPanel::on_motion_enter));
955 motion->signal_leave().connect(sigc::mem_fun(*this, &ObjectsPanel::on_motion_leave));
956 motion->signal_motion().connect([this, &motion = *motion](auto &&...args) { on_motion_motion(&motion, args...); });
957 _tree.add_controller(motion);
958
959 // Track Alt key on parent window so we don't need to have key focus to work
960 auto const window_key = Gtk::EventControllerKey::create();
961 window_key->signal_key_pressed().connect([this, &window_key = *window_key](auto &&...args) { return on_window_key(window_key, args..., EventType::pressed); }, true);
962 window_key->signal_key_released().connect([this, &window_key = *window_key](auto &&...args) { on_window_key(window_key, args..., EventType::released); });
963 connect_on_window_when_mapped(window_key, _tree);
964
965 // Before expanding a row, replace the dummy child with the actual children
966 _tree.signal_test_expand_row().connect([this](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
967 if (cleanDummyChildren(*iter)) {
968 if (getSelection()) {
970 }
971 }
972 return false;
973 }, false); // before
974 _tree.signal_row_expanded().connect([this](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
975 if (auto item = getItem(*iter)) {
976 item->setExpanded(true);
977 }
978 });
979 _tree.signal_row_collapsed().connect([this](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
980 if (auto item = getItem(*iter)) {
981 item->setExpanded(false);
982 }
983 });
984
985 auto const drag = Gtk::DragSource::create();
986 drag->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
987 drag->set_actions(Gdk::DragAction::MOVE);
988 drag->signal_prepare().connect([this, &drag = *drag](auto &&...args) { return on_prepare(drag, args...); }, false); // before
989 drag->signal_drag_begin().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_begin));
990 drag->signal_drag_end().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_end));
991 _tree.add_controller(drag);
992
993 auto const drop = Gtk::DropTarget::create(Glib::Value<Glib::ustring>::value_type(), Gdk::DragAction::MOVE);
994 drop->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
995 drop->signal_motion().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_motion), false); // before
996 drop->signal_drop().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_drop), false); // before
997 _tree.add_controller(drop);
998
999 //Set up the label editing signals
1000 _text_renderer->signal_edited().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleEdited));
1001
1002 //Set up the scroller window and pack the page
1003 // turn off overlay scrollbars - they block access to the 'lock' icon
1004 _scroller.set_overlay_scrolling(false);
1005 _scroller.set_child(_tree);
1006 _scroller.set_policy( Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC );
1007 _scroller.set_has_frame(true);
1008 _scroller.set_vexpand();
1009 Gtk::Requisition sreq;
1010 Gtk::Requisition sreq_natural;
1011 _scroller.get_preferred_size(sreq_natural, sreq);
1012 int minHeight = 70;
1013 if (sreq.get_height() < minHeight) {
1014 // Set a min height to see the layers when used with Ubuntu liboverlay-scrollbar
1015 _scroller.set_size_request(sreq.get_width(), minHeight);
1016 }
1017
1018 _page.append(header);
1019 _page.append(_scroller);
1021 _popoverbin.set_expand();
1023
1024 auto const set_selection_color = [&] {
1025 selection_color = get_color_with_class(_tree, "theme_selected_bg_color");
1026 };
1027 set_selection_color();
1028
1029 auto enter_layer_label_editing_mode = [this]() {
1030 layerChanged(getDesktop()->layerManager().currentLayer());
1031 if (auto watcher = getWatcher(_layer->getRepr())) {
1032 _tree.set_cursor(watcher->getTreePath(), *_tree.get_column(0), true);
1033 _is_editing = true;
1034 }
1035 };
1036 auto& add_layer_btn = get_widget<Gtk::Button>(_builder, "insert-layer");
1037 add_layer_btn.signal_clicked().connect(enter_layer_label_editing_mode);
1038
1039 _tree_style = _tree.connect_css_changed([=, this] (GtkCssStyleChange *change) {
1040 set_selection_color();
1041
1042 if (!root_watcher) return;
1043 for (auto&& kv : root_watcher->child_watchers) {
1044 if (kv.second) {
1045 kv.second->updateRowHighlight();
1046 }
1047 }
1048 });
1049
1050 // Clear and update entire tree (do not use this in changed/modified signals)
1051 auto prefs = Inkscape::Preferences::get();
1052 _watch_object_mode = prefs->createObserver("/dialogs/objects/layers_only", [this]() { setRootWatcher(); });
1053
1054 update();
1055}
1056
1057ObjectsPanel::~ObjectsPanel() = default;
1058
1060{
1061 layer_changed.disconnect();
1062
1063 if (auto desktop = getDesktop()) {
1065 }
1066}
1067
1072
1074{
1075 root_watcher.reset();
1076 _idle_connection.disconnect();
1077
1078 auto const document = getDocument();
1079 if (!document) return;
1080
1081 auto const prefs = Inkscape::Preferences::get();
1082 bool const filtered = prefs->getBool("/dialogs/objects/layers_only", false) || _searchBox.get_text().length();
1083
1084 // A filtered object watcher behaves differently to an unfiltered one.
1085 // Filtering disables creating dummy children and instead processes entire trees.
1086 root_watcher = std::make_unique<ObjectWatcher>(this, document->getRoot(), nullptr, filtered);
1087 root_watcher->rememberExtendedItems();
1088 layerChanged(getDesktop()->layerManager().currentLayer());
1090}
1091
1097
1098 bool show_child = true;
1099
1100 // Filter by object type, the layers dialog here.
1101 if (prefs->getBool("/dialogs/objects/layers_only", false)) {
1102 auto group = cast<SPGroup>(item);
1103 if (!group || group->layerMode() != SPGroup::LAYER) {
1104 show_child = false;
1105 }
1106 }
1107
1108 // Filter by text search, if the search text box has any contents
1109 auto term = _searchBox.get_text().lowercase();
1110 if (show_child && term.length()) {
1111 // A source document allows search for different pieces of metadata
1112 std::stringstream source;
1113 source << "#" << item->getId();
1114 if (auto label = item->label())
1115 source << " " << label;
1116 source << " @" << item->getTagName();
1117 // Might want to add class names here as ".class"
1118
1119 auto doc = source.str();
1120 transform(doc.begin(), doc.end(), doc.begin(), ::tolower);
1121 show_child = doc.find(term) != std::string::npos;
1122 }
1123
1124 // Now the terrible bit, searching all the children causing a
1125 // duplication of work as it must re-scan up the tree multiple times
1126 // when the tree is very deep.
1127 for (auto child_obj : item->childList(false)) {
1128 if (show_child)
1129 break;
1130 if (auto child = cast<SPItem>(child_obj)) {
1131 show_child = showChildInTree(child);
1132 }
1133 }
1134
1135 return show_child;
1136}
1137
1142{
1143 ObjectWatcher *watcher = nullptr;
1144
1145 for (auto &parent : item->ancestorList(true)) {
1146 if (parent->getRepr() == root_watcher->getRepr()) {
1147 watcher = root_watcher.get();
1148 } else if (watcher &&
1149 (watcher = watcher->findChild(parent->getRepr())))
1150 {
1151 if (auto const row = watcher->getRow()) {
1152 cleanDummyChildren(*row);
1153 }
1154 }
1155 }
1156
1157 return watcher;
1158}
1159
1160// Same definition as in 'document.cpp'
1161#define SP_DOCUMENT_UPDATE_PRIORITY (G_PRIORITY_HIGH_IDLE - 2)
1162
1163void ObjectsPanel::selectionChanged(Selection *selected /* not used */)
1164{
1165 if (!_idle_connection.connected()) {
1166 auto handler = sigc::mem_fun(*this, &ObjectsPanel::_selectionChanged);
1167 int priority = SP_DOCUMENT_UPDATE_PRIORITY + 1;
1168 _idle_connection = Glib::signal_idle().connect(handler, priority);
1169 }
1170}
1171
1173{
1175 root_watcher->setSelectedBitRecursive(SELECTED_OBJECT, false);
1176 root_watcher->setSelectedBitRecursive(GROUP_SELECT_CHILD, false);
1177 bool keep_current_item = false;
1178
1179 for (auto item : getSelection()->items()) {
1180 keep_current_item |= (item == current_item);
1181 if (auto watcher = unpackToObject(item)) {
1182 // Expand layers themselves, but do not expand groups.
1183 auto focus_watcher = watcher;
1184
1185 // Failing to find the child watcher here means the object is filtered out
1186 // of the current object view and we expand to the closest sublayer instead.
1187 if (auto child_watcher = watcher->findChild(item->getRepr())) {
1188 child_watcher->setSelectedBit(SELECTED_OBJECT, true);
1189 child_watcher->setSelectedBitRecursive(GROUP_SELECT_CHILD, true);
1190 watcher = child_watcher;
1191 }
1192
1193 {
1194 if (prefs->getBool("/dialogs/objects/expand_to_layer", true)) {
1195 _tree.expand_to_path(focus_watcher->getTreePath());
1196 if (!_scroll_lock) {
1197 _tree.scroll_to_row(watcher->getTreePath(), 0.5);
1198 }
1199 }
1200 }
1201 }
1202 }
1203 if (!keep_current_item) {
1204 current_item = nullptr;
1205 }
1206 _scroll_lock = false;
1207
1208 // Returning 'false' disconnects idle signal handler
1209 return false;
1210}
1211
1218{
1219 root_watcher->setSelectedBitRecursive(LAYER_FOCUS_CHILD | LAYER_FOCUSED, false);
1220
1221 if (!layer || !layer->getRepr()) return;
1222
1223 auto const watcher = getWatcher(layer->getRepr());
1224 if (watcher && watcher != root_watcher.get()) {
1225 watcher->setSelectedBitChildren(LAYER_FOCUS_CHILD, true);
1226 watcher->setSelectedBit(LAYER_FOCUSED, true);
1227 }
1228
1229 _layer = layer;
1230}
1231
1236void ObjectsPanel::_activateAction(const std::string& layerAction, const std::string& selectionAction)
1237{
1238 auto selection = getSelection();
1239 auto *prefs = Inkscape::Preferences::get();
1240 if (selection->isEmpty() || prefs->getBool("/dialogs/objects/layers_only", false)) {
1242 win->activate_action(layerAction);
1243 } else {
1244 Glib::RefPtr<Gio::Application> app = Gio::Application::get_default();
1245 app->activate_action(selectionAction);
1246 }
1247}
1248
1253bool ObjectsPanel::toggleVisible(Gdk::ModifierType const state, Gtk::TreeModel::Row row)
1254{
1255 auto desktop = getDesktop();
1256 auto selection = getSelection();
1257
1258 if (SPItem* item = getItem(row)) {
1259 if (Controller::has_flag(state, Gdk::ModifierType::SHIFT_MASK)) {
1260 // Toggle Visible for layers (hide all other layers)
1261 if (desktop->layerManager().isLayer(item)) {
1263 DocumentUndo::done(getDocument(), _("Hide other layers"), "");
1264 }
1265 return true;
1266 }
1267 bool visible = !row[_model->_colInvisible];
1268 if (Controller::has_flag(state, Gdk::ModifierType::CONTROL_MASK) ||
1270 {
1272 } else {
1273 for (auto sitem : selection->items()) {
1274 sitem->setHidden(visible);
1275 }
1276 }
1277 // Use maybeDone so user can flip back and forth without making loads of undo items
1278 DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), "");
1279 return visible;
1280 }
1281 return false;
1282}
1283
1284// show blend mode popup menu for current item
1285bool ObjectsPanel::blendModePopup(int const x, int const y, Gtk::TreeModel::Row row)
1286{
1287 auto const item = getItem(row);
1288 if (item == nullptr) {
1289 return false;
1290 }
1291
1292 current_item = nullptr;
1293
1294 auto blend = SP_CSS_BLEND_NORMAL;
1295 if (item->style && item->style->mix_blend_mode.set) {
1296 blend = item->style->mix_blend_mode.value;
1297 }
1298
1299 auto opacity = 1.0;
1300 if (item->style && item->style->opacity.set) {
1301 opacity = SP_SCALE24_TO_FLOAT(item->style->opacity.value);
1302 }
1303
1304 for (auto const &btn : _blend_items) {
1305 btn.second->property_active().set_value(btn.first == blend);
1306 }
1307
1308 _opacity_slider.set_value(opacity * 100);
1310
1312
1315 return true;
1316}
1317
1318bool ObjectsPanel::colorTagPopup(int const x, int const y, Gtk::TreeModel::Row row)
1319{
1320 auto const item = getItem(row);
1321 if (item == nullptr) {
1322 return false;
1323 }
1324 _colors->set(item->highlight_color());
1325 auto color_popup = Gtk::make_managed<Gtk::Popover>();
1326 _color_selector = Gtk::make_managed<ColorNotebook>(_colors);
1327 _color_selector->set_label(_("Highlight Color"));
1328 _color_selector->set_margin(4);
1329 color_popup->set_child(*_color_selector);
1330 _colors->signal_changed.connect([this]() {
1331 if (auto item = getItem(_clicked_item_row)) {
1332 item->setHighlight(_colors->get().value());
1333 DocumentUndo::maybeDone(getDocument(), "highlight-color", _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties"));
1334 }
1335 });
1336 _popoverbin.setPopover(&*color_popup);
1337 UI::popup_at(*color_popup, _tree, x, y);
1338
1339 return true;
1340}
1341
1347bool ObjectsPanel::toggleLocked(Gdk::ModifierType const state, Gtk::TreeModel::Row row)
1348{
1349 auto desktop = getDesktop();
1350 auto selection = getSelection();
1351
1352 if (SPItem* item = getItem(row)) {
1353 if (Controller::has_flag(state, Gdk::ModifierType::SHIFT_MASK)) {
1354 // Toggle lock for layers (lock all other layers)
1355 if (desktop->layerManager().isLayer(item)) {
1357 DocumentUndo::done(getDocument(), _("Lock other layers"), "");
1358 }
1359 return true;
1360 }
1361 bool locked = !row[_model->_colLocked];
1362 if (Controller::has_flag(state, Gdk::ModifierType::CONTROL_MASK) ||
1364 {
1365 item->setLocked(locked);
1366 } else {
1367 for (auto sitem : selection->items()) {
1368 sitem->setLocked(locked);
1369 }
1370 }
1371 // Use maybeDone so user can flip back and forth without making loads of undo items
1372 DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), "");
1373 return locked;
1374 }
1375 return false;
1376}
1377
1382bool ObjectsPanel::on_tree_key_pressed(Gtk::EventControllerKey const &controller,
1383 unsigned keyval, unsigned keycode, Gdk::ModifierType state)
1384{
1385 auto desktop = getDesktop();
1386 if (!desktop)
1387 return false;
1388
1389 // This isn't needed in Gtk4, use expand_collapse_cursor_row instead.
1390 Gtk::TreeModel::Path path;
1391 Gtk::TreeViewColumn *column;
1392 _tree.get_cursor(path, column);
1393
1394 auto const shift = Controller::has_flag(state, Gdk::ModifierType::SHIFT_MASK);
1395 auto const shortcut = Inkscape::Shortcuts::get_from(controller, keyval, keycode, state);
1396 switch (shortcut.get_key()) {
1397 case GDK_KEY_Escape:
1398 if (desktop->getCanvas()) {
1399 desktop->getCanvas()->grab_focus();
1400 return true;
1401 }
1402 break;
1403 case GDK_KEY_Left:
1404 case GDK_KEY_KP_Left:
1405 if (path && shift) {
1406 _tree.collapse_row(path);
1407 return true;
1408 }
1409 break;
1410 case GDK_KEY_Right:
1411 case GDK_KEY_KP_Right:
1412 if (path && shift) {
1413 _tree.expand_row(path, false);
1414 return true;
1415 }
1416 break;
1417 case GDK_KEY_space:
1418 selectCursorItem(Gdk::ModifierType(state));
1419 return true;
1420 // Depending on the action to cover this causes it's special
1421 // text and node handling to block deletion of objects. DIY
1422 case GDK_KEY_Delete:
1423 case GDK_KEY_KP_Delete:
1424 case GDK_KEY_BackSpace:
1425 _activateAction("win.layer-delete", "delete-selection");
1426 // NOTE: We could select a sibling object here to make deleting many objects easier.
1427 return true;
1428 case GDK_KEY_Page_Up:
1429 case GDK_KEY_KP_Page_Up:
1430 if (shift) {
1431 _activateAction("win.layer-top", "selection-top");
1432 return true;
1433 }
1434 break;
1435 case GDK_KEY_Page_Down:
1436 case GDK_KEY_KP_Page_Down:
1437 if (shift) {
1438 _activateAction("win.layer-bottom", "selection-bottom");
1439 return true;
1440 }
1441 break;
1442 case GDK_KEY_Up:
1443 case GDK_KEY_KP_Up:
1444 if (shift) {
1445 _activateAction("win.layer-raise", "selection-stack-up");
1446 return true;
1447 }
1448 break;
1449 case GDK_KEY_Down:
1450 case GDK_KEY_KP_Down:
1451 if (shift) {
1452 _activateAction("win.layer-lower", "selection-stack-down");
1453 return true;
1454 }
1455 case GDK_KEY_Return:
1456 if (auto item = getSelection()->singleItem()) {
1457 if (auto watcher = getWatcher(item->getRepr())) {
1458 auto item_path = watcher->getTreePath();
1459 _tree.set_cursor(item_path, *_tree.get_column(0), true /* start_editing */);
1460 _is_editing = true;
1461 return true;
1462 }
1463 }
1464 }
1465
1466 return false;
1467}
1468
1469bool ObjectsPanel::on_window_key(Gtk::EventControllerKey const &controller,
1470 unsigned keyval, unsigned keycode,
1471 Gdk::ModifierType state, EventType event_type)
1472{
1473 auto desktop = getDesktop();
1474 if (!desktop)
1475 return false;
1476
1477 auto const shortcut = Inkscape::Shortcuts::get_from(controller, keyval, keycode, state);
1478 switch (shortcut.get_key()) {
1479 case GDK_KEY_Alt_L:
1480 case GDK_KEY_Alt_R:
1482 return false;
1483 }
1484
1485 return false;
1486}
1487
1492// Set a status bar text when entering the widget
1493void ObjectsPanel::on_motion_enter(double /*ex*/, double /*ey*/)
1494{
1496 _("<b>Hold ALT</b> while hovering over item to highlight, "
1497 "<b>hold SHIFT</b> and click to hide/lock all."));
1498}
1499// watch mouse leave too to clear any state.
1501{
1503 on_motion_motion(nullptr, 0, 0);
1504}
1505
1506void ObjectsPanel::on_motion_motion(Gtk::EventControllerMotion const *controller,
1507 double ex, double ey)
1508{
1509 if (_is_editing) return;
1510
1511 // Unhover any existing hovered row.
1512 if (_hovered_row_ref) {
1513 if (auto row = *_store->get_iter(_hovered_row_ref.get_path())) {
1514 row[_model->_colHover] = false;
1515 row[_model->_colHoverColor] = false;
1516 // selection etc. might change _colBgColor. Erase hover
1517 // highlight only if it hasn't changed
1518 if (row[_model->_colBgColor] == _hovered_row_color) {
1519 row[_model->_colBgColor] = _hovered_row_old_color;
1520 }
1521 else { // update row's slection color if it has changed
1522 _hovered_row_old_color = row[_model->_colBgColor];
1523 }
1524 }
1525 }
1526
1527 // Allow this function to be called by LEAVE motion
1528 if (controller == nullptr) {
1529 _hovered_row_ref = Gtk::TreeModel::RowReference();
1531 return;
1532 }
1533
1534
1535 Gtk::TreeModel::Path path;
1536 Gtk::TreeViewColumn* col = nullptr;
1537 int cell_x, cell_y;
1538 if (_tree.get_path_at_pos(ex, ey, path, col, cell_x, cell_y)) {
1539 // Only allow drag and drop from the name column, not any others
1540 if (col == _name_column) {
1541 _drag_column = nullptr;
1542 }
1543
1544 // Only allow drag and drop when not filtering. Otherwise bad things happen
1545 // _tree.set_reorderable(col == _name_column);
1546
1547 if (auto row = *_store->get_iter(path)) {
1548 row[_model->_colHover] = true;
1549 _hovered_row_ref = Gtk::TreeModel::RowReference(_store, path);
1550 // update color for hovered row
1551 const Gdk::RGBA color = row[_model->_colBgColor]; // current color
1552 _hovered_row_old_color = color; // store old color
1553 _hovered_row_color = change_alpha(color, color.get_alpha() + HOVER_ALPHA);
1554 row[_model->_colBgColor] = _hovered_row_color;
1555
1556 if (col == _color_tag_column) {
1557 row[_model->_colHoverColor] = true;
1558 }
1559
1560 // Dragging over the eye or locks will set them all
1561 auto item = getItem(row);
1562 if (item && _drag_column && col == _drag_column) {
1563 if (col == _eye_column) {
1564 // Defer visibility to th idle thread (it's expensive)
1565 Glib::signal_idle().connect_once([this, item]() {
1567 DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), "");
1568 }, Glib::PRIORITY_DEFAULT_IDLE);
1569 } else if (col == _lock_column) {
1571 DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), "");
1572 }
1573 }
1574 }
1575 }
1576
1577 auto const state = controller->get_current_event_state();
1578 _handleTransparentHover(Controller::has_flag(state, Gdk::ModifierType::ALT_MASK));
1579}
1580
1582{
1583 auto &trg = getDesktop()->getTranslucencyGroup();
1584 SPItem *item = nullptr;
1585 if (enabled && _hovered_row_ref) {
1586 if (auto row = *_store->get_iter(_hovered_row_ref.get_path())) {
1587 item = getItem(row);
1588 }
1589 }
1590 // Save any solid item from other inkscape features
1591 if (enabled && !_translucency_enabled) {
1592 _old_solid_item = trg.getSolidItem();
1593 } else if (!enabled && _translucency_enabled) {
1595 }
1596 _translucency_enabled = enabled;
1597
1598 // Ask the canvas to only show one item fully opaque
1599 trg.setSolidItem(item);
1600}
1601
1602[[nodiscard]] static auto get_cell_area(Gtk::TreeView const &tree_view,
1603 Gtk::TreeModel::Path const &path, Gtk::TreeViewColumn &column)
1604{
1605 auto area = Gdk::Rectangle{};
1606 tree_view.get_cell_area(path, column, area);
1607 return area;
1608}
1609
1610[[nodiscard]] static auto get_cell_center(Gtk::TreeView const &tree_view,
1611 Gtk::TreeModel::Path const &path, Gtk::TreeViewColumn &column)
1612{
1613 auto const area = get_cell_area(tree_view, path, column);
1614 return std::pair{std::lround(area.get_x() + area.get_width () / 2.0),
1615 std::lround(area.get_y() + area.get_height() / 2.0)};
1616}
1617
1622Gtk::EventSequenceState ObjectsPanel::on_click(Gtk::GestureClick const &gesture,
1623 int const n_press, double const ex, double const ey,
1624 EventType const event_type)
1625{
1626 auto selection = getSelection();
1627 if (!selection) {
1628 return Gtk::EventSequenceState::NONE;
1629 }
1630
1631 if (event_type == EventType::released) {
1632 _drag_column = nullptr;
1633 }
1634
1635 Gtk::TreeModel::Path path;
1636 Gtk::TreeViewColumn* col = nullptr;
1637 int x, y;
1638 if (!_tree.get_path_at_pos(ex, ey, path, col, x, y)) {
1639 // Over background (below list or between list items).
1640 return Gtk::EventSequenceState::NONE;
1641 }
1642
1643 // Setting the cursor on the clicked row so that later calls to selectCursorItem knows which
1644 // item to select (via get_cursor).
1645 // This used to be done in on_motion_motion but was moved here because of issue #5156.
1646 _tree.set_cursor(path);
1647
1648 if (auto row = *_store->get_iter(path)) {
1649 if (event_type == EventType::pressed) {
1650 auto const state = gesture.get_current_event_state();
1651 // Remember column for dragging feature
1652 _drag_column = col;
1653 if (col == _eye_column) {
1654 _drag_flip = toggleVisible(state, row);
1655 } else if (col == _lock_column) {
1656 _drag_flip = toggleLocked(state, row);
1657 } else if (col == _blend_mode_column) {
1658 auto const [cx, cy] = get_cell_center(_tree, path, *_blend_mode_column);
1659 return blendModePopup(cx, cy, row) ? Gtk::EventSequenceState::CLAIMED
1660 : Gtk::EventSequenceState::NONE;
1661 } else if (col == _color_tag_column) {
1662 _clicked_item_row = *_store->get_iter(path);
1663 auto const [cx, cy] = get_cell_center(_tree, path, *_color_tag_column);
1664 return colorTagPopup(cx, cy, row) ? Gtk::EventSequenceState::CLAIMED
1665 : Gtk::EventSequenceState::NONE;
1666 }
1667 }
1668 }
1669
1670 // Block D&D via controllers if over icons.
1671 if (col != _name_column) {
1672 return Gtk::EventSequenceState::CLAIMED;
1673 }
1674
1675 // Gtk lacks the ability to detect if the user is clicking on the
1676 // expander icon. So we must detect it using the cell_area check.
1677 auto const is_expander = x < get_cell_area(_tree, path, *_name_column).get_x();
1678 if (is_expander) {
1679 return Gtk::EventSequenceState::NONE; // Or else expander won't work.
1680 }
1681
1682 // Rename row item.
1683 if (n_press == 2) {
1684 _tree.set_cursor(path, *col, true); // true -> Start editing.
1685 _is_editing = true;
1686 return Gtk::EventSequenceState::CLAIMED;
1687 }
1688
1689 _is_editing &= event_type == EventType::released;
1690
1691 auto row = *_store->get_iter(path);
1692 if (!row) {
1693 // Already handled above by get_path_at_pos()...
1694 return Gtk::EventSequenceState::NONE;
1695 }
1696
1697 SPItem *item = getItem(row);
1698 assert(item);
1699
1701 auto const state = gesture.get_current_event_state();
1702 // returns true if layer has to be set as active but not selected
1703 auto const should_set_current_layer = [&] {
1704 if (!layer) {
1705 return false;
1706 }
1707
1708 // Modifier keys force selection mode.
1709 if (Controller::has_flag(state, Gdk::ModifierType::SHIFT_MASK |
1710 Gdk::ModifierType::CONTROL_MASK)) {
1711 return false;
1712 }
1713
1714 return _layer != layer || selection->includes(layer);
1715 };
1716
1717 // Load the right click menu?
1718 auto const button = gesture.get_current_button();
1719 auto const context_menu = event_type == EventType::pressed && button == 3;
1720
1721 // Select items on button release to not confuse drag (unless it's a right-click which selects
1722 // item for use by context menu).
1723 if (!_is_editing && (event_type == EventType::released || context_menu)) {
1724 if (context_menu) {
1725 // If right-clicking on a layer, make it current for context menu actions to work correctly.
1726 if (layer && !selection->includes(layer)) {
1728 }
1729
1730 // If the item under cursor is not selected, we select it before opening the
1731 // contextmenu. Otherwise, if the item hasn't been selected with left-click
1732 // beforehand, ContextMenu's constructor may select the item and cause the list
1733 // to scroll to it. Also, if the item is the parent group of a selected object,
1734 // it won't get selected by ContextMenu's constructor.
1735 // See https://gitlab.com/inkscape/inkscape/-/issues/5243
1736 if (!selection->includes(item)) {
1737 selectCursorItem(state);
1738 }
1739
1740 // true == hide menu item for opening this dialog!
1741 std::vector<SPItem *> items = {item};
1742 auto menu = Gtk::make_managed<ContextMenu>(getDesktop(), item, items, true);
1743 // popup context menu pointing to the clicked tree row:
1744 _popoverbin.setPopover(menu);
1745 UI::popup_at(*menu, _tree, ex, ey);
1746 } else if (should_set_current_layer()) {
1748 _initial_path = path;
1749 } else {
1750 selectCursorItem(state);
1751 }
1752
1753 return Gtk::EventSequenceState::CLAIMED;
1754 } else {
1755 // Remember the item for we are about to drag it!
1757 }
1758
1759 return Gtk::EventSequenceState::NONE;
1760}
1761
1767void ObjectsPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text)
1768{
1769 _is_editing = false;
1770 if (auto row = *_store->get_iter(path)) {
1771 if (auto item = getItem(row)) {
1772 if (!new_text.empty() && (!item->label() || new_text != item->label())) {
1773 auto obj = cast<SPGroup>(item);
1774 if (obj && obj->layerMode() == SPGroup::LAYER && !obj->isHighlightSet()) {
1775 obj->setHighlight(obj->highlight_color());
1776 }
1777 item->setLabel(new_text.c_str());
1778 DocumentUndo::done(getDocument(), _("Rename object"), "");
1779 }
1780 }
1781 }
1782}
1783
1789bool ObjectsPanel::select_row( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const &path, bool /*sel*/ )
1790{
1791 return true;
1792}
1793
1797Node *ObjectsPanel::getRepr(Gtk::TreeModel::ConstRow const &row) const
1798{
1799 return row[_model->_colNode];
1800}
1801
1806SPItem *ObjectsPanel::getItem(Gtk::TreeModel::ConstRow const &row) const
1807{
1808 auto const this_const = const_cast<ObjectsPanel *>(this);
1809 return cast<SPItem>(this_const->getObject(getRepr(row)));
1810}
1811
1815bool ObjectsPanel::hasDummyChildren(Gtk::TreeModel::ConstRow const &row) const
1816{
1817 for (auto &c : row.children()) {
1818 if (isDummy(c)) {
1819 return true;
1820 }
1821 }
1822 return false;
1823}
1824
1831bool ObjectsPanel::removeDummyChildren(Gtk::TreeModel::Row row)
1832{
1833 auto &children = row.children();
1834 if (!children.empty()) {
1835 auto const iter = row.get_iter();
1836 Gtk::TreeStore::iterator child = children.begin();
1837 if (!isDummy(*child)) {
1838 assert(!hasDummyChildren(row));
1839 return false;
1840 }
1841
1842 do {
1843 assert(child->parent() == iter);
1844 assert(isDummy(*child));
1845 child = _store->erase(child);
1846 } while (child && child->parent() == iter);
1847 }
1848 return true;
1849}
1850
1851bool ObjectsPanel::cleanDummyChildren(Gtk::TreeModel::Row row)
1852{
1853 if (removeDummyChildren(row)) {
1854 assert(row);
1855 if (auto watcher = getWatcher(getRepr(row))) {
1856 watcher->addChildren(getItem(row));
1857 return true;
1858 }
1859 }
1860 return false;
1861}
1862
1868Gdk::DragAction ObjectsPanel::on_drag_motion(double x, double y)
1869{
1870 auto selection = getSelection();
1871 auto document = getDocument();
1872 if (!selection || !document) {
1873 return Gdk::DragAction{}; // not supported
1874 }
1875
1876 Gtk::TreeModel::Path path;
1877 Gtk::TreeView::DropPosition pos;
1878 _tree.get_dest_row_at_pos(x, y, path, pos);
1879 if (path) {
1880 auto item = getItem(*_store->get_iter(path));
1881 if (!item) {
1882 std::cerr << "ObjectsPanel::on_drag_motion: path doesn't correspond to an item!" << std::endl;
1883 return Gdk::DragAction{}; // not supported
1884 }
1885
1886 // Don't drop on self. This causes disturbing flickering so maybe remove this and
1887 // rely on code in "on_drag_drop" to reject dropping on self.
1888 if (selection->includes(item)) {
1889 return Gdk::DragAction{}; // not supported
1890 }
1891
1892 // Don't drop on descendent.
1894 return Gdk::DragAction{}; // not supported
1895 }
1896
1897 // Only allow dragging rows from name column.
1898 int cell_x, cell_y;
1899 Gtk::TreeViewColumn* col = nullptr;
1900 _tree.get_path_at_pos(x, y, path, col, cell_x, cell_y);
1901 if (col != _name_column) {
1902 return Gdk::DragAction{}; // not supported
1903 }
1904
1905 // Setting CSS class here is useless as we can't set CSS on CellRenderer.
1906 } else {
1907 if (_tree.is_blank_at_pos(x, y)) {
1908 // Dropping on background.
1909 path = --_store->children().end();
1910 auto item = getItem(*_store->get_iter(path));
1911 if (selection->includes(item)) {
1912 // Don't drop after self.
1913 return Gdk::DragAction{}; // not supported
1914 }
1915 } else {
1916 std::cerr << "ObjectsPanel::on_drag_motion: invalid drop area!" << std::endl;
1917 }
1918 }
1919
1920 // need to cater scenarios where we got no selection/empty bottom space
1921 return Gdk::DragAction::MOVE;
1922}
1923
1929bool ObjectsPanel::on_drag_drop(Glib::ValueBase const &/*value*/, double x, double y)
1930{
1931 Gtk::TreeModel::Path path;
1932 Gtk::TreeView::DropPosition pos;
1933 _tree.get_dest_row_at_pos(x, y, path, pos);
1934
1935 if (!path) {
1936 if (_tree.is_blank_at_pos(x, y)){
1937 // We are in background/bottom empty space. Hence, need to drop the item at end.
1938 // We will move to the last node/path and set drop position accordingly.
1939 path = --_store->children().end();
1940 pos = Gtk::TreeView::DropPosition::AFTER;
1941 } else {
1942 std::cerr << "ObjectsPanel::on_drag_drop: invalid drop area!" << std::endl;
1943 return true;
1944 }
1945 }
1946
1947 auto drop_repr = getRepr(*_store->get_iter(path));
1948 bool const drop_into = pos != Gtk::TreeView::DropPosition::BEFORE && //
1949 pos != Gtk::TreeView::DropPosition::AFTER;
1950
1951 auto selection = getSelection();
1952 auto document = getDocument();
1953 if (selection && document) {
1954 auto item = document->getObjectByRepr(drop_repr);
1955
1956 // We always try to drop the item, even if we end up dropping it after the non-group item.
1957 if (drop_into && is<SPGroup>(item)) {
1959 } else {
1960 // Note: Object dialog order opposite of XML order.
1961 Node *after = (pos == Gtk::TreeView::DropPosition::BEFORE ||
1962 pos == Gtk::TreeView::DropPosition::INTO_OR_BEFORE)
1963 ? drop_repr : drop_repr->prev();
1964 selection->toLayer(item->parent, after);
1965 }
1966 DocumentUndo::done(document, _("Move items"), INKSCAPE_ICON("selection-move-to-layer"));
1967 }
1968
1969 drag_end_impl();
1970 return true;
1971}
1972
1973Glib::RefPtr<Gdk::ContentProvider> ObjectsPanel::on_prepare(Gtk::DragSource &controller, double x, double y)
1974{
1975 Gtk::TreeModel::Path path;
1976 Gtk::TreeView::DropPosition pos;
1977 _tree.get_dest_row_at_pos(x, y, path, pos);
1978
1979 if (path) {
1980 // Set icon (or else icon is determined by provider value).
1981 auto surface = _tree.create_row_drag_icon(path);
1982 controller.set_icon(surface, x, 12);
1983 }
1984
1985 // We must have some kind of value which matches DropTarget type! Use a string for now.
1986 Glib::Value<Glib::ustring> value;
1987 value.init(G_TYPE_STRING);
1988 value.set("ObjectsPanelDrag");
1989 auto provider = Gdk::ContentProvider::create(value);
1990 return provider;
1991}
1992
1993void ObjectsPanel::on_drag_begin(Glib::RefPtr<Gdk::Drag> const &/*drag*/)
1994{
1995 _scroll_lock = true;
1996
1997 auto selection = _tree.get_selection();
1998 selection->set_mode(Gtk::SelectionMode::MULTIPLE);
1999 selection->unselect_all();
2000
2001 auto obj_selection = getSelection();
2002 if (!obj_selection)
2003 return;
2004
2005 if (current_item && !obj_selection->includes(current_item)) {
2006 // This means the item the user started to drag is not one that is selected
2007 // So we'll deselect everything and start dragging this item instead.
2008 auto watcher = getWatcher(current_item->getRepr());
2009 if (watcher) {
2010 auto path = watcher->getTreePath();
2011 selection->select(path);
2012 obj_selection->set(current_item);
2013 }
2014 } else {
2015 // Drag all the items currently selected (multi-row)
2016 for (auto item : obj_selection->items()) {
2017 auto watcher = getWatcher(item->getRepr());
2018 if (watcher) {
2019 auto path = watcher->getTreePath();
2020 selection->select(path);
2021 }
2022 }
2023 }
2024 // auto content = controller.get_content(); Can't modify content! Can't modify controller!
2025}
2026
2028{
2029 auto selection = _tree.get_selection();
2030 selection->unselect_all();
2031 selection->set_mode(Gtk::SelectionMode::NONE);
2032 current_item = nullptr;
2033}
2034
2035void ObjectsPanel::on_drag_end(Glib::RefPtr<Gdk::Drag> const &/*drag*/, bool /*delete_data*/)
2036{
2037 drag_end_impl();
2038}
2039
2040void ObjectsPanel::selectRange(Gtk::TreeModel::Path start, Gtk::TreeModel::Path end)
2041{
2042 auto &layers = getDesktop()->layerManager();
2043
2044 if (!start || !end) {
2045 return;
2046 }
2047
2048 if (gtk_tree_path_compare(start.gobj(), end.gobj()) > 0) {
2049 std::swap(start, end);
2050 }
2051
2052 auto selection = getSelection();
2053
2054 if (!_start_new_range) {
2055 // Deselect previous selection of this range first and then proceed.
2056 for (auto const &obj : _prev_range) {
2057 if (obj) {
2058 selection->remove(obj.get());
2059 }
2060 }
2061 }
2062
2063 _prev_range.clear();
2064
2065 // Select everything between the initial selection and currently selected item.
2066 _store->foreach ([&](Gtk::TreeModel::Path const &p, Gtk::TreeModel::const_iterator const &it) {
2067 if ((gtk_tree_path_compare(start.gobj(), p.gobj()) <= 0) &&
2068 (gtk_tree_path_compare(end.gobj(), p.gobj()) >= 0)) {
2069 auto obj = getItem(*it);
2070 if (obj) {
2071 if (!layers.isLayer(obj)) {
2072 _prev_range.emplace_back(obj);
2073 selection->add(obj, false);
2074 }
2075 }
2076 }
2077 return false;
2078 });
2079
2080 _start_new_range = false;
2081}
2082
2086bool ObjectsPanel::selectCursorItem(Gdk::ModifierType const state)
2087{
2088 auto &layers = getDesktop()->layerManager();
2089 auto selection = getSelection();
2090 if (!selection)
2091 return false;
2092
2093 Gtk::TreeModel::Path path;
2094 Gtk::TreeViewColumn *column;
2095 _tree.get_cursor(path, column);
2096 if (!path || !column)
2097 return false;
2098
2099 auto row = *_store->get_iter(path);
2100 if (!row)
2101 return false;
2102
2103 if (column == _eye_column) {
2104 toggleVisible(state, row);
2105 } else if (column == _lock_column) {
2106 toggleLocked(state, row);
2107 } else if (column == _name_column) {
2108 auto item = getItem(row);
2109 auto group = cast<SPGroup>(item);
2110 _scroll_lock = true; // Clicking to select shouldn't scroll the treeview.
2111
2112 if (Controller::has_flag(state, Gdk::ModifierType::SHIFT_MASK) && !selection->isEmpty()) {
2113 // Shift + Click or Shift + Ctrl + Click
2114 // TODO: Fix layers expand unexpectedly on range selection.
2116 } else if (Controller::has_flag(state, Gdk::ModifierType::CONTROL_MASK)) {
2117 if (selection->includes(item)) {
2119 } else {
2120 selection->add(item, false);
2121 _initial_path = path;
2122 _start_new_range = true;
2123 }
2124 } else if (group && selection->includes(item) && !group->isLayer()) {
2125 // Clicking off a group (second click) will enter the group
2126 layers.setCurrentLayer(item, true);
2127 } else {
2128 // Just Click
2129 if (layers.currentLayer() == item || group) {
2130 layers.setCurrentLayer(item->parent);
2131 }
2132
2133 selection->set(item);
2134 _initial_path = path;
2135 _start_new_range = true;
2136 }
2137
2138 return true;
2139 }
2140 return false;
2141}
2142
2147{
2148 // The root watcher and watcher tree handles the search operations
2150}
2151
2152} // namespace Inkscape::UI::Dialog
2153
2154/*
2155 Local Variables:
2156 mode:c++
2157 c-file-style:"stroustrup"
2158 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
2159 indent-tabs-mode:nil
2160 fill-column:99
2161 End:
2162*/
2163// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
Gtk builder utilities.
Cairo::RefPtr< Cairo::ImageSurface > surface
Definition canvas.cpp:137
Geom::IntRect visible
Definition canvas.cpp:154
Inkscape canvas widget.
InkscapeWindow * get_active_window()
static InkscapeApplication * instance()
Singleton instance.
A thin wrapper around std::ostringstream, but writing floating point numbers in the format required b...
uint32_t toRGBA(double opacity=1.0) const
Return an sRGB conversion of the color in RGBA int32 format.
Definition color.cpp:117
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
static void maybeDone(SPDocument *document, const gchar *keyconst, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
void toggleLayerSolo(SPObject *object, bool force_hide=false)
Toggle the visibility of every layer except the given layer.
void setCurrentLayer(SPObject *object, bool clear=false)
Sets the current layer of the desktop.
static SPGroup * asLayer(SPObject *object)
Return the SPGroup if we have a layer object.
void toggleLockOtherLayers(SPObject *object, bool force_lock=false)
Toggle the sensitivity of every layer except the given layer.
bool isLayer(SPObject *object) const
True if object is a layer.
sigc::connection connectCurrentLayerChanged(const sigc::slot< void(SPGroup *)> &slot)
void cancel(MessageId id)
removes a message from the stack, given its id
MessageId push(MessageType type, char const *message)
pushes a message onto the stack
SPItemRange items()
Returns a range of selected SPItems.
Definition object-set.h:255
bool isEmpty()
Returns true if no items are selected.
void toLayer(SPObject *layer)
Move selection to group moveto, after the last child of moveto (if it has any children).
Preference storage class.
Definition preferences.h:66
bool getBool(Glib::ustring const &pref_path, bool def=false)
Retrieve a Boolean value.
static Preferences * get()
Access the singleton Preferences object.
The set of selected SPObjects for a given document and layer model.
Definition selection.h:80
void add(XML::Node *repr)
Add an XML node's SPObject to the set of selected objects.
Definition selection.h:107
void set(XML::Node *repr)
Set the selection to an XML node's SPObject.
Definition selection.h:118
void remove(XML::Node *repr)
Removes an item from the set of selected objects.
Definition selection.h:131
SPObject * includesAncestor(XML::Node *repr)
Returns ancestor if the given object has ancestor selected.
Definition selection.h:149
bool includes(XML::Node *repr, bool anyAncestor=false)
Returns true if the given item is selected.
Definition selection.h:140
static Gtk::AccelKey get_from(GtkEventControllerKey const *controller, unsigned keyval, unsigned keycode, GdkModifierType state, bool fix=false)
Controller provides the group. It can be nullptr; if so, we use group 0.
DialogBase is the base class for the dialog system.
Definition dialog-base.h:42
Selection * getSelection() const
Definition dialog-base.h:84
virtual void update()
The update() method is essential to Gtk state management.
Definition dialog-base.h:57
SPDocument * getDocument() const
Definition dialog-base.h:83
SPDesktop * getDesktop() const
Definition dialog-base.h:79
A panel that displays objects.
Definition objects.h:92
UI::Widget::PopoverBin _popoverbin
Definition objects.h:166
Gtk::SearchEntry2 & _searchBox
Definition objects.h:161
void _activateAction(const std::string &layerAction, const std::string &selectionAction)
Special context-aware functions - If nothing is selected or layers-only mode is active,...
Definition objects.cpp:1236
Glib::RefPtr< Gdk::ContentProvider > on_prepare(Gtk::DragSource &controller, double x, double y)
Definition objects.cpp:1973
std::map< SPBlendMode, Glib::ustring > _blend_mode_names
Definition objects.h:221
bool hasDummyChildren(Gtk::TreeModel::ConstRow const &row) const
Return true if this row has dummy children.
Definition objects.cpp:1815
void layerChanged(SPObject *obj)
Happens when the layer selected is changed.
Definition objects.cpp:1217
Glib::RefPtr< Gtk::TreeStore > _store
Definition objects.h:121
Glib::RefPtr< Gtk::Builder > _builder
Definition objects.h:126
bool toggleLocked(Gdk::ModifierType state, Gtk::TreeModel::Row row)
Sets sensitivity of items in the tree.
Definition objects.cpp:1347
Gtk::TreeView::Column * _eye_column
Definition objects.h:155
void selectionChanged(Selection *selected) override
Definition objects.cpp:1163
bool toggleVisible(Gdk::ModifierType state, Gtk::TreeModel::Row row)
Sets visibility of items in the tree.
Definition objects.cpp:1253
std::unique_ptr< ObjectWatcher > root_watcher
Definition objects.h:128
ObjectWatcher * getWatcher(Inkscape::XML::Node *node)
Get the object watcher from the xml node (reverse lookup), it uses a ancesstor recursive pattern to m...
Definition objects.cpp:703
Inkscape::PrefObserver _watch_object_mode
Definition objects.h:127
Gtk::TreeView::Column * _lock_column
Definition objects.h:156
void selectRange(Gtk::TreeModel::Path start, Gtk::TreeModel::Path end)
Definition objects.cpp:2040
void on_drag_end(Glib::RefPtr< Gdk::Drag > const &drag, bool delete_data)
Definition objects.cpp:2035
Gtk::TreeView::Column * _blend_mode_column
Definition objects.h:154
bool selectCursorItem(Gdk::ModifierType state)
Select the object currently under the list-cursor (keyboard or mouse)
Definition objects.cpp:2086
bool on_tree_key_pressed(Gtk::EventControllerKey const &controller, unsigned keyval, unsigned keycode, Gdk::ModifierType state)
Handles keyboard events on the TreeView.
Definition objects.cpp:1382
Gtk::EventSequenceState on_click(Gtk::GestureClick const &gesture, int n_press, double x, double y, EventType type)
Handles mouse button click events.
Definition objects.cpp:1622
std::unique_ptr< ModelColumns > _model
Definition objects.h:122
Inkscape::UI::Widget::ImageToggler * _item_state_toggler
Definition objects.h:222
sigc::scoped_connection layer_changed
Definition objects.h:134
Gtk::CellRendererText * _text_renderer
Definition objects.h:152
bool on_window_key(Gtk::EventControllerKey const &controller, unsigned keyval, unsigned keycode, Gdk::ModifierType state, EventType type)
Definition objects.cpp:1469
Gtk::TreeModel::RowReference _hovered_row_ref
Definition objects.h:136
bool removeDummyChildren(Gtk::TreeModel::Row row)
If the given row has dummy children, remove them.
Definition objects.cpp:1831
UI::Widget::ColorNotebook * _color_selector
Definition objects.h:217
std::shared_ptr< Colors::ColorSet > _colors
Definition objects.h:216
std::vector< SPWeakPtr< SPObject > > _prev_range
Definition objects.h:132
sigc::scoped_connection _idle_connection
Definition objects.h:232
void on_drag_begin(Glib::RefPtr< Gdk::Drag > const &drag)
Definition objects.cpp:1993
bool showChildInTree(SPItem *item)
Apply any ongoing filters to the items.
Definition objects.cpp:1095
bool cleanDummyChildren(Gtk::TreeModel::Row row)
Definition objects.cpp:1851
void desktopReplaced() override
Called when the desktop has certainly changed.
Definition objects.cpp:1059
void _handleTransparentHover(bool enabled)
Definition objects.cpp:1581
bool colorTagPopup(int x, int y, Gtk::TreeModel::Row row)
Definition objects.cpp:1318
Gtk::TreeModel::Path _initial_path
Definition objects.h:130
bool select_row(Glib::RefPtr< Gtk::TreeModel > const &model, Gtk::TreeModel::Path const &path, bool b)
Take over the select row functionality from the TreeView, this is because we have two selections (lay...
Definition objects.cpp:1789
void _searchActivated()
User pressed return in search box, process search query.
Definition objects.cpp:2146
SPObject * getObject(Inkscape::XML::Node *node)
Get the object from the node.
Definition objects.cpp:690
void _handleEdited(const Glib::ustring &path, const Glib::ustring &new_text)
Handle a successful item label edit.
Definition objects.cpp:1767
sigc::scoped_connection _tree_style
Definition objects.h:164
void on_motion_motion(Gtk::EventControllerMotion const *controller, double x, double y)
Definition objects.cpp:1506
ObjectWatcher * unpackToObject(SPObject *item)
This both unpacks the tree, and populates lazy loading.
Definition objects.cpp:1141
bool on_drag_drop(Glib::ValueBase const &value, double x, double y)
Signal handler for "drag-drop".
Definition objects.cpp:1929
bool isDummy(Gtk::TreeModel::ConstRow const &row) const
Definition objects.h:116
Gtk::TreeView::Column * _color_tag_column
Definition objects.h:157
TreeViewWithCssChanged & _tree
Definition objects.h:151
Inkscape::XML::Node * getRepr(Gtk::TreeModel::ConstRow const &row) const
Get the XML node which is associated with a row.
Definition objects.cpp:1797
SPItem * getItem(Gtk::TreeModel::ConstRow const &row) const
Get the item which is associated with a row.
Definition objects.cpp:1806
Gtk::ScrolledWindow _scroller
Definition objects.h:162
Gtk::TreeView::Column * _name_column
Definition objects.h:153
Gdk::DragAction on_drag_motion(double x, double y)
Signal handler for "drag-motion".
Definition objects.cpp:1868
void on_motion_enter(double x, double y)
Handles mouse movements.
Definition objects.cpp:1493
std::map< SPBlendMode, Gtk::CheckButton * > _blend_items
Definition objects.h:220
Gtk::TreeViewColumn * _drag_column
Definition objects.h:225
bool blendModePopup(int x, int y, Gtk::TreeModel::Row row)
Definition objects.cpp:1285
void set_label(const Glib::ustring &label)
Glib::PropertyProxy< bool > property_activatable()
Glib::PropertyProxy< bool > property_active()
void set_force_visible(bool force_visible)
Sets whether to force visible icons in ALL cells of the column, EVEN IF their activatable & property_...
Glib::PropertyProxy< std::string > property_active_icon()
void setPopover(Gtk::Popover *popover)
Definition popover-bin.h:24
void setChild(Gtk::Widget *child)
Definition popover-bin.h:23
A class you can inherit to access GTK4ʼs Widget.css_changed & .focus vfuncs, missing in gtkmm4.
const unsigned int _length
Definition enums.h:114
const EnumData< E > & data(const unsigned int i) const
Definition enums.h:109
Interface for XML node observers.
Interface for refcounted XML nodes.
Definition node.h:80
virtual Node * parent()=0
Get the parent of this node.
virtual char const * name() const =0
Get the name of the element node.
virtual Node * firstChild()=0
Get the first child of this node.
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.
Inkscape::UI::Widget::Canvas * getCanvas() const
Definition desktop.h:190
Inkscape::MessageStack * messageStack() const
Definition desktop.h:160
Inkscape::LayerManager & layerManager()
Definition desktop.h:287
Inkscape::Display::TranslucencyGroup & getTranslucencyGroup() const
Definition desktop.h:297
SPRoot * getRoot()
Returns our SPRoot.
Definition document.h:200
SPObject * getObjectByRepr(Inkscape::XML::Node *repr) const
Base class for visual SVG elements.
Definition sp-item.h:109
bool isSensitive() const
Definition sp-item.h:174
void setHighlight(Inkscape::Colors::Color color)
Definition sp-item.cpp:275
virtual Inkscape::Colors::Color highlight_color() const
Definition sp-item.cpp:284
SPMask * getMaskObject() const
Definition sp-item.cpp:177
bool isHidden() const
Definition sp-item.cpp:242
void setLocked(bool lock, bool recursive=false)
Definition sp-item.cpp:228
void setHidden(bool hidden)
Definition sp-item.cpp:248
virtual const char * typeName() const
The item's type name, not node tag name.
Definition sp-item.cpp:1192
bool isExpanded() const
Definition sp-item.h:411
void setExpanded(bool expand)
Definition sp-item.h:412
SPClipPath * getClipObject() const
Definition sp-item.cpp:102
bool isHighlightSet() const
Definition sp-item.cpp:280
SPObject is an abstract base class of all of the document nodes at the SVG document level.
Definition sp-object.h:160
char const * label() const
Gets the author-visible label property for the object or a default if no label is defined.
std::vector< SPObject * > childList(bool add_ref, Action action=ActionGeneral)
Retrieves the children as a std vector object, optionally ref'ing the children in the process,...
SPDocument * document
Definition sp-object.h:188
char * id
Definition sp-object.h:192
char const * getId() const
Returns the objects current ID string.
void changeCSS(SPCSSAttr *css, char const *attr)
SPStyle * style
Represents the style properties, whether from presentation attributes, the style attribute,...
Definition sp-object.h:248
std::vector< SPObject * > ancestorList(bool root_to_tip)
Retrieves a list of ancestors of the object, as an easy to use vector.
char const * defaultLabel() const
Returns a default label property for this object.
SPObject * parent
Definition sp-object.h:189
void setLabel(char const *label)
Sets the author-visible label for this object.
Inkscape::XML::Node * getRepr()
Returns the XML representation of tree.
char const * getTagName() const
T< SPAttr::MIX_BLEND_MODE, SPIEnum< SPBlendMode > > mix_blend_mode
Definition style.h:220
T< SPAttr::OPACITY, SPIScale24 > opacity
opacity
Definition style.h:216
Utilities to more easily use Gtk::EventController & subclasses like Gesture.
Utility functions to convert ascii representations to numbers.
TODO: insert short description here.
std::shared_ptr< Css const > css
double c[8][4]
Editable view implementation.
static char const *const parent
Definition dir-util.cpp:70
TODO: insert short description here.
constexpr auto SP_DOCUMENT_UPDATE_PRIORITY
Definition document.cpp:93
unsigned int guint32
bool set_blend_mode(SPItem *item, SPBlendMode blend_mode)
Util::TreeifyResult const & _tree
Icon Loader.
Macro for icon names used in Inkscape.
SPItem * item
std::string original
Inkscape::XML::Node * node
Inkscape - An SVG editor.
void shift(T &a, T &b, T const &c)
Glib::ustring label
Raw stack of active status messages.
Definition desktop.h:50
auto use_state(Slot &&slot)
Definition controller.h:43
bool has_flag(Gdk::ModifierType const state, Gdk::ModifierType const flags)
Helper to query if ModifierType state contains one or more of given flag(s).
Definition controller.h:25
Dialog code.
Definition desktop.h:117
static auto get_cell_area(Gtk::TreeView const &tree_view, Gtk::TreeModel::Path const &path, Gtk::TreeViewColumn &column)
Definition objects.cpp:1602
static auto get_key(std::size_t const notebook_idx)
Gdk::RGBA selection_color
Definition objects.cpp:413
static auto get_cell_center(Gtk::TreeView const &tree_view, Gtk::TreeModel::Path const &path, Gtk::TreeViewColumn &column)
Definition objects.cpp:1610
Custom widgets.
Definition desktop.h:126
static void popup_at(Gtk::Popover &popover, Gtk::Widget &widget, double const x_offset, double const y_offset, int width, int height)
W & get_widget(const Glib::RefPtr< Gtk::Builder > &builder, const char *id)
W & get_derived_widget(const Glib::RefPtr< Gtk::Builder > &builder, const char *id, Args &&... args)
Glib::RefPtr< Gtk::Builder > create_builder(const char *filename)
std::string format_number(double val, unsigned int precision=3)
Definition converters.h:110
static void append(std::vector< T > &target, std::vector< T > &&source)
const Util::EnumDataConverter< SPBlendMode > SPBlendModeConverter
@ NORMAL_MESSAGE
Definition message.h:26
STL namespace.
New node tool with support for multiple path editing.
static cairo_user_data_key_t key
static double const HOVER_ALPHA
Definition objects.cpp:96
static double const SELECTED_ALPHA[16]
Definition objects.cpp:77
Helpers for using Gtk::Boxes, encapsulating large changes between GTK3 & GTK4.
Helpers to connect signals to events that popup a menu in both GTK3 and GTK4.
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_attr_unref(SPCSSAttr *css)
Unreferences an SPCSSAttr (will be garbage collected if no references remain).
Definition repr-css.cpp:76
void sp_repr_css_set_property(SPCSSAttr *css, gchar const *name, gchar const *value)
Set a style property to a new value (e.g.
Definition repr-css.cpp:191
GList * items
guint32 GQuark
SPRoot: SVG <svg> implementation.
static const Point data[]
Complete state of a selection, including selected objects and nodes.
Definition selection.h:57
SPStyle enums: named public enums that correspond to SVG property values.
@ SP_CSS_BLEND_NORMAL
SPStyle - a style object for SPItem objects.
double width
Glib::ustring name
Definition toolbars.cpp:55
Render some items as translucent in a document rendering stack.
Gdk::RGBA get_color_with_class(Gtk::Widget &widget, Glib::ustring const &css_class)
Definition util.cpp:303
Gdk::RGBA change_alpha(const Gdk::RGBA &color, double new_alpha)
Definition util.cpp:417
A class that can be inherited to access GTK4ʼs Widget.css_changed & .focus vfuncs,...