// 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 * Tavmjong Bah 2017 * Martin Owens 2020-2021 * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "objects.h" #include #include #include #include #include #include #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 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> 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 _colNode; Gtk::TreeModelColumn _colLabel; Gtk::TreeModelColumn _colType; Gtk::TreeModelColumn _colIconColor; Gtk::TreeModelColumn _colClipMask; Gtk::TreeModelColumn _colBgColor; Gtk::TreeModelColumn _colInvisible; Gtk::TreeModelColumn _colLocked; Gtk::TreeModelColumn _colAncestorInvisible; Gtk::TreeModelColumn _colAncestorLocked; Gtk::TreeModelColumn _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(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(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(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(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(&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(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(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 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 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 property_color() { return _property_color.get_proxy(); } int get_width() const { return _width; } sigc::signal signal_clicked() { return _signal_clicked; } private: void render_vfunc(const Cairo::RefPtr& 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 _property_color; sigc::signal _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, _("Hold ALT while hovering over item to highlight, hold SHIFT 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(&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 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(this); return dynamic_cast(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 &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(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 &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 &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 &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 :