summaryrefslogtreecommitdiffstats
path: root/src/ui/dialog/objects.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/dialog/objects.cpp')
-rw-r--r--src/ui/dialog/objects.cpp1860
1 files changed, 1860 insertions, 0 deletions
diff --git a/src/ui/dialog/objects.cpp b/src/ui/dialog/objects.cpp
new file mode 100644
index 0000000..92f0892
--- /dev/null
+++ b/src/ui/dialog/objects.cpp
@@ -0,0 +1,1860 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A panel for listing objects in a document.
+ *
+ * Authors:
+ * Martin Owens
+ * Mike Kowalski
+ * Adam Belis (UX/Design)
+ *
+ * Copyright (C) Authors 2020-2022
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "objects.h"
+
+#include <glibmm/ustring.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/icontheme.h>
+#include <gtkmm/imagemenuitem.h>
+#include <gtkmm/modelbutton.h>
+#include <gtkmm/object.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/separatormenuitem.h>
+#include <glibmm/main.h>
+#include <glibmm/i18n.h>
+#include <iomanip>
+#include <pango/pango-utils.h>
+#include <string>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "filter-chemistry.h"
+#include "inkscape.h"
+#include "layer-manager.h"
+#include "message-stack.h"
+
+#include "actions/actions-tools.h"
+
+#include "include/gtkmm_version.h"
+
+#include "display/drawing-group.h"
+
+#include "object/filters/blend.h"
+#include "object/filters/gaussian-blur.h"
+#include "object/sp-clippath.h"
+#include "object/sp-mask.h"
+#include "object/sp-root.h"
+#include "object/sp-shape.h"
+#include "style-enums.h"
+#include "style.h"
+#include "svg/css-ostringstream.h"
+#include "ui/builder-utils.h"
+#include "ui/dialog-events.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/selected-color.h"
+#include "ui/shortcuts.h"
+#include "ui/tools/node-tool.h"
+
+#include "ui/contextmenu.h"
+#include "ui/util.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/filter-effect-chooser.h"
+#include "ui/widget/imagetoggler.h"
+#include "ui/widget/shapeicon.h"
+#include "ui/widget/objects-dialog-cells.h"
+#include "util/numeric/converters.h"
+
+// alpha (transparency) multipliers corresponding to item selection state combinations (SelectionState)
+// when 0 - do not color item's background
+static double const SELECTED_ALPHA[8] = {
+ 0.00, //0 not selected
+ 0.90, //1 selected
+ 0.50, //2 layer focused
+ 0.20, //3 layer focused & selected
+ 0.00, //4 child of focused layer
+ 0.90, //5 selected child of focused layer
+ 0.50, //6 2 and 4
+ 0.90 //7 1, 2 and 4
+};
+
+//#define DUMP_LAYERS 1
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class ObjectWatcher : public Inkscape::XML::NodeObserver
+{
+public:
+ ObjectWatcher() = delete;
+ ObjectWatcher(ObjectsPanel *panel, SPItem *, Gtk::TreeRow *row, bool is_filtered);
+ ~ObjectWatcher() override;
+
+ void initRowInfo();
+ void updateRowInfo();
+ void updateRowHighlight();
+ void updateRowAncestorState(bool invisible, bool locked);
+ void updateRowBg(guint32 rgba = 0.0);
+
+ ObjectWatcher *findChild(Node *node);
+ void addDummyChild();
+ bool addChild(SPItem *, bool dummy = true);
+ void addChildren(SPItem *, bool dummy = false);
+ void setSelectedBit(SelectionState mask, bool enabled);
+ void setSelectedBitRecursive(SelectionState mask, bool enabled);
+ void setSelectedBitChildren(SelectionState mask, bool enabled);
+ void rememberExtendedItems();
+ void moveChild(Node &child, Node *sibling);
+ bool isFiltered() const { return is_filtered; }
+
+ Gtk::TreeNodeChildren getChildren() const;
+ Gtk::TreeIter getChildIter(Node *) const;
+
+ void notifyChildAdded(Node &, Node &, Node *) override;
+ void notifyChildRemoved(Node &, Node &, Node *) override;
+ void notifyChildOrderChanged(Node &, Node &child, Node *, Node *) override;
+ void notifyAttributeChanged(Node &, GQuark, Util::ptr_shared, Util::ptr_shared) override;
+
+ /// Associate this watcher with a tree row
+ void setRow(const Gtk::TreeModel::Path &path)
+ {
+ assert(path);
+ row_ref = Gtk::TreeModel::RowReference(panel->_store, path);
+ }
+ void setRow(const Gtk::TreeModel::Row &row)
+ {
+ setRow(panel->_store->get_path(row));
+ }
+
+ // Get the path out of this watcher
+ Gtk::TreeModel::Path getTreePath() const {
+ return row_ref.get_path();
+ }
+
+ /// True if this watchr has a valid row reference.
+ bool hasRow() const { return bool(row_ref); }
+
+ /// Transfer a child watcher to its new parent
+ void transferChild(Node *childnode)
+ {
+ auto *target = panel->getWatcher(childnode->parent());
+ assert(target != this);
+ auto nh = child_watchers.extract(childnode);
+ assert(nh);
+ bool inserted = target->child_watchers.insert(std::move(nh)).inserted;
+ assert(inserted);
+ }
+
+ /// The XML node associated with this watcher.
+ Node *getRepr() const { return node; }
+ std::optional<Gtk::TreeRow> getRow() const {
+ if (auto path = row_ref.get_path()) {
+ if(auto iter = panel->_store->get_iter(path)) {
+ return *iter;
+ }
+ }
+ return std::nullopt;
+ }
+
+ std::unordered_map<Node const *, std::unique_ptr<ObjectWatcher>> child_watchers;
+
+private:
+ Node *node;
+ Gtk::TreeModel::RowReference row_ref;
+ ObjectsPanel *panel;
+ SelectionState selection_state;
+ bool is_filtered;
+};
+
+class ObjectsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord
+{
+public:
+ ModelColumns()
+ {
+ add(_colNode);
+ add(_colLabel);
+ add(_colType);
+ add(_colIconColor);
+ add(_colClipMask);
+ add(_colBgColor);
+ add(_colInvisible);
+ add(_colLocked);
+ add(_colAncestorInvisible);
+ add(_colAncestorLocked);
+ add(_colHover);
+ add(_colItemStateSet);
+ add(_colBlendMode);
+ add(_colOpacity);
+ add(_colItemState);
+ add(_colHoverColor);
+ }
+ ~ModelColumns() override = default;
+ Gtk::TreeModelColumn<Node*> _colNode;
+ Gtk::TreeModelColumn<Glib::ustring> _colLabel;
+ Gtk::TreeModelColumn<Glib::ustring> _colType;
+ Gtk::TreeModelColumn<unsigned int> _colIconColor;
+ Gtk::TreeModelColumn<unsigned int> _colClipMask;
+ Gtk::TreeModelColumn<Gdk::RGBA> _colBgColor;
+ Gtk::TreeModelColumn<bool> _colInvisible;
+ Gtk::TreeModelColumn<bool> _colLocked;
+ Gtk::TreeModelColumn<bool> _colAncestorInvisible;
+ Gtk::TreeModelColumn<bool> _colAncestorLocked;
+ Gtk::TreeModelColumn<bool> _colHover;
+ Gtk::TreeModelColumn<bool> _colItemStateSet;
+ Gtk::TreeModelColumn<SPBlendMode> _colBlendMode;
+ Gtk::TreeModelColumn<double> _colOpacity;
+ Gtk::TreeModelColumn<Glib::ustring> _colItemState;
+ // Set when hovering over the color tag cell
+ Gtk::TreeModelColumn<bool> _colHoverColor;
+};
+
+/**
+ * Creates a new ObjectWatcher, a gtk TreeView iterated watching device.
+ *
+ * @param panel The panel to which the object watcher belongs
+ * @param obj The SPItem to watch in the document
+ * @param row The optional list store tree row for the item,
+ if not provided, assumes this is the root 'document' object.
+ * @param filtered, if true this watcher will filter all chldren using the panel filtering function on each item to decide if it should be shown.
+ */
+ObjectWatcher::ObjectWatcher(ObjectsPanel* panel, SPItem* obj, Gtk::TreeRow *row, bool filtered)
+ : panel(panel)
+ , row_ref()
+ , selection_state(0)
+ , is_filtered(filtered)
+ , node(obj->getRepr())
+{
+ if(row != nullptr) {
+ assert(row->children().empty());
+ setRow(*row);
+ initRowInfo();
+ updateRowInfo();
+ }
+ node->addObserver(*this);
+
+ // Only show children for groups (and their subclasses like SPAnchor or SPRoot)
+ if (!is<SPGroup>(obj)) {
+ return;
+ }
+
+ // Add children as a dummy row to avoid excensive execution when
+ // the tree is really large, but not in layers mode.
+ addChildren(obj, (bool)row && !obj->isExpanded());
+}
+ObjectWatcher::~ObjectWatcher()
+{
+ node->removeObserver(*this);
+ Gtk::TreeModel::Path path;
+ if (bool(row_ref) && (path = row_ref.get_path())) {
+ if (auto iter = panel->_store->get_iter(path)) {
+ panel->_store->erase(iter);
+ }
+ }
+ child_watchers.clear();
+}
+
+void ObjectWatcher::initRowInfo()
+{
+ auto _model = panel->_model;
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colHover] = false;
+}
+
+/**
+ * Update the information in the row from the stored node
+ */
+void ObjectWatcher::updateRowInfo()
+{
+ if (auto item = cast<SPItem>(panel->getObject(node))) {
+ assert(row_ref);
+ assert(row_ref.get_path());
+
+ auto _model = panel->_model;
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colNode] = node;
+
+ // show ids without "#"
+ char const *id = item->getId();
+ row[_model->_colLabel] = (id && !item->label()) ? id : item->defaultLabel();
+
+ row[_model->_colType] = item->typeName();
+ row[_model->_colClipMask] =
+ (item->getClipObject() ? Inkscape::UI::Widget::OVERLAY_CLIP : 0) |
+ (item->getMaskObject() ? Inkscape::UI::Widget::OVERLAY_MASK : 0);
+ row[_model->_colInvisible] = item->isHidden();
+ row[_model->_colLocked] = !item->isSensitive();
+ auto blend = item->style && item->style->mix_blend_mode.set ? item->style->mix_blend_mode.value : SP_CSS_BLEND_NORMAL;
+ row[_model->_colBlendMode] = blend;
+ auto opacity = 1.0;
+ if (item->style && item->style->opacity.set) {
+ opacity = SP_SCALE24_TO_FLOAT(item->style->opacity.value);
+ }
+ row[_model->_colOpacity] = opacity;
+ std::string item_state;
+ if (opacity == 0.0) {
+ item_state = "object-transparent";
+ }
+ else if (blend != SP_CSS_BLEND_NORMAL) {
+ item_state = opacity == 1.0 ? "object-blend-mode" : "object-translucent-blend-mode";
+ }
+ else if (opacity < 1.0) {
+ item_state = "object-translucent";
+ }
+ row[_model->_colItemState] = item_state;
+ row[_model->_colItemStateSet] = !item_state.empty();
+
+ updateRowHighlight();
+ updateRowAncestorState(row[_model->_colAncestorInvisible], row[_model->_colAncestorLocked]);
+ }
+}
+
+/**
+ * Propagate changes to the highlight color to all children.
+ */
+void ObjectWatcher::updateRowHighlight() {
+ if (auto item = cast<SPItem>(panel->getObject(node))) {
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ auto new_color = item->highlight_color();
+ if (new_color != row[panel->_model->_colIconColor]) {
+ row[panel->_model->_colIconColor] = new_color;
+ updateRowBg(new_color);
+ for (auto &watcher : child_watchers) {
+ watcher.second->updateRowHighlight();
+ }
+ }
+ }
+}
+
+/**
+ * Propagate a change in visibility or locked state to all children
+ */
+void ObjectWatcher::updateRowAncestorState(bool invisible, bool locked) {
+ auto _model = panel->_model;
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colAncestorInvisible] = invisible;
+ row[_model->_colAncestorLocked] = locked;
+ for (auto &watcher : child_watchers) {
+ watcher.second->updateRowAncestorState(
+ invisible || row[_model->_colInvisible],
+ locked || row[_model->_colLocked]);
+ }
+}
+
+Gdk::RGBA selection_color;
+
+/**
+ * Updates the row's background colour as indicated by its selection.
+ */
+void ObjectWatcher::updateRowBg(guint32 rgba)
+{
+ assert(row_ref);
+ if (auto row = *panel->_store->get_iter(row_ref.get_path())) {
+ auto alpha = SELECTED_ALPHA[selection_state];
+ if (alpha == 0.0) {
+ row[panel->_model->_colBgColor] = Gdk::RGBA();
+ return;
+ }
+
+ const auto& sel = selection_color;
+ auto gdk_color = Gdk::RGBA();
+ gdk_color.set_red(sel.get_red());
+ gdk_color.set_green(sel.get_green());
+ gdk_color.set_blue(sel.get_blue());
+ gdk_color.set_alpha(sel.get_alpha() * alpha);
+ row[panel->_model->_colBgColor] = gdk_color;
+ }
+}
+
+/**
+ * Flip the selected state bit on or off as needed, calls updateRowBg if changed.
+ *
+ * @param mask - The selection bit to set or unset
+ * @param enabled - If the bit should be set or unset
+ */
+void ObjectWatcher::setSelectedBit(SelectionState mask, bool enabled) {
+ if (!row_ref) return;
+ SelectionState value = selection_state;
+ SelectionState original = value;
+ if (enabled) {
+ value |= mask;
+ } else {
+ value &= ~mask;
+ }
+ if (value != original) {
+ selection_state = value;
+ updateRowBg();
+ }
+}
+
+/**
+ * Flip the selected state bit on or off as needed, on this watcher and all
+ * its direct and indirect children.
+ */
+void ObjectWatcher::setSelectedBitRecursive(SelectionState mask, bool enabled)
+{
+ setSelectedBit(mask, enabled);
+ setSelectedBitChildren(mask, enabled);
+}
+void ObjectWatcher::setSelectedBitChildren(SelectionState mask, bool enabled)
+{
+ for (auto &pair : child_watchers) {
+ pair.second->setSelectedBitRecursive(mask, enabled);
+ }
+}
+
+/**
+ * Keep expanded rows expanded and recurse through all children.
+ */
+void ObjectWatcher::rememberExtendedItems()
+{
+ if (auto item = cast<SPItem>(panel->getObject(node))) {
+ if (item->isExpanded())
+ panel->_tree.expand_row(row_ref.get_path(), false);
+ }
+ for (auto &pair : child_watchers) {
+ pair.second->rememberExtendedItems();
+ }
+}
+
+/**
+ * Find the child watcher for the given node.
+ */
+ObjectWatcher *ObjectWatcher::findChild(Node *node)
+{
+ auto it = child_watchers.find(node);
+ if (it != child_watchers.end()) {
+ return it->second.get();
+ }
+ return nullptr;
+}
+
+/**
+ * Add the child object to this node.
+ *
+ * @param child - SPObject to be added
+ * @param dummy - Add a dummy objects (hidden) instead
+ *
+ * @returns true if child added was a dummy objects
+ */
+bool ObjectWatcher::addChild(SPItem *child, bool dummy)
+{
+ if (is_filtered && !panel->showChildInTree(child)) {
+ return false;
+ }
+
+ auto const children = getChildren();
+ if (!is_filtered && dummy && row_ref) {
+ if (children.empty()) {
+ auto const iter = panel->_store->append(children);
+ assert(panel->isDummy(*iter));
+ return true;
+ } else if (panel->isDummy(children[0])) {
+ return false;
+ }
+ }
+
+ auto *node = child->getRepr();
+ assert(node);
+ Gtk::TreeModel::Row row = *(panel->_store->prepend(children));
+
+ // Ancestor states are handled inside the list store (so we don't have to re-ask every update)
+ auto _model = panel->_model;
+ if (row_ref) {
+ auto parent_row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colAncestorInvisible] = parent_row[_model->_colAncestorInvisible] || parent_row[_model->_colInvisible];
+ row[_model->_colAncestorLocked] = parent_row[_model->_colAncestorLocked] || parent_row[_model->_colLocked];
+ } else {
+ row[_model->_colAncestorInvisible] = false;
+ row[_model->_colAncestorLocked] = false;
+ }
+
+ auto &watcher = child_watchers[node];
+ assert(!watcher);
+ watcher.reset(new ObjectWatcher(panel, child, &row, is_filtered));
+
+ // Make sure new children have the right focus set.
+ if ((selection_state & LAYER_FOCUSED) != 0) {
+ watcher->setSelectedBit(LAYER_FOCUS_CHILD, true);
+ }
+ return false;
+}
+
+/**
+ * Add all SPItem children as child rows.
+ */
+void ObjectWatcher::addChildren(SPItem *obj, bool dummy)
+{
+ assert(child_watchers.empty());
+
+ for (auto &child : obj->children) {
+ if (auto item = cast<SPItem>(&child)) {
+ if (addChild(item, dummy) && dummy) {
+ // one dummy child is enough to make the group expandable
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Move the child to just after the given sibling
+ *
+ * @param child - SPObject to be moved
+ * @param sibling - Optional sibling Object to add next to, if nullptr the
+ * object is moved to BEFORE the first item.
+ */
+void ObjectWatcher::moveChild(Node &child, Node *sibling)
+{
+ auto child_iter = getChildIter(&child);
+ if (!child_iter)
+ return; // This means the child was never added, probably not an SPItem.
+
+ // sibling might not be an SPItem and thus not be represented in the
+ // TreeView. Find the closest SPItem and use that for the reordering.
+ while (sibling && !is<SPItem>(panel->getObject(sibling))) {
+ sibling = sibling->prev();
+ }
+
+ auto sibling_iter = getChildIter(sibling);
+ panel->_store->move(child_iter, sibling_iter);
+}
+
+/**
+ * Get the TreeRow's children iterator
+ *
+ * @returns Gtk Tree Node Children iterator
+ */
+Gtk::TreeNodeChildren ObjectWatcher::getChildren() const
+{
+ Gtk::TreeModel::Path path;
+ if (row_ref && (path = row_ref.get_path())) {
+ return panel->_store->get_iter(path)->children();
+ }
+ assert(!row_ref);
+ return panel->_store->children();
+}
+
+/**
+ * Convert SPObject to TreeView Row, assuming the object is a child.
+ *
+ * @param child - The child object to find in this branch
+ * @returns Gtk TreeRow for the child, or end() if not found
+ */
+Gtk::TreeIter ObjectWatcher::getChildIter(Node *node) const
+{
+ auto childrows = getChildren();
+
+ if (!node) {
+ return childrows.end();
+ }
+
+ // Note: TreeRow inherits from TreeIter, so this `row` variable is
+ // also an iterator and a valid return value.
+ for (auto &row : childrows) {
+ if (panel->getRepr(row) == node) {
+ return row;
+ }
+ }
+ // In layer mode, we will come here for all non-layers
+ return childrows.begin();
+}
+
+void ObjectWatcher::notifyChildAdded( Node &node, Node &child, Node *prev )
+{
+ assert(this->node == &node);
+ // Ignore XML nodes which are not displayable items
+ if (auto item = cast<SPItem>(panel->getObject(&child))) {
+ addChild(item);
+ moveChild(child, prev);
+ }
+}
+void ObjectWatcher::notifyChildRemoved( Node &node, Node &child, Node* /*prev*/ )
+{
+ assert(this->node == &node);
+
+ if (child_watchers.erase(&child) > 0) {
+ return;
+ }
+
+ if (node.firstChild() == nullptr) {
+ assert(row_ref);
+ auto iter = panel->_store->get_iter(row_ref.get_path());
+ panel->removeDummyChildren(*iter);
+ }
+}
+void ObjectWatcher::notifyChildOrderChanged( Node &parent, Node &child, Node */*old_prev*/, Node *new_prev )
+{
+ assert(this->node == &parent);
+
+ moveChild(child, new_prev);
+}
+void ObjectWatcher::notifyAttributeChanged( Node &node, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ )
+{
+ assert(this->node == &node);
+
+ // The root <svg> node doesn't have a row
+ if (this == panel->getRootWatcher()) {
+ return;
+ }
+
+ // Almost anything could change the icon, so update upon any change, defer for lots of updates.
+
+ // examples of not-so-obvious cases:
+ // - width/height: Can change type "circle" to an "ellipse"
+
+ static std::set<GQuark> const excluded{
+ g_quark_from_static_string("transform"),
+ g_quark_from_static_string("x"),
+ g_quark_from_static_string("y"),
+ g_quark_from_static_string("d"),
+ g_quark_from_static_string("sodipodi:nodetypes"),
+ };
+
+ if (excluded.count(name)) {
+ return;
+ }
+
+ updateRowInfo();
+}
+
+
+/**
+ * Get the object from the node.
+ *
+ * @param node - XML Node involved in the signal.
+ * @returns SPObject matching the node, returns nullptr if not found.
+ */
+SPObject *ObjectsPanel::getObject(Node *node) {
+ if (node != nullptr && getDocument())
+ return getDocument()->getObjectByRepr(node);
+ return nullptr;
+}
+
+/**
+ * Get the object watcher from the xml node (reverse lookup), it uses a ancesstor
+ * recursive pattern to match up with the root_watcher.
+ *
+ * @param node - The node to look up.
+ * @return the ObjectWatcher object if it's possible to find.
+ */
+ObjectWatcher* ObjectsPanel::getWatcher(Node *node)
+{
+ assert(node);
+ if (root_watcher->getRepr() == node) {
+ return root_watcher;
+ } else if (node->parent()) {
+ if (auto parent_watcher = getWatcher(node->parent())) {
+ return parent_watcher->findChild(node);
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Constructor
+ */
+ObjectsPanel::ObjectsPanel()
+ : DialogBase("/dialogs/objects", "Objects")
+ , root_watcher(nullptr)
+ , _model(new ModelColumns())
+ , _layer(nullptr)
+ , _is_editing(false)
+ , _page(Gtk::ORIENTATION_VERTICAL)
+ , _color_picker(_("Highlight color"), "", 0, true)
+ , _builder(create_builder("dialog-objects.glade"))
+ , _settings_menu(get_widget<Gtk::Popover>(_builder, "settings-menu"))
+ , _object_menu(get_widget<Gtk::Popover>(_builder, "object-menu"))
+ , _searchBox(get_widget<Gtk::SearchEntry>(_builder, "search"))
+ , _opacity_slider(get_widget<Gtk::Scale>(_builder, "opacity-slider"))
+ , _setting_layers(get_derived_widget<PrefCheckButton, Glib::ustring, bool>(_builder, "setting-layers", "/dialogs/objects/layers_only", false))
+ , _setting_track(get_derived_widget<PrefCheckButton, Glib::ustring, bool>(_builder, "setting-track", "/dialogs/objects/expand_to_layer", true))
+{
+ _store = Gtk::TreeStore::create(*_model);
+ _color_picker.hide();
+
+ //Set up the tree
+ _tree.set_model(_store);
+ _tree.set_headers_visible(false);
+ // Reorderable means that we allow drag-and-drop, but we only allow that
+ // when at least one row is selected
+ _tree.enable_model_drag_dest (Gdk::ACTION_MOVE);
+ _tree.set_name("ObjectsTreeView");
+
+ auto& header = get_widget<Gtk::Box>(_builder, "header");
+ // Search
+ _searchBox.signal_activate().connect(sigc::mem_fun(*this, &ObjectsPanel::_searchActivated));
+ _searchBox.signal_search_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_searchChanged));
+
+ //Label
+ _name_column = Gtk::manage(new Gtk::TreeViewColumn());
+ _text_renderer = Gtk::manage(new Gtk::CellRendererText());
+ _text_renderer->property_editable() = true;
+ _text_renderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END);
+ _text_renderer->signal_editing_started().connect([=](Gtk::CellEditable*,const Glib::ustring&){
+ _is_editing = true;
+ });
+ _text_renderer->signal_editing_canceled().connect([=](){
+ _is_editing = false;
+ });
+ _text_renderer->signal_edited().connect([=](const Glib::ustring&,const Glib::ustring&){
+ _is_editing = false;
+ });
+
+ const int icon_col_width = 24;
+ auto icon_renderer = Gtk::manage(new Inkscape::UI::Widget::CellRendererItemIcon());
+ icon_renderer->property_xpad() = 2;
+ icon_renderer->property_width() = icon_col_width;
+ _tree.append_column(*_name_column);
+ _name_column->set_expand(true);
+ _name_column->pack_start(*icon_renderer, false);
+ _name_column->pack_start(*_text_renderer, true);
+ _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel);
+ _name_column->add_attribute(_text_renderer->property_cell_background_rgba(), _model->_colBgColor);
+ _name_column->add_attribute(icon_renderer->property_shape_type(), _model->_colType);
+ _name_column->add_attribute(icon_renderer->property_color(), _model->_colIconColor);
+ _name_column->add_attribute(icon_renderer->property_clipmask(), _model->_colClipMask);
+ _name_column->add_attribute(icon_renderer->property_cell_background_rgba(), _model->_colBgColor);
+
+ // blend mode and opacity icon(s)
+ _item_state_toggler = Gtk::manage(new Inkscape::UI::Widget::ImageToggler(
+ INKSCAPE_ICON("object-blend-mode"), INKSCAPE_ICON("object-opaque")));
+ int modeColNum = _tree.append_column("mode", *_item_state_toggler) - 1;
+ if (auto col = _tree.get_column(modeColNum)) {
+ col->add_attribute(_item_state_toggler->property_active(), _model->_colItemStateSet);
+ col->add_attribute(_item_state_toggler->property_active_icon(), _model->_colItemState);
+ col->add_attribute(_item_state_toggler->property_cell_background_rgba(), _model->_colBgColor);
+ col->add_attribute(_item_state_toggler->property_activatable(), _model->_colHover);
+ col->set_fixed_width(icon_col_width);
+ _blend_mode_column = col;
+ }
+
+ _tree.set_has_tooltip();
+ _tree.signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltip){
+ Gtk::TreeModel::iterator iter;
+ if (!_tree.get_tooltip_context_iter(x, y, kbd, iter) || !iter) {
+ return false;
+ }
+ auto blend = (*iter)[_model->_colBlendMode];
+ auto opacity = (*iter)[_model->_colOpacity];
+ auto templt = !pango_version_check(1, 50, 0) ?
+ "<span>%1 %2%%\n</span><span line_height=\"0.5\">\n</span><span>%3\n<i>%4</i></span>" :
+ "<span>%1 %2%%\n</span><span>\n</span><span>%3\n<i>%4</i></span>";
+ auto label = Glib::ustring::compose(templt,
+ _("Opacity:"), Util::format_number(opacity * 100.0, 1),
+ _("Blend mode:"), _blend_mode_names[blend]
+ );
+ tooltip->set_markup(label);
+ _tree.set_tooltip_cell(tooltip, nullptr, _blend_mode_column, _item_state_toggler);
+ return true;
+ });
+
+ _object_menu.set_relative_to(_tree);
+ _object_menu.signal_closed().connect([=](){ _item_state_toggler->set_active(false); _tree.queue_draw(); });
+ auto& modes = get_widget<Gtk::Grid>(_builder, "modes");
+ _opacity_slider.signal_format_value().connect([](double val){
+ return Util::format_number(val, 1) + "%";
+ });
+ const int min = 0, max = 100;
+ for (int i = min; i <= max; i += 50) {
+ _opacity_slider.add_mark(i, Gtk::POS_BOTTOM, "");
+ }
+ _opacity_slider.signal_value_changed().connect([=](){
+ if (current_item) {
+ auto value = _opacity_slider.get_value() / 100.0;
+ Inkscape::CSSOStringStream os;
+ os << CLAMP(value, 0.0, 1.0);
+ auto css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "opacity", os.str().c_str());
+ current_item->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::maybeDone(current_item->document, ":opacity", _("Change opacity"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ });
+
+ // object blend mode and opacity popup
+ int top = 0;
+ int left = 0;
+ int width = 2;
+ for (size_t i = 0; i < Inkscape::SPBlendModeConverter._length; ++i) {
+ auto& data = Inkscape::SPBlendModeConverter.data(i);
+ auto label = _blend_mode_names[data.id] = g_dpgettext2(nullptr, "BlendMode", data.label.c_str());
+ if (Inkscape::SPBlendModeConverter.get_key(data.id) == "-") {
+ if (top >= (Inkscape::SPBlendModeConverter._length + 1) / 2) {
+ ++left;
+ top = 2;
+ } else if (!left) {
+ auto sep = Gtk::make_managed<Gtk::Separator>();
+ sep->show();
+ modes.attach(*sep, left, top, 2, 1);
+ }
+ } else {
+ // Manual correction that indicates this should all be done in glade
+ if (left == 1 && top == 9)
+ top++;
+
+ auto check = Gtk::make_managed<Gtk::ModelButton>();
+ check->set_label(label);
+ check->property_role().set_value(Gtk::BUTTON_ROLE_RADIO);
+ check->property_inverted().set_value(true);
+ check->property_centered().set_value(false);
+ check->set_halign(Gtk::ALIGN_START);
+ check->signal_clicked().connect([=](){
+ // set blending mode
+ if (set_blend_mode(current_item, data.id)) {
+ for (auto btn : _blend_items) {
+ btn.second->property_active().set_value(btn.first == data.id);
+ }
+ DocumentUndo::done(getDocument(), "set-blend-mode", _("Change blend mode"));
+ }
+ });
+ _blend_items[data.id] = check;
+ _blend_mode_names[data.id] = label;
+ check->show();
+ modes.attach(*check, left, top, width, 1);
+ width = 1; // First element takes whole width
+ }
+ top++;
+ }
+
+ // Visible icon
+ auto *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler(
+ INKSCAPE_ICON("object-hidden"), INKSCAPE_ICON("object-visible")));
+ int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1;
+ if (auto eye = _tree.get_column(visibleColNum)) {
+ eye->add_attribute(eyeRenderer->property_active(), _model->_colInvisible);
+ eye->add_attribute(eyeRenderer->property_cell_background_rgba(), _model->_colBgColor);
+ eye->add_attribute(eyeRenderer->property_activatable(), _model->_colHover);
+ eye->add_attribute(eyeRenderer->property_gossamer(), _model->_colAncestorInvisible);
+ eye->set_fixed_width(icon_col_width);
+ _eye_column = eye;
+ }
+
+ // Unlocked icon
+ Inkscape::UI::Widget::ImageToggler * lockRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler(
+ INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")));
+ int lockedColNum = _tree.append_column("lock", *lockRenderer) - 1;
+ if (auto lock = _tree.get_column(lockedColNum)) {
+ lock->add_attribute(lockRenderer->property_active(), _model->_colLocked);
+ lock->add_attribute(lockRenderer->property_cell_background_rgba(), _model->_colBgColor);
+ lock->add_attribute(lockRenderer->property_activatable(), _model->_colHover);
+ lock->add_attribute(lockRenderer->property_gossamer(), _model->_colAncestorLocked);
+ lock->set_fixed_width(icon_col_width);
+ _lock_column = lock;
+ }
+
+ // hierarchy indicator - using item's layer highlight color
+ auto tag_renderer = Gtk::manage(new Inkscape::UI::Widget::ColorTagRenderer());
+ int tag_column = _tree.append_column("tag", *tag_renderer) - 1;
+ if (auto tag = _tree.get_column(tag_column)) {
+ tag->add_attribute(tag_renderer->property_color(), _model->_colIconColor);
+ tag->add_attribute(tag_renderer->property_hover(), _model->_colHoverColor);
+ tag->set_fixed_width(tag_renderer->get_width());
+ _color_tag_column = tag;
+ }
+ tag_renderer->signal_clicked().connect([=](const Glib::ustring& path) {
+ // object's color indicator clicked - open color picker
+ _clicked_item_row = *_store->get_iter(path);
+ if (auto item = getItem(_clicked_item_row)) {
+ // find object's color
+ _color_picker.setRgba32(item->highlight_color());
+ _color_picker.open();
+ }
+ });
+
+ _color_picker.connectChanged([=](guint rgba) {
+ if (auto item = getItem(_clicked_item_row)) {
+ item->setHighlight(rgba);
+ DocumentUndo::maybeDone(getDocument(), "highlight-color", _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ });
+
+ //Set the expander columns and search columns
+ _tree.set_expander_column(*_name_column);
+ _tree.set_search_column(-1);
+ _tree.set_enable_search(false);
+ _tree.get_selection()->set_mode(Gtk::SELECTION_NONE);
+
+ //Set up tree signals
+ _tree.signal_button_press_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false);
+ _tree.signal_button_release_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false);
+ _tree.signal_key_press_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleKeyPress), false);
+ _tree.signal_key_release_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleKeyEvent), false);
+ _tree.signal_motion_notify_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleMotionEvent), false);
+
+ // Set a status bar text when entering the widget
+ _tree.signal_enter_notify_event().connect([=](GdkEventCrossing*){
+ _msg_id = getDesktop()->messageStack()->push(Inkscape::NORMAL_MESSAGE,
+ _("<b>Hold ALT</b> while hovering over item to highlight, <b>hold SHIFT</b> and click to hide/lock all."));
+ return false;
+ }, false);
+ // watch mouse leave too to clear any state.
+ _tree.signal_leave_notify_event().connect([=](GdkEventCrossing*){
+ getDesktop()->messageStack()->cancel(_msg_id);
+ return _handleMotionEvent(nullptr);
+ }, false);
+
+ // Before expanding a row, replace the dummy child with the actual children
+ _tree.signal_test_expand_row().connect([this](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
+ if (cleanDummyChildren(*iter)) {
+ if (getSelection()) {
+ _selectionChanged();
+ }
+ }
+ return false;
+ });
+ _tree.signal_row_expanded().connect([=](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
+ if (auto item = getItem(*iter)) {
+ item->setExpanded(true);
+ }
+ });
+ _tree.signal_row_collapsed().connect([=](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
+ if (auto item = getItem(*iter)) {
+ item->setExpanded(false);
+ }
+ });
+
+ _tree.signal_drag_motion().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_motion), false);
+ _tree.signal_drag_drop().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_drop), false);
+ _tree.signal_drag_begin().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_start), false);
+ _tree.signal_drag_end().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_end), false);
+
+ //Set up the label editing signals
+ _text_renderer->signal_edited().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleEdited));
+
+ //Set up the scroller window and pack the page
+ // turn off overlay scrollbars - they block access to the 'lock' icon
+ _scroller.set_overlay_scrolling(false);
+ _scroller.add(_tree);
+ _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC );
+ _scroller.set_shadow_type(Gtk::SHADOW_IN);
+ Gtk::Requisition sreq;
+ Gtk::Requisition sreq_natural;
+ _scroller.get_preferred_size(sreq_natural, sreq);
+ int minHeight = 70;
+ if (sreq.height < minHeight) {
+ // Set a min height to see the layers when used with Ubuntu liboverlay-scrollbar
+ _scroller.set_size_request(sreq.width, minHeight);
+ }
+
+ _page.pack_start(header, false, true);
+ _page.pack_end(_scroller, Gtk::PACK_EXPAND_WIDGET);
+ pack_start(_page, Gtk::PACK_EXPAND_WIDGET);
+
+ selection_color = get_background_color(_tree.get_style_context(), Gtk::STATE_FLAG_SELECTED);
+ _tree_style = _tree.signal_style_updated().connect([=](){
+ selection_color = get_background_color(_tree.get_style_context(), Gtk::STATE_FLAG_SELECTED);
+
+ if (!root_watcher) return;
+ for (auto&& kv : root_watcher->child_watchers) {
+ if (kv.second) {
+ kv.second->updateRowHighlight();
+ }
+ }
+ });
+ // Clear and update entire tree (do not use this in changed/modified signals)
+ auto prefs = Inkscape::Preferences::get();
+ _watch_object_mode = prefs->createObserver("/dialogs/objects/layers_only", [=]() { setRootWatcher(); });
+
+ update();
+ show_all_children();
+}
+
+/**
+ * Destructor
+ */
+ObjectsPanel::~ObjectsPanel()
+{
+ if (root_watcher) {
+ delete root_watcher;
+ }
+ root_watcher = nullptr;
+
+ if (_model) {
+ delete _model;
+ _model = nullptr;
+ }
+}
+
+void ObjectsPanel::desktopReplaced()
+{
+ layer_changed.disconnect();
+
+ if (auto desktop = getDesktop()) {
+ layer_changed = desktop->layerManager().connectCurrentLayerChanged(sigc::mem_fun(*this, &ObjectsPanel::layerChanged));
+ }
+}
+
+void ObjectsPanel::documentReplaced()
+{
+ setRootWatcher();
+}
+
+void ObjectsPanel::setRootWatcher()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (root_watcher) {
+ delete root_watcher;
+ }
+ root_watcher = nullptr;
+
+ if (auto document = getDocument()) {
+ bool filtered = prefs->getBool("/dialogs/objects/layers_only", false) || _searchBox.get_text_length();
+
+ // A filtered object watcher behaves differently to an unfiltered one.
+ // Filtering disables creating dummy children and instead processes entire trees.
+ root_watcher = new ObjectWatcher(this, document->getRoot(), nullptr, filtered);
+ root_watcher->rememberExtendedItems();
+ layerChanged(getDesktop()->layerManager().currentLayer());
+ _selectionChanged();
+ }
+}
+
+/**
+ * Apply any ongoing filters to the items.
+ */
+bool ObjectsPanel::showChildInTree(SPItem *item) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ bool show_child = true;
+
+ // Filter by object type, the layers dialog here.
+ if (prefs->getBool("/dialogs/objects/layers_only", false)) {
+ auto group = cast<SPGroup>(item);
+ if (!group || group->layerMode() != SPGroup::LAYER) {
+ show_child = false;
+ }
+ }
+
+ // Filter by text search, if the search text box has any contents
+ auto term = _searchBox.get_text().lowercase();
+ if (show_child && term.length()) {
+ // A source document allows search for different pieces of metadata
+ std::stringstream source;
+ source << "#" << item->getId();
+ if (auto label = item->label())
+ source << " " << label;
+ source << " @" << item->getTagName();
+ // Might want to add class names here as ".class"
+
+ auto doc = source.str();
+ transform(doc.begin(), doc.end(), doc.begin(), ::tolower);
+ show_child = doc.find(term) != std::string::npos;
+ }
+
+ // Now the terrible bit, searching all the children causing a
+ // duplication of work as it must re-scan up the tree multiple times
+ // when the tree is very deep.
+ for (auto child_obj : item->childList(false)) {
+ if (show_child)
+ break;
+ if (auto child = cast<SPItem>(child_obj)) {
+ show_child = showChildInTree(child);
+ }
+ }
+
+ return show_child;
+}
+
+/**
+ * This both unpacks the tree, and populates lazy loading
+ */
+ObjectWatcher *ObjectsPanel::unpackToObject(SPObject *item)
+{
+ ObjectWatcher *watcher = nullptr;
+ for (auto &parent : item->ancestorList(true)) {
+ if (parent->getRepr() == root_watcher->getRepr()) {
+ watcher = root_watcher;
+ } else if (watcher) {
+ if ((watcher = watcher->findChild(parent->getRepr()))) {
+ if (auto row = watcher->getRow()) {
+ cleanDummyChildren(*row);
+ }
+ }
+ }
+ }
+ return watcher;
+}
+
+// Same definition as in 'document.cpp'
+#define SP_DOCUMENT_UPDATE_PRIORITY (G_PRIORITY_HIGH_IDLE - 2)
+
+void ObjectsPanel::selectionChanged(Selection *selected /* not used */)
+{
+ if (!_idle_connection.connected()) {
+ auto handler = sigc::mem_fun(*this, &ObjectsPanel::_selectionChanged);
+ int priority = SP_DOCUMENT_UPDATE_PRIORITY + 1;
+ _idle_connection = Glib::signal_idle().connect(handler, priority);
+ }
+}
+
+bool ObjectsPanel::_selectionChanged()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ root_watcher->setSelectedBitRecursive(SELECTED_OBJECT, false);
+ bool keep_current_item = false;
+
+ for (auto item : getSelection()->items()) {
+ keep_current_item |= (item == current_item);
+ // Failing to find the watchers here means the object is filtered out
+ // of the current object view and can be safely skipped.
+ if (auto watcher = unpackToObject(item)) {
+ if (auto child_watcher = watcher->findChild(item->getRepr())) {
+ // Expand layers themselves, but do not expand groups.
+ auto group = cast<SPGroup>(item);
+ auto focus_watcher = (group && group->isLayer()) ? child_watcher : watcher;
+ child_watcher->setSelectedBit(SELECTED_OBJECT, true);
+
+ if (prefs->getBool("/dialogs/objects/expand_to_layer", true)) {
+ _tree.expand_to_path(focus_watcher->getTreePath());
+ if (!_scroll_lock) {
+ _tree.scroll_to_row(child_watcher->getTreePath(), 0.5);
+ }
+ }
+ }
+ }
+ }
+ if (!keep_current_item) {
+ current_item = nullptr;
+ }
+ _scroll_lock = false;
+
+ // Returning 'false' disconnects idle signal handler
+ return false;
+}
+
+/**
+ * Happens when the layer selected is changed.
+ *
+ * @param layer - The layer now selected
+ */
+void ObjectsPanel::layerChanged(SPObject *layer)
+{
+ root_watcher->setSelectedBitRecursive(LAYER_FOCUS_CHILD | LAYER_FOCUSED, false);
+
+ if (!layer || !layer->getRepr()) return;
+ auto watcher = getWatcher(layer->getRepr());
+ if (watcher && watcher != root_watcher) {
+ watcher->setSelectedBitChildren(LAYER_FOCUS_CHILD, true);
+ watcher->setSelectedBit(LAYER_FOCUSED, true);
+ }
+ _layer = layer;
+}
+
+
+/**
+ * Stylizes a button using the given icon name and tooltip
+ */
+Gtk::Button* ObjectsPanel::_addBarButton(char const* iconName, char const* tooltip, char const *action_name)
+{
+ Gtk::Button* btn = Gtk::manage(new Gtk::Button());
+ auto child = Glib::wrap(sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR));
+ child->show();
+ btn->add(*child);
+ btn->set_relief(Gtk::RELIEF_NONE);
+ btn->set_tooltip_text(tooltip);
+ // Must use C API until GTK4.
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(btn->gobj()), action_name);
+ return btn;
+}
+
+/**
+ * Sets visibility of items in the tree
+ * @param iter Current item in the tree
+ */
+bool ObjectsPanel::toggleVisible(unsigned int state, Gtk::TreeModel::Row row)
+{
+ auto desktop = getDesktop();
+ auto selection = getSelection();
+
+ if (SPItem* item = getItem(row)) {
+ if (state & GDK_SHIFT_MASK) {
+ // Toggle Visible for layers (hide all other layers)
+ if (desktop->layerManager().isLayer(item)) {
+ desktop->layerManager().toggleLayerSolo(item);
+ DocumentUndo::done(getDocument(), _("Hide other layers"), "");
+ }
+ return true;
+ }
+ bool visible = !row[_model->_colInvisible];
+ if (state & GDK_CONTROL_MASK || !selection->includes(item)) {
+ item->setHidden(visible);
+ } else {
+ for (auto sitem : selection->items()) {
+ sitem->setHidden(visible);
+ }
+ }
+ // Use maybeDone so user can flip back and forth without making loads of undo items
+ DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), "");
+ return visible;
+ }
+ return false;
+}
+
+// show blend mode popup menu for current item
+bool ObjectsPanel::blendModePopup(GdkEventButton* event, Gtk::TreeModel::Row row) {
+ if (SPItem* item = getItem(row)) {
+ current_item = nullptr;
+ auto blend = SP_CSS_BLEND_NORMAL;
+ if (item->style && item->style->mix_blend_mode.set) {
+ blend = item->style->mix_blend_mode.value;
+ }
+ auto opacity = 1.0;
+ if (item->style && item->style->opacity.set) {
+ opacity = SP_SCALE24_TO_FLOAT(item->style->opacity.value);
+ }
+ for (auto btn : _blend_items) {
+ btn.second->property_active().set_value(btn.first == blend);
+ }
+ _opacity_slider.set_value(opacity * 100);
+ current_item = item;
+
+ Gdk::Rectangle rect(event->x, event->y, 1, 1);
+ _object_menu.set_pointing_to(rect);
+ _item_state_toggler->set_active();
+ _object_menu.popup();
+ }
+
+ return true;
+}
+
+/**
+ * Sets sensitivity of items in the tree
+ * @param iter Current item in the tree
+ * @param locked Whether the item should be locked
+ */
+bool ObjectsPanel::toggleLocked(unsigned int state, Gtk::TreeModel::Row row)
+{
+ auto desktop = getDesktop();
+ auto selection = getSelection();
+
+ if (SPItem* item = getItem(row)) {
+ if (state & GDK_SHIFT_MASK) {
+ // Toggle lock for layers (lock all other layers)
+ if (desktop->layerManager().isLayer(item)) {
+ desktop->layerManager().toggleLockOtherLayers(item);
+ DocumentUndo::done(getDocument(), _("Lock other layers"), "");
+ }
+ return true;
+ }
+ bool locked = !row[_model->_colLocked];
+ if (state & GDK_CONTROL_MASK || !selection->includes(item)) {
+ item->setLocked(locked);
+ } else {
+ for (auto sitem : selection->items()) {
+ sitem->setLocked(locked);
+ }
+ }
+ // Use maybeDone so user can flip back and forth without making loads of undo items
+ DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), "");
+ return locked;
+ }
+ return false;
+}
+
+bool ObjectsPanel::_handleKeyPress(GdkEventKey *event)
+{
+ auto desktop = getDesktop();
+ if (!desktop)
+ return false;
+
+ // This isn't needed in Gtk4, use expand_collapse_cursor_row instead.
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn *column;
+ _tree.get_cursor(path, column);
+
+ auto selection = getSelection();
+ bool shift = event->state & GDK_SHIFT_MASK;
+ Gtk::AccelKey shortcut = Inkscape::Shortcuts::get_from_event(event);
+ switch (shortcut.get_key()) {
+ case GDK_KEY_Escape:
+ if (desktop->canvas) {
+ desktop->canvas->grab_focus();
+ return true;
+ }
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (path && shift) {
+ _tree.collapse_row(path);
+ return true;
+ }
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (path && shift) {
+ _tree.expand_row(path, false);
+ return true;
+ }
+ break;
+ case GDK_KEY_space:
+ selectCursorItem(event->state);
+ return true;
+ // Depending on the action to cover this causes it's special
+ // text and node handling to block deletion of objects. DIY
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ getSelection()->deleteItems();
+ // NOTE: We could select a sibling object here to make deleting many objects easier.
+ return true;
+ case GDK_KEY_Page_Up:
+ case GDK_KEY_KP_Page_Up:
+ if (shift) {
+ selection->raiseToTop();
+ return true;
+ }
+ break;
+ case GDK_KEY_Page_Down:
+ case GDK_KEY_KP_Page_Down:
+ if (shift) {
+ selection->lowerToBottom();
+ return true;
+ }
+ break;
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ if (shift) {
+ selection->stackUp();
+ return true;
+ }
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ if (shift) {
+ selection->stackDown();
+ return true;
+ }
+ break;
+
+ }
+ return _handleKeyEvent(event);
+}
+
+/**
+ * Handles keyboard events
+ * @param event Keyboard event passed in from GDK
+ * @return Whether the event should be eaten (om nom nom)
+ */
+bool ObjectsPanel::_handleKeyEvent(GdkEventKey *event)
+{
+ auto desktop = getDesktop();
+ if (!desktop)
+ return false;
+
+ bool press = event->type == GDK_KEY_PRESS;
+ Gtk::AccelKey shortcut = Inkscape::Shortcuts::get_from_event(event);
+ switch (shortcut.get_key()) {
+ // space and return enter label editing mode; leave them for the tree to handle
+ case GDK_KEY_space:
+ case GDK_KEY_Return:
+ return false;
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ _handleTransparentHover(press);
+ return false;
+ }
+ return false;
+}
+
+/**
+ * Handles mouse movements
+ * @param event Motion event passed in from GDK
+ * @returns Whether the event should be eaten.
+ */
+bool ObjectsPanel::_handleMotionEvent(GdkEventMotion* motion_event)
+{
+ if (_is_editing) return false;
+
+ // Unhover any existing hovered row.
+ if (_hovered_row_ref) {
+ if (auto row = *_store->get_iter(_hovered_row_ref.get_path())) {
+ row[_model->_colHover] = false;
+ row[_model->_colHoverColor] = false;
+ }
+ }
+ // Allow this function to be called by LEAVE motion
+ if (!motion_event) {
+ _hovered_row_ref = Gtk::TreeModel::RowReference();
+ _handleTransparentHover(false);
+ return false;
+ }
+
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn* col = nullptr;
+ int x, y;
+ if (_tree.get_path_at_pos((int)motion_event->x, (int)motion_event->y, path, col, x, y)) {
+ // Only allow drag and drop from the name column, not any others
+ if (col == _name_column) {
+ _drag_column = nullptr;
+ }
+ // Only allow drag and drop when not filtering. Otherwise bad things happen
+ _tree.set_reorderable(col == _name_column);
+ if (auto row = *_store->get_iter(path)) {
+ row[_model->_colHover] = true;
+ _hovered_row_ref = Gtk::TreeModel::RowReference(_store, path);
+ _tree.set_cursor(path);
+
+ if (col == _color_tag_column) {
+ row[_model->_colHoverColor] = true;
+ }
+
+ // Dragging over the eye or locks will set them all
+ auto item = getItem(row);
+ if (item && _drag_column && col == _drag_column) {
+ if (col == _eye_column) {
+ // Defer visibility to th idle thread (it's expensive)
+ Glib::signal_idle().connect_once([=]() {
+ item->setHidden(_drag_flip);
+ DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), "");
+ }, Glib::PRIORITY_DEFAULT_IDLE);
+ } else if (col == _lock_column) {
+ item->setLocked(_drag_flip);
+ DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), "");
+ }
+ }
+ }
+ }
+
+ _handleTransparentHover(motion_event->state & GDK_MOD1_MASK);
+ return false;
+}
+
+void ObjectsPanel::_handleTransparentHover(bool enabled)
+{
+ SPItem *item = nullptr;
+ if (enabled && _hovered_row_ref) {
+ if (auto row = *_store->get_iter(_hovered_row_ref.get_path())) {
+ item = getItem(row);
+ }
+ }
+
+ if (item == _solid_item)
+ return;
+
+ // Set the target item, this prevents rerunning too.
+ _solid_item = item;
+ auto desktop = getDesktop();
+
+ // Reset all the items in the list.
+ for (auto &item : _translucent_items) {
+ Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey);
+ arenaitem->setOpacity(SP_SCALE24_TO_FLOAT(item->style->opacity.value));
+ }
+ _translucent_items.clear();
+
+ if (item) {
+ _generateTranslucentItems(getDocument()->getRoot());
+
+ for (auto &item : _translucent_items) {
+ Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey);
+ arenaitem->setOpacity(0.2);
+ }
+ }
+}
+
+/**
+ * Generate a new list of sibling items (recursive)
+ */
+void ObjectsPanel::_generateTranslucentItems(SPItem *parent)
+{
+ if (parent == _solid_item)
+ return;
+ if (parent->isAncestorOf(_solid_item)) {
+ for (auto &child: parent->children) {
+ if (auto item = cast<SPItem>(&child)) {
+ _generateTranslucentItems(item);
+ }
+ }
+ } else {
+ _translucent_items.push_back(parent);
+ }
+}
+
+/**
+ * Handles mouse up events
+ * @param event Mouse event from GDK
+ * @return whether to eat the event (om nom nom)
+ */
+bool ObjectsPanel::_handleButtonEvent(GdkEventButton* event)
+{
+ auto selection = getSelection();
+ if (!selection)
+ return false;
+
+ if (event->type == GDK_BUTTON_RELEASE) {
+ _drag_column = nullptr;
+ }
+
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn* col = nullptr;
+ int x, y;
+ if (_tree.get_path_at_pos((int)event->x, (int)event->y, path, col, x, y)) {
+ if (auto row = *_store->get_iter(path)) {
+ if (event->type == GDK_BUTTON_PRESS) {
+ // Remember column for dragging feature
+ _drag_column = col;
+ if (col == _eye_column) {
+ _drag_flip = toggleVisible(event->state, row);
+ } else if (col == _lock_column) {
+ _drag_flip = toggleLocked(event->state, row);
+ }
+ else if (col == _blend_mode_column) {
+ return blendModePopup(event, row);
+ }
+ }
+ }
+
+ // Gtk lacks the ability to detect if the user is clicking on the
+ // expander icon. So we must detect it using the cell_area check.
+ Gdk::Rectangle r;
+ _tree.get_cell_area(path, *_name_column, r);
+ bool is_expander = x < r.get_x();
+
+ if (col != _name_column || is_expander)
+ return false;
+
+ // This doesn't work, it might be being eaten.
+ if (event->type == GDK_2BUTTON_PRESS) {
+ _tree.set_cursor(path, *col, true);
+ _is_editing = true;
+ return true;
+ }
+ _is_editing = _is_editing && event->type == GDK_BUTTON_RELEASE;
+ auto row = *_store->get_iter(path);
+ if (!row) return false;
+ SPItem *item = getItem(row);
+
+ if (!item) return false;
+ auto group = cast<SPGroup>(item);
+
+ // Load the right click menu
+ const bool context_menu = event->type == GDK_BUTTON_PRESS && event->button == 3;
+
+ // Select items on button release to not confuse drag (unless it's a right-click)
+ // Right-click selects too to set up the stage for context menu which frequently relies on current selection!
+ if (!_is_editing && (event->type == GDK_BUTTON_RELEASE || context_menu)) {
+ if (context_menu) {
+ // if right-clicking on a layer, make it current for context menu actions to work correctly
+ if (group && group->layerMode() == SPGroup::LAYER && getDesktop()->layerManager().currentLayer() != item) {
+ getDesktop()->layerManager().setCurrentLayer(item, true);
+ }
+ ContextMenu *menu = new ContextMenu(getDesktop(), item, true); // true == hide menu item for opening this dialog!
+ menu->attach_to_widget(*this); // So actions work!
+ menu->show();
+ menu->popup_at_pointer(nullptr);
+ } else {
+ selectCursorItem(event->state);
+ }
+ return true;
+ } else {
+ // Remember the item for we are about to drag it!
+ current_item = item;
+ }
+ }
+ return false;
+}
+
+/**
+ * Handle a successful item label edit
+ * @param path Tree path of the item currently being edited
+ * @param new_text New label text
+ */
+void ObjectsPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text)
+{
+ _is_editing = false;
+ if (auto row = *_store->get_iter(path)) {
+ if (auto item = getItem(row)) {
+ if (!new_text.empty() && (!item->label() || new_text != item->label())) {
+ item->setLabel(new_text.c_str());
+ DocumentUndo::done(getDocument(), _("Rename object"), "");
+ }
+ }
+ }
+}
+
+/**
+ * Take over the select row functionality from the TreeView, this is because
+ * we have two selections (layer and object selection) and require a custom
+ * method of rendering the result to the treeview.
+ */
+bool ObjectsPanel::select_row( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const &path, bool /*sel*/ )
+{
+ return true;
+}
+
+/**
+ * Get the XML node which is associated with a row. Can be NULL for dummy children.
+ */
+Node *ObjectsPanel::getRepr(Gtk::TreeModel::Row const &row) const
+{
+ return row[_model->_colNode];
+}
+
+/**
+ * Get the item which is associated with a row. If getRepr(row) is not NULL,
+ * then this call is expected to also not be NULL.
+ */
+SPItem *ObjectsPanel::getItem(Gtk::TreeModel::Row const &row) const
+{
+ auto const this_const = const_cast<ObjectsPanel *>(this);
+ return cast<SPItem>(this_const->getObject(getRepr(row)));
+}
+
+/**
+ * Return true if this row has dummy children.
+ */
+bool ObjectsPanel::hasDummyChildren(Gtk::TreeModel::Row const &row) const
+{
+ for (auto &c : row.children()) {
+ if (isDummy(c)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * If the given row has dummy children, remove them.
+ * @pre Eiter all, or no children are dummies
+ * @post If the function returns true, the row has no children
+ * @return False if there are children and they are not dummies
+ */
+bool ObjectsPanel::removeDummyChildren(Gtk::TreeModel::Row const &row)
+{
+ auto &children = row.children();
+ if (!children.empty()) {
+ Gtk::TreeStore::iterator child = children[0];
+ if (!isDummy(*child)) {
+ assert(!hasDummyChildren(row));
+ return false;
+ }
+
+ do {
+ assert(child->parent() == row);
+ assert(isDummy(*child));
+ child = _store->erase(child);
+ } while (child && child->parent() == row);
+ }
+ return true;
+}
+
+bool ObjectsPanel::cleanDummyChildren(Gtk::TreeModel::Row const &row)
+{
+ if (removeDummyChildren(row)) {
+ assert(row);
+ if (auto watcher = getWatcher(getRepr(row))) {
+ watcher->addChildren(getItem(row));
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Signal handler for "drag-motion"
+ *
+ * Refuses drops into non-group items.
+ */
+bool ObjectsPanel::on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, guint time)
+{
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+
+ auto selection = getSelection();
+ auto document = getDocument();
+
+ if (!selection || !document)
+ goto finally;
+
+ _tree.get_dest_row_at_pos(x, y, path, pos);
+
+ if (path) {
+ auto item = getItem(*_store->get_iter(path));
+
+ // don't drop on self
+ if (selection->includes(item)) {
+ goto finally;
+ }
+
+ context->drag_status(Gdk::ACTION_MOVE, time);
+ return false;
+ }
+
+finally:
+ // remove drop highlight
+ _tree.unset_drag_dest_row();
+ context->drag_refuse(time);
+ return true;
+}
+
+/**
+ * Signal handler for "drag-drop".
+ *
+ * Do the actual work of drag-and-drop.
+ */
+bool ObjectsPanel::on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, guint time)
+{
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+ _tree.get_dest_row_at_pos(x, y, path, pos);
+
+ if (!path) {
+ return true;
+ }
+
+ auto drop_repr = getRepr(*_store->get_iter(path));
+ bool const drop_into = pos != Gtk::TREE_VIEW_DROP_BEFORE && //
+ pos != Gtk::TREE_VIEW_DROP_AFTER;
+
+ auto selection = getSelection();
+ auto document = getDocument();
+ if (selection && document) {
+ auto item = document->getObjectByRepr(drop_repr);
+ // We always try to drop the item, even if we end up dropping it after the non-group item
+ if (drop_into && is<SPGroup>(item)) {
+ selection->toLayer(item);
+ } else {
+ Node *after = (pos == Gtk::TREE_VIEW_DROP_BEFORE) ? drop_repr : drop_repr->prev();
+ selection->toLayer(item->parent, after);
+ }
+ DocumentUndo::done(document, _("Move items"), INKSCAPE_ICON("selection-move-to-layer"));
+ }
+
+ on_drag_end(context);
+ return true;
+}
+
+void ObjectsPanel::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ _scroll_lock = true;
+
+ auto selection = _tree.get_selection();
+ selection->set_mode(Gtk::SELECTION_MULTIPLE);
+ selection->unselect_all();
+
+ auto obj_selection = getSelection();
+ if (!obj_selection)
+ return;
+
+ if (current_item && !obj_selection->includes(current_item)) {
+ // This means the item the user started to drag is not one that is selected
+ // So we'll deselect everything and start dragging this item instead.
+ auto watcher = getWatcher(current_item->getRepr());
+ if (watcher) {
+ auto path = watcher->getTreePath();
+ selection->select(path);
+ obj_selection->set(current_item);
+ }
+ } else {
+ // Drag all the items currently selected (multi-row)
+ for (auto item : obj_selection->items()) {
+ auto watcher = getWatcher(item->getRepr());
+ if (watcher) {
+ auto path = watcher->getTreePath();
+ selection->select(path);
+ }
+ }
+ }
+}
+
+void ObjectsPanel::on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ auto selection = _tree.get_selection();
+ selection->unselect_all();
+ selection->set_mode(Gtk::SELECTION_NONE);
+ current_item = nullptr;
+}
+
+/**
+ * Select the object currently under the list-cursor (keyboard or mouse)
+ */
+bool ObjectsPanel::selectCursorItem(unsigned int state)
+{
+ auto &layers = getDesktop()->layerManager();
+ auto selection = getSelection();
+ if (!selection)
+ return false;
+
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn *column;
+ _tree.get_cursor(path, column);
+ if (!path || !column)
+ return false;
+
+ auto row = *_store->get_iter(path);
+ if (!row)
+ return false;
+
+ if (column == _eye_column) {
+ toggleVisible(state, row);
+ } else if (column == _lock_column) {
+ toggleLocked(state, row);
+ } else if (column == _name_column) {
+ auto item = getItem(row);
+ auto group = cast<SPGroup>(item);
+ _scroll_lock = true; // Clicking to select shouldn't scroll the treeview.
+ if (state & GDK_SHIFT_MASK && !selection->isEmpty()) {
+ // Select everything between this row and the last selected item
+ selection->setBetween(item);
+ } else if (state & GDK_CONTROL_MASK) {
+ selection->toggle(item);
+ } else if (group && selection->includes(item) && !group->isLayer()) {
+ // Clicking off a group (second click) will enter the group
+ layers.setCurrentLayer(item, true);
+ } else {
+ if (layers.currentLayer() == item) {
+ layers.setCurrentLayer(item->parent);
+ }
+ selection->set(item);
+ }
+ return true;
+ }
+ return false;
+}
+
+
+/**
+ * User pressed return in search box, process search query.
+ */
+void ObjectsPanel::_searchActivated()
+{
+ // The root watcher and watcher tree handles the search operations
+ setRootWatcher();
+}
+
+/**
+ * User has typed more into the search box
+ */
+void ObjectsPanel::_searchChanged()
+{
+ if (root_watcher->isFiltered() && !_searchBox.get_text_length()) {
+ _searchActivated();
+ }
+}
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :