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