// 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 #include #include #include #include #include #include #include #include #include #include #include #include #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 "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 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 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 _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; Gtk::TreeModelColumn _colItemStateSet; Gtk::TreeModelColumn _colBlendMode; Gtk::TreeModelColumn _colOpacity; Gtk::TreeModelColumn _colItemState; // Set when hovering over the color tag cell Gtk::TreeModelColumn _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(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(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(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(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(&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(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(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; } /** * 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(_builder, "settings-menu")) , _object_menu(get_widget(_builder, "object-menu")) , _searchBox(get_widget(_builder, "search")) , _opacity_slider(get_widget(_builder, "opacity-slider")) , _setting_layers(get_derived_widget(_builder, "setting-layers", "/dialogs/objects/layers_only", false)) , _setting_track(get_derived_widget(_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(_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& 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) ? "%1 %2%%\n\n%3\n%4" : "%1 %2%%\n\n%3\n%4"; 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(_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(); 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(); 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, _("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*){ 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(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(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(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(&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(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 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 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); 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 &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 &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(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 &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 &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(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 :