summaryrefslogtreecommitdiffstats
path: root/src/ui/dialog/objects.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
commitcca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch)
tree146f39ded1c938019e1ed42d30923c2ac9e86789 /src/ui/dialog/objects.cpp
parentInitial commit. (diff)
downloadinkscape-upstream/1.2.2.tar.xz
inkscape-upstream/1.2.2.zip
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/ui/dialog/objects.cpp1468
1 files changed, 1468 insertions, 0 deletions
diff --git a/src/ui/dialog/objects.cpp b/src/ui/dialog/objects.cpp
new file mode 100644
index 0000000..fb8e98f
--- /dev/null
+++ b/src/ui/dialog/objects.cpp
@@ -0,0 +1,1468 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A simple panel for objects (originally developed for Ponyscape, an Inkscape derivative)
+ *
+ * Authors:
+ * Martin Owens, completely rewritten
+ * Theodore Janeczko
+ * Tweaked by Liam P White for use in Inkscape
+ * Tavmjong Bah
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ * Tavmjong Bah 2017
+ * Martin Owens 2020-2021
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "objects.h"
+
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/icontheme.h>
+#include <gtkmm/imagemenuitem.h>
+#include <gtkmm/separatormenuitem.h>
+#include <glibmm/main.h>
+#include <glibmm/i18n.h>
+
+#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 "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.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/imagetoggler.h"
+#include "ui/widget/shapeicon.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
+ 1.00, //1 selected
+ 0.50, //2 layer focused
+ 1.00, //3 layer focused & selected
+ 0.00, //4 child of focused layer
+ 1.00, //5 selected child of focused layer
+ 0.50, //6 2 and 4
+ 1.00 //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 layers_only);
+ ~ObjectWatcher() override;
+
+ 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 moveChild(Node &child, Node *sibling);
+
+ 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 layers_only;
+};
+
+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);
+ }
+ ~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;
+};
+
+/**
+ * Gets an instance of the Objects panel
+ */
+ObjectsPanel& ObjectsPanel::getInstance()
+{
+ return *new ObjectsPanel();
+}
+
+/**
+ * Creates a new ObjectWatcher, a gtk TreeView iterated watching device.
+ *
+ * @param panel The panel to which the object watcher belongs
+ * @param obj The object to watch
+ * @param iter The optional list store iter for the item, if not provided,
+ * assumes this is the root 'document' object.
+ * @param layers If true, only show and watch layers, not groups or other objects.
+ */
+ObjectWatcher::ObjectWatcher(ObjectsPanel* panel, SPItem* obj, Gtk::TreeRow *row, bool layers)
+ : panel(panel)
+ , layers_only(layers)
+ , row_ref()
+ , selection_state(0)
+ , node(obj->getRepr())
+{
+ if(row != nullptr) {
+ assert(row->children().empty());
+ setRow(*row);
+ updateRowInfo();
+ }
+ node->addObserver(*this);
+
+ // Only show children for groups (and their subclasses like SPAnchor or SPRoot)
+ if (!dynamic_cast<SPGroup const*>(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 && !layers);
+}
+ObjectWatcher::~ObjectWatcher()
+{
+ node->removeObserver(*this);
+ Gtk::TreeModel::Path path;
+ if (bool(row_ref) && (path = row_ref.get_path())) {
+ auto iter = panel->_store->get_iter(path);
+ if(iter) {
+ panel->_store->erase(iter);
+ }
+ }
+ child_watchers.clear();
+}
+
+/**
+ * Update the information in the row from the stored node
+ */
+void ObjectWatcher::updateRowInfo() {
+ if (auto item = dynamic_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();
+
+ updateRowHighlight();
+ updateRowAncestorState(row[_model->_colAncestorInvisible], row[_model->_colAncestorLocked]);
+ }
+}
+
+/**
+ * Propegate changes to the highlight color to all children.
+ */
+void ObjectWatcher::updateRowHighlight() {
+ if (auto item = dynamic_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();
+ }
+ }
+ }
+}
+
+/**
+ * Propegate 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 it's 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);
+ }
+}
+
+/**
+ * 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)
+{
+ auto group = dynamic_cast<SPGroup *>(child);
+ if (layers_only && (!group || group->layerMode() != SPGroup::LAYER)) {
+ return false;
+ }
+
+ auto const children = getChildren();
+ if (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, layers_only));
+
+ // 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 = dynamic_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 && !dynamic_cast<SPItem const *>(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 = dynamic_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;
+}
+
+class ColorTagRenderer : public Gtk::CellRenderer {
+public:
+ ColorTagRenderer() :
+ Glib::ObjectBase(typeid(CellRenderer)),
+ Gtk::CellRenderer(),
+ _property_color(*this, "tagcolor", 0) {
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+
+ int dummy_width;
+ // height size is not critical
+ Gtk::IconSize::lookup(Gtk::ICON_SIZE_MENU, dummy_width, _height);
+ }
+
+ ~ColorTagRenderer() override = default;
+
+ Glib::PropertyProxy<unsigned int> property_color() {
+ return _property_color.get_proxy();
+ }
+
+ int get_width() const {
+ return _width;
+ }
+
+ sigc::signal<void, const Glib::ustring&> signal_clicked() {
+ return _signal_clicked;
+ }
+
+private:
+ void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags) override {
+ cr->rectangle(cell_area.get_x(), cell_area.get_y(), cell_area.get_width(), cell_area.get_height());
+ ColorRGBA color(_property_color.get_value());
+ cr->set_source_rgb(color[0], color[1], color[2]);
+ cr->fill();
+ }
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const override {
+ min_w = nat_w = _width;
+ }
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const override {
+ min_h = 1;
+ nat_h = _height;
+ }
+
+ bool activate_vfunc(GdkEvent* event, Gtk::Widget& /*widget*/, const Glib::ustring& path,
+ const Gdk::Rectangle& /*background_area*/, const Gdk::Rectangle& /*cell_area*/,
+ Gtk::CellRendererState /*flags*/) override {
+ _signal_clicked.emit(path);
+ return false;
+ }
+
+ int _width = 8;
+ int _height;
+ Glib::Property<unsigned int> _property_color;
+ sigc::signal<void, const Glib::ustring&> _signal_clicked;
+};
+
+/**
+ * Constructor
+ */
+ObjectsPanel::ObjectsPanel() :
+ DialogBase("/dialogs/objects", "Objects"),
+ root_watcher(nullptr),
+ _model(nullptr),
+ _layer(nullptr),
+ _is_editing(false),
+ _page(Gtk::ORIENTATION_VERTICAL),
+ _color_picker(_("Highlight color"), "", 0, true)
+{
+ //Create the tree model and store
+ ModelColumns *zoop = new ModelColumns();
+ _model = zoop;
+
+ _store = Gtk::TreeStore::create( *zoop );
+
+ _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.set_reorderable(true);
+ _tree.enable_model_drag_dest (Gdk::ACTION_MOVE);
+
+ //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);
+
+ // 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 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->set_fixed_width(tag_renderer->get_width());
+ }
+ 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(), "highligh-color", _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ });
+
+ //Set the expander and search columns
+ _tree.set_expander_column(*_name_column);
+ // Disable search (it doesn't make much sense)
+ _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::_handleKeyEvent), 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*){
+ getDesktop()->messageStack()->flash(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*){ 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 (auto selection = getSelection()) {
+ selectionChanged(selection);
+ }
+ }
+ return 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(_buttonsRow, Gtk::PACK_SHRINK);
+ _page.pack_end(_scroller, Gtk::PACK_EXPAND_WIDGET);
+ pack_start(_page, Gtk::PACK_EXPAND_WIDGET);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ {
+ auto child = Glib::wrap(sp_get_icon_image("layer-duplicate", GTK_ICON_SIZE_SMALL_TOOLBAR));
+ child->show();
+ _object_mode.add(*child);
+ _object_mode.set_relief(Gtk::RELIEF_NONE);
+ }
+ _object_mode.set_tooltip_text(_("Switch to layers only view."));
+ _object_mode.property_active() = prefs->getBool("/dialogs/objects/layers_only", false);
+ _object_mode.property_active().signal_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_objects_toggle));
+
+ _buttonsPrimary.pack_start(_object_mode, Gtk::PACK_SHRINK);
+ _buttonsPrimary.pack_start(*_addBarButton(INKSCAPE_ICON("layer-new"), _("Add layer..."), "win.layer-new"), Gtk::PACK_SHRINK);
+
+ _buttonsSecondary.pack_end(*_addBarButton(INKSCAPE_ICON("edit-delete"), _("Remove object"), "app.delete-selection"), Gtk::PACK_SHRINK);
+ _buttonsSecondary.pack_end(*_addBarButton(INKSCAPE_ICON("go-down"), _("Move Down"), "app.selection-stack-down"), Gtk::PACK_SHRINK);
+ _buttonsSecondary.pack_end(*_addBarButton(INKSCAPE_ICON("go-up"), _("Move Up"), "app.selection-stack-up"), Gtk::PACK_SHRINK);
+
+ _buttonsRow.pack_start(_buttonsPrimary, Gtk::PACK_SHRINK);
+ _buttonsRow.pack_end(_buttonsSecondary, Gtk::PACK_SHRINK);
+
+ 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)
+ _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::_objects_toggle()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/objects/layers_only", _object_mode.get_active());
+}
+
+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()
+{
+ if (root_watcher) {
+ delete root_watcher;
+ }
+ root_watcher = nullptr;
+
+ if (auto document = getDocument()) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool layers_only = prefs->getBool("/dialogs/objects/layers_only", false);
+ root_watcher = new ObjectWatcher(this, document->getRoot(), nullptr, layers_only);
+ layerChanged(getDesktop()->layerManager().currentLayer());
+ selectionChanged(getSelection());
+ }
+}
+
+void ObjectsPanel::selectionChanged(Selection *selected)
+{
+ root_watcher->setSelectedBitRecursive(SELECTED_OBJECT, false);
+
+ for (auto item : selected->items()) {
+ ObjectWatcher *watcher = nullptr;
+ // This both unpacks the tree, and populates lazy loading
+ 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);
+ }
+ }
+ }
+ }
+ if (watcher) {
+ if (auto final_watcher = watcher->findChild(item->getRepr())) {
+ final_watcher->setSelectedBit(SELECTED_OBJECT, true);
+ _tree.expand_to_path(final_watcher->getTreePath());
+ } else {
+ g_warning("Can't find final step in tree selection!");
+ }
+ } else {
+ g_warning("Can't find a mid step in tree selection!");
+ }
+ }
+}
+
+/**
+ * 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) 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(GdkEventButton* event, Gtk::TreeModel::Row row)
+{
+ if (SPItem* item = getItem(row)) {
+ if (event->state & GDK_SHIFT_MASK) {
+ // Toggle Visible for layers (hide all other layers)
+ if (auto desktop = getDesktop()) {
+ if (desktop->layerManager().isLayer(item)) {
+ desktop->layerManager().toggleLayerSolo(item);
+ DocumentUndo::done(getDocument(), _("Hide other layers"), "");
+ }
+ }
+ } else {
+ item->setHidden(!row[_model->_colInvisible]);
+ // Use maybeDone so user can flip back and forth without making loads of undo items
+ DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), "");
+ }
+ }
+ 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(GdkEventButton* event, Gtk::TreeModel::Row row)
+{
+ if (SPItem* item = getItem(row)) {
+ if (event->state & GDK_SHIFT_MASK) {
+ // Toggle lock for layers (lock all other layers)
+ if (auto desktop = getDesktop()) {
+ if (desktop->layerManager().isLayer(item)) {
+ desktop->layerManager().toggleLockOtherLayers(item);
+ DocumentUndo::done(getDocument(), _("Lock other layers"), "");
+ }
+ }
+ } else {
+ item->setLocked(!row[_model->_colLocked]);
+ // Use maybeDone so user can flip back and forth without making loads of undo items
+ DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), "");
+ }
+ }
+ return true;
+}
+
+/**
+ * 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()) {
+ case GDK_KEY_Escape:
+ if (desktop->canvas) {
+ desktop->canvas->grab_focus();
+ return true;
+ }
+ break;
+
+ // space and return enter label editing mode; leave them for the tree to handle
+ case GDK_KEY_Return:
+ case GDK_KEY_space:
+ 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;
+ }
+ // 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)) {
+ if (auto row = *_store->get_iter(path)) {
+ row[_model->_colHover] = true;
+ _hovered_row_ref = Gtk::TreeModel::RowReference(_store, path);
+ }
+ }
+
+ _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 = dynamic_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;
+
+ 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_RELEASE) {
+ if (col == _eye_column) {
+ return toggleVisible(event, row);
+ } else if (col == _lock_column) {
+ return toggleLocked(event, row);
+ }
+ }
+ }
+ // Only the label reacts to clicks, nothing else, only need to test horz
+ Gdk::Rectangle r;
+ _tree.get_cell_area(path, *_name_column, r);
+ if (x < r.get_x() || x > (r.get_x() + r.get_width()))
+ 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;
+ SPGroup *group = SP_GROUP(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 (event->state & GDK_SHIFT_MASK && !selection->isEmpty()) {
+ // Select everything between this row and the last selected item
+ selection->setBetween(item);
+ } else if (event->state & GDK_CONTROL_MASK) {
+ selection->toggle(item);
+ } else if (group && group->layerMode() == SPGroup::LAYER) {
+ // if right-clicking on a layer, make it current for context menu actions to work correctly
+ if (context_menu) {
+ if (getDesktop()->layerManager().currentLayer() != item) {
+ getDesktop()->layerManager().setCurrentLayer(item, true);
+ }
+ } else {
+ selection->set(item);
+ }
+ } else if (!context_menu) {
+ selection->set(item);
+ }
+
+ if (context_menu) {
+ 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);
+ }
+ 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 dynamic_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);
+ getWatcher(getRepr(row))->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 iter = _store->get_iter(path);
+ auto repr = getRepr(*iter);
+ auto obj = document->getObjectByRepr(repr);
+
+ bool const drop_into = pos != Gtk::TREE_VIEW_DROP_BEFORE && //
+ pos != Gtk::TREE_VIEW_DROP_AFTER;
+
+ // don't drop on self
+ if (selection->includes(obj)) {
+ goto finally;
+ }
+
+ auto item = getItem(*iter);
+
+ // only groups can have children
+ if (drop_into && !dynamic_cast<SPGroup const *>(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) {
+ if (drop_into) {
+ selection->toLayer(document->getObjectByRepr(drop_repr));
+ } else {
+ Node *after = (pos == Gtk::TREE_VIEW_DROP_BEFORE) ? drop_repr : drop_repr->prev();
+ selection->toLayer(document->getObjectByRepr(drop_repr->parent()), false, after);
+ }
+ }
+
+ on_drag_end(context);
+ return true;
+}
+
+void ObjectsPanel::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ 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;
+}
+
+} //namespace Dialogs
+} //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 :