// SPDX-License-Identifier: GPL-2.0-or-later /* * A simple panel for objects (originally developed for Ponyscape, an Inkscape derivative) * * Authors: * Theodore Janeczko * Tweaked by Liam P White for use in Inkscape * Tavmjong Bah * * Copyright (C) Theodore Janeczko 2012 * Tavmjong Bah 2017 * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "objects.h" #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 "shortcuts.h" #include "verbs.h" #include "helper/action.h" #include "ui/icon-loader.h" #include "include/gtkmm_version.h" #include "object/filters/blend.h" #include "object/filters/gaussian-blur.h" #include "object/sp-clippath.h" #include "object/sp-mask.h" #include "object/sp-root.h" #include "object/sp-shape.h" #include "style.h" #include "ui/contextmenu.h" #include "ui/dialog-events.h" #include "ui/icon-names.h" #include "ui/selected-color.h" #include "ui/tools-switch.h" #include "ui/tools/node-tool.h" #include "ui/widget/clipmaskicon.h" #include "ui/widget/color-notebook.h" #include "ui/widget/highlight-picker.h" #include "ui/widget/imagetoggler.h" #include "ui/widget/insertordericon.h" #include "ui/widget/layertypeicon.h" #include "xml/node-observer.h" //#define DUMP_LAYERS 1 namespace Inkscape { namespace UI { namespace Dialog { using Inkscape::XML::Node; /** * Gets an instance of the Objects panel */ ObjectsPanel& ObjectsPanel::getInstance() { return *new ObjectsPanel(); } /** * Column enumeration */ enum { COL_VISIBLE = 1, COL_LOCKED, COL_TYPE, // COL_INSERTORDER, COL_CLIPMASK, COL_HIGHLIGHT }; /** * Button enumeration */ enum { BUTTON_NEW = 0, BUTTON_RENAME, BUTTON_TOP, BUTTON_BOTTOM, BUTTON_UP, BUTTON_DOWN, BUTTON_DUPLICATE, BUTTON_DELETE, BUTTON_SOLO, BUTTON_SHOW_ALL, BUTTON_HIDE_ALL, BUTTON_LOCK_OTHERS, BUTTON_LOCK_ALL, BUTTON_UNLOCK_ALL, BUTTON_SETCLIP, BUTTON_CLIPGROUP, // BUTTON_SETINVCLIP, BUTTON_UNSETCLIP, BUTTON_SETMASK, BUTTON_UNSETMASK, BUTTON_GROUP, BUTTON_UNGROUP, BUTTON_COLLAPSE_ALL, DRAGNDROP, UPDATE_TREE }; /** * Xml node observer for observing objects in the document */ class ObjectsPanel::ObjectWatcher : public Inkscape::XML::NodeObserver { public: /** * Creates a new object watcher * @param pnl The panel to which the object watcher belongs * @param obj The object to watch */ ObjectWatcher(ObjectsPanel* pnl, SPObject* obj) : _pnl(pnl), _obj(obj), _repr(obj->getRepr()), _highlightAttr(g_quark_from_string("inkscape:highlight-color")), _lockedAttr(g_quark_from_string("sodipodi:insensitive")), _labelAttr(g_quark_from_string("inkscape:label")), _groupAttr(g_quark_from_string("inkscape:groupmode")), _styleAttr(g_quark_from_string("style")), _clipAttr(g_quark_from_string("clip-path")), _maskAttr(g_quark_from_string("mask")) { _repr->addObserver(*this); } ~ObjectWatcher() override { _repr->removeObserver(*this); } void notifyChildAdded( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override { if ( _pnl && _obj ) { _pnl->_objectsChangedWrapper( _obj ); } } void notifyChildRemoved( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override { if ( _pnl && _obj ) { _pnl->_objectsChangedWrapper( _obj ); } } void notifyChildOrderChanged( Node &/*node*/, Node &/*child*/, Node */*old_prev*/, Node */*new_prev*/ ) override { if ( _pnl && _obj ) { _pnl->_objectsChangedWrapper( _obj ); } } void notifyContentChanged( Node &/*node*/, Util::ptr_shared /*old_content*/, Util::ptr_shared /*new_content*/ ) override {} void notifyAttributeChanged( Node &node, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ ) override { /* Weird things happen on undo! we get notified about the child being removed, but after that we still get * notified for attributes being changed on this XML node! In that case the corresponding SPObject might already * have been deleted and the pointer to might be invalid, leading to a segfault if we're not carefull. * So after we initiated the update of the treeview using _objectsChangedWrapper() in notifyChildRemoved(), the * _pending_update flag is set, and we will no longer process any notifyAttributeChanged() * Reproducing the crash: new document -> open objects panel -> draw freehand line -> undo -> segfault (but only * if we don't check for _pending_update) */ if ( _pnl && (!_pnl->_pending_update) && _obj ) { if ( name == _lockedAttr || name == _labelAttr || name == _highlightAttr || name == _groupAttr || name == _styleAttr || name == _clipAttr || name == _maskAttr ) { _pnl->_updateObject(_obj, name == _highlightAttr); if ( name == _styleAttr ) { _pnl->_updateComposite(); } } } } /** * Objects panel to which this watcher belongs */ ObjectsPanel* _pnl; /** * The object that is being observed */ SPObject* _obj; /** * The xml representation of the object that is being observed */ Inkscape::XML::Node* _repr; /* These are quarks which define the attributes that we are observing */ GQuark _highlightAttr; GQuark _lockedAttr; GQuark _labelAttr; GQuark _groupAttr; GQuark _styleAttr; GQuark _clipAttr; GQuark _maskAttr; }; class ObjectsPanel::InternalUIBounce { public: int _actionCode; sigc::connection _signal; }; class ObjectsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord { public: ModelColumns() { add(_colObject); add(_colVisible); add(_colLocked); add(_colLabel); add(_colType); add(_colHighlight); add(_colClipMask); add(_colPrevSelectionState); //add(_colInsertOrder); } ~ModelColumns() override = default; Gtk::TreeModelColumn _colObject; Gtk::TreeModelColumn _colLabel; Gtk::TreeModelColumn _colVisible; Gtk::TreeModelColumn _colLocked; Gtk::TreeModelColumn _colType; Gtk::TreeModelColumn _colHighlight; Gtk::TreeModelColumn _colClipMask; Gtk::TreeModelColumn _colPrevSelectionState; //Gtk::TreeModelColumn _colInsertOrder; }; /** * Stylizes a button using the given icon name and tooltip */ void ObjectsPanel::_styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip) { GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_widget_show( child ); btn.add( *Gtk::manage(Glib::wrap(child)) ); btn.set_relief(Gtk::RELIEF_NONE); btn.set_tooltip_text (tooltip); } /** * Adds an item to the pop-up (right-click) menu * @param desktop The active destktop * @param code Action code * @param id Button id for callback function * @return The generated menu item */ Gtk::MenuItem& ObjectsPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, int id ) { Verb *verb = Verb::get( code ); g_assert(verb); SPAction *action = verb->get_action(Inkscape::ActionContext(desktop)); Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem()); Gtk::Label *label = Gtk::manage(new Gtk::Label(action->name, true)); label->set_xalign(0.0); if (_show_contextmenu_icons && action->image) { item->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" Gtk::Image *icon = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can only hold one child Gtk::Box *box = Gtk::manage(new Gtk::Box()); box->pack_start(*icon, false, false, 0); box->pack_start(*label, true, true, 0); item->add(*box); } else { item->add(*label); } item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_takeAction), id)); _popupMenu.append(*item); return *item; } /** * Attach a watcher to the XML node of an item, which will signal us in case of changes to that item or node * @param item The item of which the XML node is to be watched */ void ObjectsPanel::_addWatcher(SPItem *item) { bool used = true; // Any newly created watcher is obviously being used auto iter = _objectWatchers.find(item); if (iter == _objectWatchers.end()) { // If not found then watcher doesn't exist yet ObjectsPanel::ObjectWatcher *w = new ObjectsPanel::ObjectWatcher(this, item); _objectWatchers.emplace(item, std::make_pair(w, used)); } else { // Found; no need to create a new watcher; just flag it as "in use" (*iter).second.second = used; } } /** * Delete the watchers, which signal us in case of changes to the item being watched * @param only_unused Only delete those watchers that are no longer in use */ void ObjectsPanel::_removeWatchers(bool only_unused = false) { // Delete all watchers (optionally only those which are not in use) auto iter = _objectWatchers.begin(); while (iter != _objectWatchers.end()) { bool used = (*iter).second.second; bool delete_watcher = (!only_unused) || (only_unused && !used); if ( delete_watcher ) { ObjectsPanel::ObjectWatcher *w = (*iter).second.first; delete w; iter = _objectWatchers.erase(iter); } else { // It must be in use, so the used "field" should be set to true; // However, when _removeWatchers is being called, we will already have processed the complete queue ... g_assert(_tree_update_queue.empty()); // .. and we can preemptively flag it as unused for the processing of the next queue (*iter).second.second = false; // It will be set to true again by _addWatcher, if in use iter++; } } } /** * Call function for asynchronous invocation of _objectsChanged */ void ObjectsPanel::_objectsChangedWrapper(SPObject */*obj*/) { // We used to call _objectsChanged with a reference to _obj, // but since _obj wasn't used, I'm dropping that for now _takeAction(UPDATE_TREE); } /** * Callback function for when an object changes. Essentially refreshes the entire tree * @param obj Object which was changed (currently not used as the entire tree is recreated) */ void ObjectsPanel::_objectsChanged(SPObject */*obj*/) { if (_desktop) { //Get the current document's root and use that to enumerate the tree SPDocument* document = _desktop->doc(); SPRoot* root = document->getRoot(); if ( root ) { _selectedConnection.block(); // Will be unblocked after the queue has been processed fully _documentChangedCurrentLayer.block(); //Clear the tree store _store->clear(); // This will increment it's stamp, making all old iterators _tree_cache.clear(); // invalid. So we will also clear our own cache, as well _tree_update_queue.clear(); // as any remaining update queue // Temporarily detach the TreeStore from the TreeView to slightly reduce flickering, and to speed up // Note: if we truly want to eliminate the flickering, we should implement double buffering on the _store, // but maybe this is a bit too much effort/bloat for too little gain? _tree.unset_model(); //Add all items recursively; we will do this asynchronously, by first filling a queue, which is rather fast _queueObject( root, nullptr ); //However, the processing of this queue is slow, so this is done at a low priority and in small chunks. Using //only small chunks keeps Inkscape responsive, for example while using the spray tool. After processing each //of the chunks, Inkscape will check if there are other tasks with a high priority, for example when user is //spraying. If so, the sprayed objects will be added first, and the whole updating will be restarted before //it even finished. _paths_to_be_expanded.clear(); _processQueue_sig.disconnect(); // Might be needed in case objectsChanged is called directly, and not through objectsChangedWrapper() _processQueue_sig = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_processQueue), 0, Glib::PRIORITY_DEFAULT_IDLE+100); } } } /** * Recursively adds the children of the given item to the tree * @param obj Root object to add to the tree * @param parentRow Parent tree row (or NULL if adding to tree root) */ void ObjectsPanel::_queueObject(SPObject* obj, Gtk::TreeModel::Row* parentRow) { bool already_expanded = false; for(auto& child: obj->children) { if (SP_IS_ITEM(&child)) { //Add the item to the tree, basically only creating an empty row in the tree view Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); //Add the item to a queue, so we can fill in the data in each row asynchronously //at a later stage. See the comments in _objectsChanged() for more details bool expand = SP_IS_GROUP(obj) && SP_GROUP(obj)->expanded() && (not already_expanded); _tree_update_queue.emplace_back(SP_ITEM(&child), iter, expand); already_expanded = expand || already_expanded; // We need to expand only a single child in each group //If the item is a group, recursively add its children if (SP_IS_GROUP(&child)) { Gtk::TreeModel::Row row = *iter; _queueObject(&child, &row); } } } } /** * Walks through the queue in small chunks, and fills in the rows in the tree view accordingly * @return False if the queue has been fully emptied */ bool ObjectsPanel::_processQueue() { auto queue_iter = _tree_update_queue.begin(); auto queue_end = _tree_update_queue.end(); int count = 0; while (queue_iter != queue_end) { //The queue is a list of tuples; expand the tuples SPItem *item = std::get<0>(*queue_iter); Gtk::TreeModel::iterator iter = std::get<1>(*queue_iter); bool expanded = std::get<2>(*queue_iter); //Add the object to the tree view and tree cache _addObjectToTree(item, *iter, expanded); _tree_cache.emplace(item, *iter); /* Update the watchers; No watcher shall be deleted before the processing of the queue has * finished; we need to keep watching for items that might have been deleted while the queue, * which is being processed on idle, was not yet empty. This is because when an item is deleted, the * queue is still holding a pointer to it. The NotifyChildRemoved method of the watcher will stop the * processing of the queue and prevent a segmentation fault, but only if there is a watcher in place*/ _addWatcher(item); queue_iter = _tree_update_queue.erase(queue_iter); count++; if (count == 100 && (!_tree_update_queue.empty())) { return true; // we have not yet reached the end of the queue, so return true to keep the timeout signal alive } } //We have reached the end of the queue, and it is safe to remove any watchers _removeWatchers(true); // ... but only remove those that are no longer in use // Now we can bring the tree view back to life safely _tree.set_model(_store); // Attach the store again to the tree view this sets search columns as -1 _tree.set_search_column(_model->_colLabel);//set search column again // Expand the tree; this is kept outside of _addObjectToTree() and _processQueue() to allow // temporarily detaching the store from the tree, which slightly reduces flickering for (auto path: _paths_to_be_expanded) { _tree.expand_to_path(path); _tree.collapse_row(path); } _blockAllSignals(false); _objectsSelected(_desktop->selection); //Set the tree selection; will also invoke _checkTreeSelection() _pending_update = false; return false; // Return false to kill the timeout signal that kept calling _processQueue } /** * Fills in the details of an item in the already existing row of the tree view * @param item Item of which the name, visibility, lock status, etc, will be filled in * @param row Row where the item is residing * @param expanded True if the item is part of a group that is shown as expanded in the tree view */ void ObjectsPanel::_addObjectToTree(SPItem* item, const Gtk::TreeModel::Row &row, bool expanded) { SPGroup * group = SP_IS_GROUP(item) ? SP_GROUP(item) : nullptr; row[_model->_colObject] = item; gchar const * label = item->label() ? item->label() : item->getId(); row[_model->_colLabel] = label ? label : item->defaultLabel(); row[_model->_colVisible] = !item->isHidden(); row[_model->_colLocked] = !item->isSensitive(); row[_model->_colType] = group ? (group->layerMode() == SPGroup::LAYER ? 2 : 1) : 0; row[_model->_colHighlight] = item->isHighlightSet() ? item->highlight_color() : item->highlight_color() & 0xffffff00; row[_model->_colClipMask] = item ? ( (item->getClipObject() ? 1 : 0) | (item->getMaskObject() ? 2 : 0) ) : 0; row[_model->_colPrevSelectionState] = false; //row[_model->_colInsertOrder] = group ? (group->insertBottom() ? 2 : 1) : 0; //If our parent object is a group and it's expanded, expand the tree if (expanded) { _paths_to_be_expanded.emplace_back(_store->get_path(row)); } } /** * Updates an item in the tree and optionally recursively updates the item's children * @param obj The item to update in the tree * @param recurse Whether to recurse through the item's children */ void ObjectsPanel::_updateObject( SPObject *obj, bool recurse ) { Gtk::TreeModel::iterator tree_iter; if (_findInTreeCache(SP_ITEM(obj), tree_iter)) { Gtk::TreeModel::Row row = *tree_iter; //We found our item in the tree; now update it! SPItem * item = SP_IS_ITEM(obj) ? SP_ITEM(obj) : nullptr; SPGroup * group = SP_IS_GROUP(obj) ? SP_GROUP(obj) : nullptr; gchar const * label = obj->label() ? obj->label() : obj->getId(); row[_model->_colLabel] = label ? label : obj->defaultLabel(); row[_model->_colVisible] = item ? !item->isHidden() : false; row[_model->_colLocked] = item ? !item->isSensitive() : false; row[_model->_colType] = group ? (group->layerMode() == SPGroup::LAYER ? 2 : 1) : 0; row[_model->_colHighlight] = item ? (item->isHighlightSet() ? item->highlight_color() : item->highlight_color() & 0xffffff00) : 0; row[_model->_colClipMask] = item ? ( (item->getClipObject() ? 1 : 0) | (item->getMaskObject() ? 2 : 0) ) : 0; //row[_model->_colInsertOrder] = group ? (group->insertBottom() ? 2 : 1) : 0; if (recurse){ for (auto& iter: obj->children) { _updateObject(&iter, recurse); } } } } /** * Updates the composite controls for the selected item */ void ObjectsPanel::_updateComposite() { if (!_blockCompositeUpdate) { //Set the default values bool setValues = true; //Get/set the values _tree.get_selection()->selected_foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_compositingChanged), &setValues)); } } /** * Sets the compositing values for the first selected item in the tree * @param iter Current tree item * @param setValues Whether to set the compositing values */ void ObjectsPanel::_compositingChanged( const Gtk::TreeModel::iterator& iter, bool *setValues ) { if (iter) { Gtk::TreeModel::Row row = *iter; SPItem *item = row[_model->_colObject]; if (*setValues) { _setCompositingValues(item); *setValues = false; } } } /** * Occurs when the current desktop selection changes * @param sel The current selection */ void ObjectsPanel::_objectsSelected( Selection *sel ) { bool setOpacity = true; _selectedConnection.block(); _tree.get_selection()->unselect_all(); _store->foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_clearPrevSelectionState)); SPItem *item = nullptr; auto items = sel->items(); for(auto i=items.begin(); i!=items.end(); ++i){ item = *i; if (setOpacity) { _setCompositingValues(item); setOpacity = false; } _updateObjectSelected(item, (*i)==items.back(), false); } if (!item) { if (_desktop->currentLayer() && SP_IS_ITEM(_desktop->currentLayer())) { item = SP_ITEM(_desktop->currentLayer()); _setCompositingValues(item); _updateObjectSelected(item, false, true); } } _selectedConnection.unblock(); _checkTreeSelection(); } /** * Helper function for setting the compositing values * @param item Item to use for setting the compositing values */ void ObjectsPanel::_setCompositingValues(SPItem *item) { // Block the connections to avoid interference _isolationConnection.block(); _opacityConnection.block(); _blendConnection.block(); _blurConnection.block(); // Set the isolation auto isolation = item->style->isolation.set ? item->style->isolation.value : SP_CSS_ISOLATION_AUTO; _filter_modifier.set_isolation_mode(isolation, true); // Set the opacity double opacity = (item->style->opacity.set ? SP_SCALE24_TO_FLOAT(item->style->opacity.value) : 1); opacity *= 100; // Display in percent. _filter_modifier.set_opacity_value(opacity); // Set the blend mode if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, true); } else { _filter_modifier.set_blend_mode(item->style->mix_blend_mode.value, true); } if (_filter_modifier.get_blend_mode() == SP_CSS_BLEND_NORMAL) { if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) { auto blend = filter_get_legacy_blend(item); _filter_modifier.set_blend_mode(blend, true); } } SPGaussianBlur *spblur = nullptr; if (item->style->getFilter()) { for (auto& primitive_obj: item->style->getFilter()->children) { if (!SP_IS_FILTER_PRIMITIVE(&primitive_obj)) { break; } if (SP_IS_GAUSSIANBLUR(&primitive_obj) && !spblur) { //Get the blur value spblur = SP_GAUSSIANBLUR(&primitive_obj); } } } //Set the blur value double blur_value = 0; if (spblur) { Geom::OptRect bbox = item->bounds(SPItem::GEOMETRIC_BBOX); // calculating the bbox is expensive; only do this if we have an spblur in the first place if (bbox) { double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? blur_value = spblur->stdDeviation.getNumber() * 400 / perimeter; } } _filter_modifier.set_blur_value(blur_value); //Unblock connections _isolationConnection.unblock(); _blurConnection.unblock(); _blendConnection.unblock(); _opacityConnection.unblock(); } // See the comment in objects.h for _tree_cache /** * Find the specified item in the tree cache * @param iter Current tree item * @param tree_iter Tree_iter will point to the row in which the tree item was found * @return True if found */ bool ObjectsPanel::_findInTreeCache(SPItem* item, Gtk::TreeModel::iterator &tree_iter) { if (not item) { return false; } try { tree_iter = _tree_cache.at(item); } catch (std::out_of_range) { // Apparently, item cannot be found in the tree_cache, which could mean that // - the tree and/or tree_cache are out-dated or in the process of being updated. // - a layer is selected, which is not visible in the objects panel (see _objectsSelected()) // Anyway, this doesn't seem all that critical, so no warnings; just return false return false; } /* If the row in the tree has been deleted, and an old tree_cache is being used, then we will * get a segmentation fault crash somewhere here; so make sure iters don't linger around! * We can only check the validity as done below, but this is rather slow according to the * documentation (adds 0.25 s for a 2k long tree). But better safe than sorry */ if (not _store->iter_is_valid(tree_iter)) { g_critical("Invalid iterator to Gtk::tree in objects panel; just prevented a segfault!"); return false; } return true; } /** * Find the specified item in the tree store and (de)select it, optionally scrolling to the item * @param item Item to select in the tree * @param scrollto Whether to scroll to the item * @param expand If true, the path in the tree towards item will be expanded */ void ObjectsPanel::_updateObjectSelected(SPItem* item, bool scrollto, bool expand) { Gtk::TreeModel::iterator tree_iter; if (_findInTreeCache(item, tree_iter)) { Gtk::TreeModel::Row row = *tree_iter; //We found the item! Expand to the path and select it in the tree. Gtk::TreePath path = _store->get_path(tree_iter); _tree.expand_to_path( path ); if (!expand) // but don't expand itself, just the path _tree.collapse_row(path); Glib::RefPtr select = _tree.get_selection(); select->select(tree_iter); row[_model->_colPrevSelectionState] = true; if (scrollto) { //Scroll to the item in the tree _tree.scroll_to_row(path, 0.5); } } } /** * Pushes the current tree selection to the canvas */ void ObjectsPanel::_pushTreeSelectionToCurrent() { if ( _desktop && _desktop->currentRoot() ) { //block connections for selection and compositing values to prevent interference _selectionChangedConnection.block(); _documentChangedCurrentLayer.block(); //Clear the selection and then iterate over the tree selection, pushing each item to the desktop _desktop->selection->clear(); if (_tree.get_selection()->count_selected_rows() == 0) { _store->foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_clearPrevSelectionState)); } bool setOpacity = true; bool first_pass = true; _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass)); first_pass = false; _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass)); //unblock connections, unless we were already blocking them beforehand _selectionChangedConnection.unblock(); _documentChangedCurrentLayer.unblock(); _checkTreeSelection(); } } /** * Helper function for pushing the current tree selection to the current desktop * @param iter Current tree item * @param setCompositingValues Whether to set the compositing values */ bool ObjectsPanel::_selectItemCallback(const Gtk::TreeModel::iterator& iter, bool *setCompositingValues, bool *first_pass) { Gtk::TreeModel::Row row = *iter; bool selected = _tree.get_selection()->is_selected(iter); if (selected) { // All items selected in the treeview will be added to the current selection /* Adding/removing only the items that were selected or deselected since the previous call to _pushTreeSelectionToCurrent() * is very slow on large documents, because _desktop->selection->remove(item) needs to traverse the whole ObjectSet to find * the item to be removed. When all N objects are selected in a document, clearing the whole selection would require O(N^2) * That's why we simply clear the complete selection using _desktop->selection->clear(), and re-add all items one by one. * This is much faster. */ /* On the first pass, we will add only the items that were selected before too. Then, on the second pass, we will add the * newly selected items such that the last selected items will be actually last. This is needed for example when the user * wants to align relative to the last selected item. */ if (*first_pass == row[_model->_colPrevSelectionState]) { SPItem *item = row[_model->_colObject]; if (!SP_IS_GROUP(item) || SP_GROUP(item)->layerMode() != SPGroup::LAYER) { //If the item is not a layer, then select it and set the current layer to its parent (if it's the first item) if (_desktop->selection->isEmpty()) { _desktop->setCurrentLayer(item->parent); } _desktop->selection->add(item); } else { //If the item is a layer, set the current layer if (_desktop->selection->isEmpty()) { _desktop->setCurrentLayer(item); } } if (*setCompositingValues) { //Only set the compositing values for the first item <-- TODO: We have this comment here, but this has not actually been implemented? _setCompositingValues(item); *setCompositingValues = false; } } } if (not *first_pass) { row[_model->_colPrevSelectionState] = selected; } return false; } bool ObjectsPanel::_clearPrevSelectionState( const Gtk::TreeModel::iterator& iter) { Gtk::TreeModel::Row row = *iter; row[_model->_colPrevSelectionState] = false; SPItem *item = row[_model->_colObject]; return false; } /** * Handles button sensitivity */ void ObjectsPanel::_checkTreeSelection() { bool sensitive = _tree.get_selection()->count_selected_rows() > 0; //TODO: top/bottom sensitivity bool sensitiveNonTop = true; bool sensitiveNonBottom = true; for (auto & it : _watching) { it->set_sensitive( sensitive ); } for (auto & it : _watchingNonTop) { it->set_sensitive( sensitiveNonTop ); } for (auto & it : _watchingNonBottom) { it->set_sensitive( sensitiveNonBottom ); } _tree.set_reorderable(sensitive); // Reorderable means that we allow drag-and-drop, but we only allow that when at least one row is selected } /** * Sets visibility of items in the tree * @param iter Current item in the tree * @param visible Whether the item should be visible or not */ void ObjectsPanel::_setVisibleIter( const Gtk::TreeModel::iterator& iter, const bool visible ) { Gtk::TreeModel::Row row = *iter; SPItem* item = row[_model->_colObject]; if (item) { item->setHidden( !visible ); row[_model->_colVisible] = visible; item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); } } /** * Sets sensitivity of items in the tree * @param iter Current item in the tree * @param locked Whether the item should be locked */ void ObjectsPanel::_setLockedIter( const Gtk::TreeModel::iterator& iter, const bool locked ) { Gtk::TreeModel::Row row = *iter; SPItem* item = row[_model->_colObject]; if (item) { item->setLocked( locked ); row[_model->_colLocked] = locked; item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); } } /** * 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) { if (!_desktop) return false; unsigned int shortcut; shortcut = sp_shortcut_get_for_event(event); switch (shortcut) { // how to get users key binding for the action “start-interactive-search” ?? // ctrl+f is just the default case GDK_KEY_f | SP_SHORTCUT_CONTROL_MASK: return false; break; // shall we slurp ctrl+w to close panel? // defocus: case GDK_KEY_Escape: if (_desktop->canvas) { gtk_widget_grab_focus (GTK_WIDGET(_desktop->canvas)); return true; } break; } // invoke user defined shortcuts first bool done = sp_shortcut_invoke(shortcut, _desktop); if (done) return true; // handle events for the treeview bool empty = _desktop->selection->isEmpty(); switch (Inkscape::UI::Tools::get_latin_keyval(event)) { case GDK_KEY_Return: case GDK_KEY_KP_Enter: { Gtk::TreeModel::Path path; Gtk::TreeViewColumn *focus_column = nullptr; _tree.get_cursor(path, focus_column); if (focus_column == _name_column && !_text_renderer->property_editable()) { //Rename item _text_renderer->property_editable() = true; _tree.set_cursor(path, *_name_column, true); grab_focus(); return true; } return false; break; } } return false; } /** * Handles mouse events * @param event Mouse event from GDK * @return whether to eat the event (om nom nom) */ bool ObjectsPanel::_handleButtonEvent(GdkEventButton* event) { static unsigned doubleclick = 0; static bool overVisible = false; //Right mouse button was clicked, launch the pop-up menu if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 3) ) { Gtk::TreeModel::Path path; int x = static_cast(event->x); int y = static_cast(event->y); if ( _tree.get_path_at_pos( x, y, path ) ) { _checkTreeSelection(); _popupMenu.popup_at_pointer(reinterpret_cast(event)); if (_tree.get_selection()->is_selected(path)) { return true; } } } //Left mouse button was pressed! In order to handle multiple item drag & drop, //we need to defer selection by setting the select function so that the tree doesn't //automatically select anything. In order to handle multiple item icon clicking, //we need to eat the event. There might be a better way to do both of these... if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1)) { overVisible = false; Gtk::TreeModel::Path path; Gtk::TreeViewColumn* col = nullptr; int x = static_cast(event->x); int y = static_cast(event->y); int x2 = 0; int y2 = 0; if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { if (col == _tree.get_column(COL_VISIBLE-1)) { //Click on visible column, eat this event to keep row selection overVisible = true; return true; } else if (col == _tree.get_column(COL_LOCKED-1) || col == _tree.get_column(COL_TYPE-1) || //col == _tree.get_column(COL_INSERTORDER - 1) || col == _tree.get_column(COL_HIGHLIGHT-1)) { //Click on an icon column, eat this event to keep row selection return true; } else if ( !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) & _tree.get_selection()->is_selected(path) ) { //Click on a selected item with no modifiers, defer selection to the mouse-up by //setting the select function to _noSelection _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &ObjectsPanel::_noSelection)); _defer_target = path; } } } //Restore the selection function to allow tree selection on mouse button release if ( event->type == GDK_BUTTON_RELEASE) { _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &ObjectsPanel::_rowSelectFunction)); } //CellRenderers do not have good support for dealing with multiple items, so //we handle all events on them here if ( (event->type == GDK_BUTTON_RELEASE) && (event->button == 1)) { Gtk::TreeModel::Path path; Gtk::TreeViewColumn* col = nullptr; int x = static_cast(event->x); int y = static_cast(event->y); int x2 = 0; int y2 = 0; if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { if (_defer_target) { //We had deferred a selection target, select it here (assuming no drag & drop) if (_defer_target == path && !(event->x == 0 && event->y == 0)) { _tree.set_cursor(path, *col, false); } _defer_target = Gtk::TreeModel::Path(); } else { if (event->state & GDK_SHIFT_MASK) { // Shift left click on the visible/lock columns toggles "solo" mode if (col == _tree.get_column(COL_VISIBLE - 1)) { _takeAction(BUTTON_SOLO); } else if (col == _tree.get_column(COL_LOCKED - 1)) { _takeAction(BUTTON_LOCK_OTHERS); } } else if (event->state & GDK_MOD1_MASK) { // Alt+left click on the visible/lock columns toggles "solo" mode and preserves selection Gtk::TreeModel::iterator iter = _store->get_iter(path); if (_store->iter_is_valid(iter)) { Gtk::TreeModel::Row row = *iter; SPItem *item = row[_model->_colObject]; if (col == _tree.get_column(COL_VISIBLE - 1)) { _desktop->toggleLayerSolo( item ); DocumentUndo::maybeDone(_desktop->doc(), "layer:solo", SP_VERB_LAYER_SOLO, _("Toggle layer solo")); } else if (col == _tree.get_column(COL_LOCKED - 1)) { _desktop->toggleLockOtherLayers( item ); DocumentUndo::maybeDone(_desktop->doc(), "layer:lockothers", SP_VERB_LAYER_LOCK_OTHERS, _("Lock other layers")); } } } else { Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path); Gtk::TreeModel::Row row = *iter; SPItem* item = row[_model->_colObject]; if (col == _tree.get_column(COL_VISIBLE - 1)) { if (overVisible) { //Toggle visibility bool newValue = !row[_model->_colVisible]; if (_tree.get_selection()->is_selected(path)) { //If the current row is selected, toggle the visibility //for all selected items _tree.get_selection()->selected_foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_setVisibleIter), newValue)); } else { //If the current row is not selected, toggle just its visibility row[_model->_colVisible] = newValue; item->setHidden(!newValue); item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); } DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, newValue? _("Unhide objects") : _("Hide objects")); overVisible = false; } } else if (col == _tree.get_column(COL_LOCKED - 1)) { //Toggle locking bool newValue = !row[_model->_colLocked]; if (_tree.get_selection()->is_selected(path)) { //If the current row is selected, toggle the sensitivity for //all selected items _tree.get_selection()->selected_foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_setLockedIter), newValue)); } else { //If the current row is not selected, toggle just its sensitivity row[_model->_colLocked] = newValue; item->setLocked( newValue ); item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); } DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, newValue? _("Lock objects") : _("Unlock objects")); } else if (col == _tree.get_column(COL_TYPE - 1)) { if (SP_IS_GROUP(item)) { //Toggle the current item between a group and a layer SPGroup * g = SP_GROUP(item); bool newValue = g->layerMode() == SPGroup::LAYER; row[_model->_colType] = newValue ? 1: 2; g->setLayerMode(newValue ? SPGroup::GROUP : SPGroup::LAYER); g->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, newValue? _("Layer to group") : _("Group to layer")); _pushTreeSelectionToCurrent(); } } /*else if (col == _tree.get_column(COL_INSERTORDER - 1)) { if (SP_IS_GROUP(item)) { //Toggle the current item's insert order SPGroup * g = SP_GROUP(item); bool newValue = !g->insertBottom(); row[_model->_colInsertOrder] = newValue ? 2: 1; g->setInsertBottom(newValue); g->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, newValue? _("Set insert mode bottom") : _("Set insert mode top")); } }*/ else if (col == _tree.get_column(COL_HIGHLIGHT - 1)) { //Clear the highlight targets _highlight_target.clear(); if (_tree.get_selection()->is_selected(path)) { //If the current item is selected, store all selected items //in the highlight source _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_storeHighlightTarget)); } else { //If the current item is not selected, store only it in the highlight source _storeHighlightTarget(iter); } if (_selectedColor) { //Set up the color selector SPColor color; color.set( row[_model->_colHighlight] ); _selectedColor->setColorAlpha(color, SP_RGBA32_A_F(row[_model->_colHighlight])); } //Show the color selector dialog _colorSelectorDialog.show(); } } } } } //Second mouse button press, set double click status for when the mouse is released if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { doubleclick = 1; } //Double click on mouse button release, if we're over the label column, edit //the item name if ( event->type == GDK_BUTTON_RELEASE && doubleclick) { doubleclick = 0; Gtk::TreeModel::Path path; Gtk::TreeViewColumn* col = nullptr; int x = static_cast(event->x); int y = static_cast(event->y); int x2 = 0; int y2 = 0; if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) && col == _name_column) { // Double click on the Layer name, enable editing _text_renderer->property_editable() = true; _tree.set_cursor (path, *_name_column, true); grab_focus(); } } return false; } /** * Stores items in the highlight target vector to manipulate with the color selector * @param iter Current tree item to store */ void ObjectsPanel::_storeHighlightTarget(const Gtk::TreeModel::iterator& iter) { Gtk::TreeModel::Row row = *iter; SPItem* item = row[_model->_colObject]; if (item) { _highlight_target.push_back(item); } } /* * Drag and drop within the tree */ bool ObjectsPanel::_handleDragDrop(const Glib::RefPtr& /*context*/, int x, int y, guint /*time*/) { //Set up our defaults and clear the source vector _dnd_into = false; _dnd_target = nullptr; _dnd_source.clear(); _dnd_source_includes_layer = false; //Add all selected items to the source vector _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_storeDragSource)); bool cancel_dnd = false; bool dnd_to_top_at_end = false; Gtk::TreeModel::Path target_path; Gtk::TreeViewDropPosition pos; if (_tree.get_dest_row_at_pos(x, y, target_path, pos)) { // SPItem::moveTo() will be used to move the selected items to their new position, but // moveTo() can only "drop before"; we therefore need to find the next path and drop // the selection just before it, instead of "dropping after" the target path if (pos == Gtk::TREE_VIEW_DROP_AFTER) { Gtk::TreeModel::Path next_path = target_path; if (_tree.row_expanded(next_path)) { next_path.down(); // The next path is at a lower level in the hierarchy, i.e. in a layer or group } else { next_path.next(); // The next path is at the same level } // A next path might however not be present, if we're dropping at the end of the tree view if (_store->iter_is_valid(_store->get_iter(next_path))) { target_path = next_path; } else { // Dragging to the "end" of the treeview ; we'll get the parent group or layer of the last // item, and drop into that parent Gtk::TreeModel::Path up_path = target_path; up_path.up(); if (_store->iter_is_valid(_store->get_iter(up_path))) { // Drop into the parent of the last item target_path = up_path; _dnd_into = true; } else { // Drop into the top level, completely at the end of the treeview; dnd_to_top_at_end = true; } } } if (dnd_to_top_at_end) { g_assert(_dnd_target == nullptr); } else { // Find the SPItem corresponding to the target_path/row at which we're dropping our selection Gtk::TreeModel::iterator iter = _store->get_iter(target_path); if (_store->iter_is_valid(iter)) { Gtk::TreeModel::Row row = *iter; _dnd_target = row[_model->_colObject]; //Set the drop target if ((pos == Gtk::TREE_VIEW_DROP_INTO_OR_BEFORE) or (pos == Gtk::TREE_VIEW_DROP_INTO_OR_AFTER)) { // Trying to drop into a layer or group if (SP_IS_GROUP(_dnd_target)) { _dnd_into = true; } else { // If the target is not a group (or layer), then we cannot drop into it (unless we // would create a group on the fly), so we will cancel the drag and drop action. cancel_dnd = true; } } // If the source selection contains a layer however, then it can not be dropped ... bool c1 = target_path.size() > 1; // .. below the top-level bool c2 = SP_IS_GROUP(_dnd_target) and _dnd_into; // .. or in any group (at the top level) if (_dnd_source_includes_layer and (c1 or c2)) { cancel_dnd = true; } } else { cancel_dnd = true; } } } if (not cancel_dnd) { _takeAction(DRAGNDROP); } return true; // If True: then we're signaling here that nothing needs to be done by the TreeView; we're updating ourselves.. } /** * Stores all selected items as the drag source * @param iter Current tree item */ void ObjectsPanel::_storeDragSource(const Gtk::TreeModel::iterator& iter) { Gtk::TreeModel::Row row = *iter; SPItem* item = row[_model->_colObject]; if (item) { _dnd_source.push_back(item); if (SP_IS_GROUP(item) && (SP_GROUP(item)->layerMode() == SPGroup::LAYER)) { _dnd_source_includes_layer = true; } } } /* * Move a selection of items in response to a drag & drop action */ void ObjectsPanel::_doTreeMove( ) { g_assert(_desktop != nullptr); g_assert(_document != nullptr); std::vector idvector; //Clear the desktop selection _desktop->selection->clear(); while (!_dnd_source.empty()) { SPItem *obj = _dnd_source.back(); _dnd_source.pop_back(); if (obj != _dnd_target) { //Store the object id (for selection later) and move the object idvector.push_back(g_strdup(obj->getId())); obj->moveTo(_dnd_target, _dnd_into); } } //Select items while (!idvector.empty()) { //Grab the id from the vector, get the item in the document and select it gchar * id = idvector.back(); idvector.pop_back(); SPObject *obj = _document->getObjectById(id); g_free(id); if (obj && SP_IS_ITEM(obj)) { SPItem *item = SP_ITEM(obj); if (!SP_IS_GROUP(item) || SP_GROUP(item)->layerMode() != SPGroup::LAYER) { if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item->parent); _desktop->selection->add(item); } else { if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item); } } } DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, _("Moved objects")); } /** * Prevents the treeview from emiting and responding to most signals; needed when it's not up to date */ void ObjectsPanel::_blockAllSignals(bool should_block = true) { // incoming signals _documentChangedCurrentLayer.block(should_block); _isolationConnection.block(should_block); _opacityConnection.block(should_block); _blendConnection.block(should_block); _blurConnection.block(should_block); if (_pending && should_block) { // Kill any pending UI event, e.g. a delete or drag 'n drop action, which could // become unpredictable after the tree has been updated _pending->_signal.disconnect(); } _selectionChangedConnection.block(should_block); // outgoing signal _selectedConnection.block(should_block); // These are not blocked: desktopChangeConn, _documentChangedConnection } /** * Fires the action verb */ void ObjectsPanel::_fireAction( unsigned int code ) { if ( _desktop ) { Verb *verb = Verb::get( code ); if ( verb ) { SPAction *action = verb->get_action(_desktop); if ( action ) { sp_action_perform( action, nullptr ); } } } } bool ObjectsPanel::_executeUpdate() { _objectsChanged(nullptr); return false; } /** * Executes the given button action during the idle time */ void ObjectsPanel::_takeAction( int val ) { if (val == UPDATE_TREE) { _pending_update = true; // We might already have been updating the tree, but new data is available now // so we will then first cancel the old update before scheduling a new one _processQueue_sig.disconnect(); _executeUpdate_sig.disconnect(); _blockAllSignals(true); //_store->clear(); _tree_cache.clear(); _executeUpdate_sig = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_executeUpdate), 500, Glib::PRIORITY_DEFAULT_IDLE+50); // In the spray tool, updating the tree competes in priority with the redrawing of the canvas, // see SPCanvas::addIdle(), which is set to UPDATE_PRIORITY (=G_PRIORITY_DEFAULT_IDLE). We // should take a lower priority (= higher value) to keep the spray tool updating longer, and to prevent // the objects-panel from clogging the processor; however, once the spraying slows down, the tree might // get updated anyway. } else if ( !_pending ) { _pending = new InternalUIBounce(); _pending->_actionCode = val; _pending->_signal = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_executeAction), 0 ); } } /** * Executes the pending button action */ bool ObjectsPanel::_executeAction() { // Make sure selected layer hasn't changed since the action was triggered if ( _document && _pending) { int val = _pending->_actionCode; // SPObject* target = _pending->_target; switch ( val ) { case BUTTON_NEW: { _fireAction( SP_VERB_LAYER_NEW ); } break; case BUTTON_RENAME: { _fireAction( SP_VERB_LAYER_RENAME ); } break; case BUTTON_TOP: { if (_desktop->selection->isEmpty()) { _fireAction( SP_VERB_LAYER_TO_TOP ); } else { _fireAction( SP_VERB_SELECTION_TO_FRONT); } } break; case BUTTON_BOTTOM: { if (_desktop->selection->isEmpty()) { _fireAction( SP_VERB_LAYER_TO_BOTTOM ); } else { _fireAction( SP_VERB_SELECTION_TO_BACK); } } break; case BUTTON_UP: { if (_desktop->selection->isEmpty()) { _fireAction( SP_VERB_LAYER_RAISE ); } else { _fireAction( SP_VERB_SELECTION_STACK_UP ); } } break; case BUTTON_DOWN: { if (_desktop->selection->isEmpty()) { _fireAction( SP_VERB_LAYER_LOWER ); } else { _fireAction( SP_VERB_SELECTION_STACK_DOWN ); } } break; case BUTTON_DUPLICATE: { if (_desktop->selection->isEmpty()) { _fireAction( SP_VERB_LAYER_DUPLICATE ); } else { _fireAction( SP_VERB_EDIT_DUPLICATE ); } } break; case BUTTON_DELETE: { if (_desktop->selection->isEmpty()) { _fireAction( SP_VERB_LAYER_DELETE ); } else { _fireAction( SP_VERB_EDIT_DELETE ); } } break; case BUTTON_SOLO: { _fireAction( SP_VERB_LAYER_SOLO ); } break; case BUTTON_SHOW_ALL: { _fireAction( SP_VERB_LAYER_SHOW_ALL ); } break; case BUTTON_HIDE_ALL: { _fireAction( SP_VERB_LAYER_HIDE_ALL ); } break; case BUTTON_LOCK_OTHERS: { _fireAction( SP_VERB_LAYER_LOCK_OTHERS ); } break; case BUTTON_LOCK_ALL: { _fireAction( SP_VERB_LAYER_LOCK_ALL ); } break; case BUTTON_UNLOCK_ALL: { _fireAction( SP_VERB_LAYER_UNLOCK_ALL ); } break; case BUTTON_CLIPGROUP: { _fireAction ( SP_VERB_OBJECT_CREATE_CLIP_GROUP ); } case BUTTON_SETCLIP: { _fireAction( SP_VERB_OBJECT_SET_CLIPPATH ); } break; case BUTTON_UNSETCLIP: { _fireAction( SP_VERB_OBJECT_UNSET_CLIPPATH ); } break; case BUTTON_SETMASK: { _fireAction( SP_VERB_OBJECT_SET_MASK ); } break; case BUTTON_UNSETMASK: { _fireAction( SP_VERB_OBJECT_UNSET_MASK ); } break; case BUTTON_GROUP: { _fireAction( SP_VERB_SELECTION_GROUP ); } break; case BUTTON_UNGROUP: { _fireAction( SP_VERB_SELECTION_UNGROUP ); } break; case BUTTON_COLLAPSE_ALL: { for (auto& obj: _document->getRoot()->children) { if (SP_IS_GROUP(&obj)) { _setCollapsed(SP_GROUP(&obj)); } } _objectsChanged(_document->getRoot()); } break; case DRAGNDROP: { _doTreeMove( ); // The notifyChildOrderChanged signal will ensure that the TreeView gets updated } break; } delete _pending; _pending = nullptr; } return false; } /** * Handles an unsuccessful item label edit (escape pressed, etc.) */ void ObjectsPanel::_handleEditingCancelled() { _text_renderer->property_editable() = 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) { Gtk::TreeModel::iterator iter = _tree.get_model()->get_iter(path); Gtk::TreeModel::Row row = *iter; _renameObject(row, new_text); _text_renderer->property_editable() = false; } /** * Renames an item in the tree * @param row Tree row * @param name New label to give to the item */ void ObjectsPanel::_renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name) { if ( row && _desktop) { SPItem* item = row[_model->_colObject]; if ( item ) { gchar const* oldLabel = item->label(); if ( !name.empty() && (!oldLabel || name != oldLabel) ) { item->setLabel(name.c_str()); DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, _("Rename object")); } } } } /** * A row selection function used by the tree that doesn't allow any new items to be selected. * Currently, this is used to allow multi-item drag & drop. */ bool ObjectsPanel::_noSelection( Glib::RefPtr const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool /*currentlySelected*/ ) { return false; } /** * Default row selection function taken from the layers dialog */ bool ObjectsPanel::_rowSelectFunction( Glib::RefPtr const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool currentlySelected ) { bool val = true; if ( !currentlySelected && _toggleEvent ) { GdkEvent* event = gtk_get_current_event(); if ( event ) { // (keep these checks separate, so we know when to call gdk_event_free() if ( event->type == GDK_BUTTON_PRESS ) { GdkEventButton const* target = reinterpret_cast(_toggleEvent); GdkEventButton const* evtb = reinterpret_cast(event); if ( (evtb->window == target->window) && (evtb->send_event == target->send_event) && (evtb->time == target->time) && (evtb->state == target->state) ) { // Ooooh! It's a magic one val = false; } } gdk_event_free(event); } } return val; } /** * Sets a group to be collapsed and recursively collapses its children * @param group The group to collapse */ void ObjectsPanel::_setCollapsed(SPGroup * group) { group->setExpanded(false); group->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); for (auto& iter: group->children) { if (SP_IS_GROUP(&iter)) { _setCollapsed(SP_GROUP(&iter)); } } } /** * Sets a group to be expanded or collapsed * @param iter Current tree item * @param isexpanded Whether to expand or collapse */ void ObjectsPanel::_setExpanded(const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& /*path*/, bool isexpanded) { Gtk::TreeModel::Row row = *iter; SPItem* item = row[_model->_colObject]; if (item && SP_IS_GROUP(item)) { if (isexpanded) { //If we're expanding, simply perform the expansion SP_GROUP(item)->setExpanded(isexpanded); item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); } else { //If we're collapsing, we need to recursively collapse, so call our helper function _setCollapsed(SP_GROUP(item)); } } } /** * Callback for when the highlight color is changed * @param csel Color selector * @param cp Objects panel */ void ObjectsPanel::_highlightPickerColorMod() { SPColor color; float alpha = 0; _selectedColor->colorAlpha(color, alpha); guint32 rgba = color.toRGBA32( alpha ); //Set the highlight color for all items in the _highlight_target (all selected items) for (auto target : _highlight_target) { target->setHighlightColor(rgba); target->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); } DocumentUndo::maybeDone(SP_ACTIVE_DOCUMENT, "highlight", SP_VERB_DIALOG_OBJECTS, _("Set object highlight color")); } /** * Callback for when the opacity value is changed */ void ObjectsPanel::_opacityValueChanged() { _blockCompositeUpdate = true; _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_opacityChangedIter)); DocumentUndo::maybeDone(_document, "opacity", SP_VERB_DIALOG_OBJECTS, _("Set object opacity")); _blockCompositeUpdate = false; } /** * Change the opacity of the selected items in the tree * @param iter Current tree item */ void ObjectsPanel::_opacityChangedIter(const Gtk::TreeIter& iter) { Gtk::TreeModel::Row row = *iter; SPItem* item = row[_model->_colObject]; if (item) { item->style->opacity.set = TRUE; item->style->opacity.value = SP_SCALE24_FROM_FLOAT(_filter_modifier.get_opacity_value() / 100); item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); } } /** * Callback for when the isolation value is changed */ void ObjectsPanel::_isolationValueChanged() { _blockCompositeUpdate = true; _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_isolationChangedIter)); DocumentUndo::maybeDone(_document, "isolation", SP_VERB_DIALOG_OBJECTS, _("Set object isolation")); _blockCompositeUpdate = false; } /** * Change the isolation of the selected items in the tree * @param iter Current tree item */ void ObjectsPanel::_isolationChangedIter(const Gtk::TreeIter &iter) { Gtk::TreeModel::Row row = *iter; SPItem *item = row[_model->_colObject]; if (item) { item->style->isolation.set = TRUE; item->style->isolation.value = _filter_modifier.get_isolation_mode(); if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { item->style->mix_blend_mode.set = TRUE; item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); } item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); } } /** * Callback for when the blend mode is changed */ void ObjectsPanel::_blendValueChanged() { _blockCompositeUpdate = true; _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_blendChangedIter)); DocumentUndo::done(_document, SP_VERB_DIALOG_OBJECTS, _("Set object blend mode")); _blockCompositeUpdate = false; } /** * Sets the blend mode of the selected tree items * @param iter Current tree item * @param blendmode Blend mode to set */ void ObjectsPanel::_blendChangedIter(const Gtk::TreeIter &iter) { Gtk::TreeModel::Row row = *iter; SPItem* item = row[_model->_colObject]; if (item) { // < 1.0 filter based blend removal if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) { remove_filter_legacy_blend(item); } item->style->mix_blend_mode.set = TRUE; if (_filter_modifier.get_blend_mode() && item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); } else { item->style->mix_blend_mode.value = _filter_modifier.get_blend_mode(); } item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); } } /** * Callback for when the blur value has changed */ void ObjectsPanel::_blurValueChanged() { _blockCompositeUpdate = true; _tree.get_selection()->selected_foreach_iter(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_blurChangedIter), _filter_modifier.get_blur_value())); DocumentUndo::maybeDone(_document, "blur", SP_VERB_DIALOG_OBJECTS, _("Set object blur")); _blockCompositeUpdate = false; } /** * Sets the blur value for the selected items in the tree * @param iter Current tree item * @param blur Blur value to set */ void ObjectsPanel::_blurChangedIter(const Gtk::TreeIter& iter, double blur) { Gtk::TreeModel::Row row = *iter; SPItem* item = row[_model->_colObject]; if (item) { //Since blur and blend are both filters, we need to set both at the same time SPStyle *style = item->style; if (style) { Geom::OptRect bbox = item->bounds(SPItem::GEOMETRIC_BBOX); double radius; if (bbox) { double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? radius = blur * perimeter / 400; } else { radius = 0; } if (radius != 0) { // The modify function expects radius to be in display pixels. Geom::Affine i2d (item->i2dt_affine()); double expansion = i2d.descrim(); radius *= expansion; SPFilter *filter = modify_filter_gaussian_blur_from_item(_document, item, radius); sp_style_set_property_url(item, "filter", filter, false); } else if (item->style->filter.set && item->style->getFilter()) { for (auto& primitive: item->style->getFilter()->children) { if (!SP_IS_FILTER_PRIMITIVE(&primitive)) { break; } if (SP_IS_GAUSSIANBLUR(&primitive)) { primitive.deleteObject(); break; } } if (!item->style->getFilter()->firstChild()) { remove_filter(item, false); } } item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); } } } /** * Constructor */ ObjectsPanel::ObjectsPanel() : UI::Widget::Panel("/dialogs/objects", SP_VERB_DIALOG_OBJECTS), _rootWatcher(nullptr), _deskTrack(), _desktop(nullptr), _document(nullptr), _model(nullptr), _pending(nullptr), _pending_update(false), _toggleEvent(nullptr), _defer_target(), _visibleHeader(C_("Visibility", "V")), _lockHeader(C_("Lock", "L")), _typeHeader(C_("Type", "T")), _clipmaskHeader(C_("Clip and mask", "CM")), _highlightHeader(C_("Highlight", "HL")), _nameHeader(_("Label")), _filter_modifier( UI::Widget::SimpleFilterModifier::ISOLATION | UI::Widget::SimpleFilterModifier::BLEND | UI::Widget::SimpleFilterModifier::BLUR | UI::Widget::SimpleFilterModifier::OPACITY ), _colorSelectorDialog("dialogs.colorpickerwindow") { //Create the tree model and store ModelColumns *zoop = new ModelColumns(); _model = zoop; _store = Gtk::TreeStore::create( *zoop ); //Set up the tree _tree.set_model( _store ); _tree.set_headers_visible(true); _tree.set_reorderable(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); //Create the column CellRenderers //Visible Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; eyeRenderer->property_activatable() = true; Gtk::TreeViewColumn* col = _tree.get_column(visibleColNum); if ( col ) { col->add_attribute( eyeRenderer->property_active(), _model->_colVisible ); // In order to get tooltips on header, we must create our own label. _visibleHeader.set_tooltip_text(_("Toggle visibility of Layer, Group, or Object.")); _visibleHeader.show(); col->set_widget( _visibleHeader ); } //Locked Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")) ); int lockedColNum = _tree.append_column("lock", *renderer) - 1; renderer->property_activatable() = true; col = _tree.get_column(lockedColNum); if ( col ) { col->add_attribute( renderer->property_active(), _model->_colLocked ); _lockHeader.set_tooltip_text(_("Toggle lock of Layer, Group, or Object.")); _lockHeader.show(); col->set_widget( _lockHeader ); } //Type Inkscape::UI::Widget::LayerTypeIcon * typeRenderer = Gtk::manage( new Inkscape::UI::Widget::LayerTypeIcon()); int typeColNum = _tree.append_column("type", *typeRenderer) - 1; typeRenderer->property_activatable() = true; col = _tree.get_column(typeColNum); if ( col ) { col->add_attribute( typeRenderer->property_active(), _model->_colType ); _typeHeader.set_tooltip_text(_("Type: Layer, Group, or Object. Clicking on Layer or Group icon, toggles between the two types.")); _typeHeader.show(); col->set_widget( _typeHeader ); } //Insert order (LiamW: unused) /*Inkscape::UI::Widget::InsertOrderIcon * insertRenderer = Gtk::manage( new Inkscape::UI::Widget::InsertOrderIcon()); int insertColNum = _tree.append_column("type", *insertRenderer) - 1; col = _tree.get_column(insertColNum); if ( col ) { col->add_attribute( insertRenderer->property_active(), _model->_colInsertOrder ); }*/ //Clip/mask Inkscape::UI::Widget::ClipMaskIcon * clipRenderer = Gtk::manage( new Inkscape::UI::Widget::ClipMaskIcon()); int clipColNum = _tree.append_column("clipmask", *clipRenderer) - 1; col = _tree.get_column(clipColNum); if ( col ) { col->add_attribute( clipRenderer->property_active(), _model->_colClipMask ); _clipmaskHeader.set_tooltip_text(_("Is object clipped and/or masked?")); _clipmaskHeader.show(); col->set_widget( _clipmaskHeader ); } //Highlight Inkscape::UI::Widget::HighlightPicker * highlightRenderer = Gtk::manage( new Inkscape::UI::Widget::HighlightPicker()); int highlightColNum = _tree.append_column("highlight", *highlightRenderer) - 1; col = _tree.get_column(highlightColNum); if ( col ) { col->add_attribute( highlightRenderer->property_active(), _model->_colHighlight ); _highlightHeader.set_tooltip_text(_("Highlight color of outline in Node tool. Click to set. If alpha is zero, use inherited color.")); _highlightHeader.show(); col->set_widget( _highlightHeader ); } //Label _text_renderer = Gtk::manage(new Gtk::CellRendererText()); int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; _name_column = _tree.get_column(nameColNum); if( _name_column ) { _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); _nameHeader.set_tooltip_text(_("Layer/Group/Object label (inkscape:label). Double-click to set. Default value is object 'id'.")); _nameHeader.show(); _name_column->set_widget( _nameHeader ); } //Set the expander and search columns _tree.set_expander_column( *_tree.get_column(nameColNum) ); _tree.set_search_column(_model->_colLabel); // use ctrl+f to start search _tree.set_enable_search(false); //Set up the tree selection _tree.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE); _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &ObjectsPanel::_pushTreeSelectionToCurrent) ); _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &ObjectsPanel::_rowSelectFunction) ); //Set up tree signals _tree.signal_button_press_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false ); _tree.signal_button_release_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false ); _tree.signal_key_press_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleKeyEvent), false ); _tree.signal_drag_drop().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleDragDrop), false); _tree.signal_row_collapsed().connect( sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), false)); _tree.signal_row_expanded().connect( sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), true)); //Set up the label editing signals _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleEdited) ); _text_renderer->signal_editing_canceled().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleEditingCancelled) ); //Set up the scroller window and pack the page _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( _scroller, Gtk::PACK_EXPAND_WIDGET ); //Set up the compositing items _blendConnection = _filter_modifier.signal_blend_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_blendValueChanged)); _blurConnection = _filter_modifier.signal_blur_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_blurValueChanged)); _opacityConnection = _filter_modifier.signal_opacity_changed().connect( sigc::mem_fun(*this, &ObjectsPanel::_opacityValueChanged)); _isolationConnection = _filter_modifier.signal_isolation_changed().connect( sigc::mem_fun(*this, &ObjectsPanel::_isolationValueChanged)); //Pack the compositing functions and the button row _page.pack_end(_filter_modifier, Gtk::PACK_SHRINK); _page.pack_end(_buttonsRow, Gtk::PACK_SHRINK); //Pack into the panel contents _getContents()->pack_start(_page, Gtk::PACK_EXPAND_WIDGET); SPDesktop* targetDesktop = getDesktop(); //Set up the button row //Add object/layer Gtk::Button* btn = Gtk::manage( new Gtk::Button() ); _styleButton(*btn, INKSCAPE_ICON("list-add"), _("Add layer...")); btn->set_relief(Gtk::RELIEF_NONE); btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_NEW) ); _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); //Remove object btn = Gtk::manage( new Gtk::Button() ); _styleButton(*btn, INKSCAPE_ICON("list-remove"), _("Remove object")); btn->set_relief(Gtk::RELIEF_NONE); btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_DELETE) ); _watching.push_back( btn ); _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); //Move to bottom btn = Gtk::manage( new Gtk::Button() ); _styleButton(*btn, INKSCAPE_ICON("go-bottom"), _("Move To Bottom")); btn->set_relief(Gtk::RELIEF_NONE); btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_BOTTOM) ); _watchingNonBottom.push_back( btn ); _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); //Move down btn = Gtk::manage( new Gtk::Button() ); _styleButton(*btn, INKSCAPE_ICON("go-down"), _("Move Down")); btn->set_relief(Gtk::RELIEF_NONE); btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_DOWN) ); _watchingNonBottom.push_back( btn ); _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); //Move up btn = Gtk::manage( new Gtk::Button() ); _styleButton(*btn, INKSCAPE_ICON("go-up"), _("Move Up")); btn->set_relief(Gtk::RELIEF_NONE); btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_UP) ); _watchingNonTop.push_back( btn ); _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); //Move to top btn = Gtk::manage( new Gtk::Button() ); _styleButton(*btn, INKSCAPE_ICON("go-top"), _("Move To Top")); btn->set_relief(Gtk::RELIEF_NONE); btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_TOP) ); _watchingNonTop.push_back( btn ); _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); //Collapse all btn = Gtk::manage( new Gtk::Button() ); _styleButton(*btn, INKSCAPE_ICON("format-indent-less"), _("Collapse All")); btn->set_relief(Gtk::RELIEF_NONE); btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_COLLAPSE_ALL) ); _watchingNonBottom.push_back( btn ); _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); _buttonsRow.pack_start(_buttonsSecondary, Gtk::PACK_EXPAND_WIDGET); _buttonsRow.pack_end(_buttonsPrimary, Gtk::PACK_EXPAND_WIDGET); _watching.push_back(&_filter_modifier); //Set up the pop-up menu // ------------------------------------------------------- { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); _show_contextmenu_icons = prefs->getBool("/theme/menuIcons_objects", true); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RENAME, (int)BUTTON_RENAME ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_NEW, (int)BUTTON_NEW ) ); _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SOLO, (int)BUTTON_SOLO ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SHOW_ALL, (int)BUTTON_SHOW_ALL ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_HIDE_ALL, (int)BUTTON_HIDE_ALL ) ); _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_OTHERS, (int)BUTTON_LOCK_OTHERS ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_ALL, (int)BUTTON_LOCK_ALL ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_UNLOCK_ALL, (int)BUTTON_UNLOCK_ALL ) ); _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); _watchingNonTop.push_back( &_addPopupItem(targetDesktop, SP_VERB_SELECTION_STACK_UP, (int)BUTTON_UP) ); _watchingNonBottom.push_back( &_addPopupItem(targetDesktop, SP_VERB_SELECTION_STACK_DOWN, (int)BUTTON_DOWN) ); _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_SELECTION_GROUP, (int)BUTTON_GROUP ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_SELECTION_UNGROUP, (int)BUTTON_UNGROUP ) ); _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_CLIPPATH, (int)BUTTON_SETCLIP ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_CREATE_CLIP_GROUP, (int)BUTTON_CLIPGROUP ) ); //will never be implemented //_watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_INVERSE_CLIPPATH, (int)BUTTON_SETINVCLIP ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_UNSET_CLIPPATH, (int)BUTTON_UNSETCLIP ) ); _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_MASK, (int)BUTTON_SETMASK ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_UNSET_MASK, (int)BUTTON_UNSETMASK ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_EDIT_DUPLICATE, (int)BUTTON_DUPLICATE ) ); _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_EDIT_DELETE, (int)BUTTON_DELETE ) ); _popupMenu.show_all_children(); // Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items). _popupMenu.signal_map().connect(sigc::mem_fun(static_cast(&_popupMenu), &ContextMenu::ShiftIcons)); } // ------------------------------------------------------- //Set initial sensitivity of buttons for (auto & it : _watching) { it->set_sensitive( false ); } for (auto & it : _watchingNonTop) { it->set_sensitive( false ); } for (auto & it : _watchingNonBottom) { it->set_sensitive( false ); } //Set up the color selection dialog GtkWidget *dlg = GTK_WIDGET(_colorSelectorDialog.gobj()); sp_transientize(dlg); _colorSelectorDialog.hide(); _colorSelectorDialog.set_title (_("Select Highlight Color")); _colorSelectorDialog.set_border_width (4); _colorSelectorDialog.property_modal() = true; _selectedColor.reset(new Inkscape::UI::SelectedColor); Gtk::Widget *color_selector = Gtk::manage(new Inkscape::UI::Widget::ColorNotebook(*_selectedColor)); _colorSelectorDialog.get_content_area()->pack_start ( *color_selector, true, true, 0); _selectedColor->signal_dragged.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); _selectedColor->signal_released.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); _selectedColor->signal_changed.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); color_selector->show(); setDesktop( targetDesktop ); show_all_children(); //Connect the desktop changed connection desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &ObjectsPanel::setDesktop) ); _deskTrack.connect(GTK_WIDGET(gobj())); } /** * Callback method that will be called when the desktop is destroyed */ void ObjectsPanel::_desktopDestroyed(SPDesktop* /*desktop*/) { // We need to make sure that we're not trying to update the tree after the desktop has vanished, e.g. // when closing Inkscape. Preferably, we would have done so in the destructor of the ObjectsPanel. But // as this destructor is never ever called, we will do this by attaching to the desktop_destroyed signal // instead _processQueue_sig.disconnect(); _executeUpdate_sig.disconnect(); _desktop = nullptr; } /** * Destructor */ ObjectsPanel::~ObjectsPanel() { // Never being called, not even when closing Inkscape? //Close the highlight selection dialog _colorSelectorDialog.hide(); //Set the desktop to null, which will disconnect all object watchers setDesktop(nullptr); if ( _model ) { delete _model; _model = nullptr; } if (_pending) { delete _pending; _pending = nullptr; } if ( _toggleEvent ) { gdk_event_free( _toggleEvent ); _toggleEvent = nullptr; } desktopChangeConn.disconnect(); _deskTrack.disconnect(); } /** * Sets the current document */ void ObjectsPanel::setDocument(SPDesktop* /*desktop*/, SPDocument* document) { //Clear all object watchers _removeWatchers(); //Delete the root watcher if (_rootWatcher) { _rootWatcher->_repr->removeObserver(*_rootWatcher); delete _rootWatcher; _rootWatcher = nullptr; } _document = document; if (document && document->getRoot() && document->getRoot()->getRepr()) { //Create a new root watcher for the document and then call _objectsChanged to fill the tree _rootWatcher = new ObjectsPanel::ObjectWatcher(this, document->getRoot()); document->getRoot()->getRepr()->addObserver(*_rootWatcher); _objectsChanged(document->getRoot()); } } /** * Set the current panel desktop */ void ObjectsPanel::setDesktop( SPDesktop* desktop ) { Panel::setDesktop(desktop); if ( desktop != _desktop ) { _documentChangedConnection.disconnect(); _documentChangedCurrentLayer.disconnect(); _selectionChangedConnection.disconnect(); if ( _desktop ) { _desktop = nullptr; } _desktop = Panel::getDesktop(); if ( _desktop ) { //Connect desktop signals _documentChangedConnection = _desktop->connectDocumentReplaced( sigc::mem_fun(*this, &ObjectsPanel::setDocument)); _documentChangedCurrentLayer = _desktop->connectCurrentLayerChanged( sigc::mem_fun(*this, &ObjectsPanel::_objectsChangedWrapper)); _selectionChangedConnection = _desktop->selection->connectChanged( sigc::mem_fun(*this, &ObjectsPanel::_objectsSelected)); _desktopDestroyedConnection = _desktop->connectDestroy( sigc::mem_fun(*this, &ObjectsPanel::_desktopDestroyed)); setDocument(_desktop, _desktop->doc()); } else { setDocument(nullptr, nullptr); } } _deskTrack.setBase(desktop); } } //namespace Dialogs } //namespace UI } //namespace Inkscape //should be okay to put these here because they are never referenced anywhere else using namespace Inkscape::UI::Tools; void SPItem::setHighlightColor(guint32 const color) { g_free(_highlightColor); if (color & 0x000000ff) { _highlightColor = g_strdup_printf("%u", color); } else { _highlightColor = nullptr; } NodeTool *tool = nullptr; if (SP_ACTIVE_DESKTOP ) { Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; if (INK_IS_NODE_TOOL(ec)) { tool = static_cast(ec); tools_switch(tool->desktop, TOOLS_NODES); } } } void SPItem::unsetHighlightColor() { g_free(_highlightColor); _highlightColor = nullptr; NodeTool *tool = nullptr; if (SP_ACTIVE_DESKTOP ) { Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; if (INK_IS_NODE_TOOL(ec)) { tool = static_cast(ec); tools_switch(tool->desktop, TOOLS_NODES); } } } /* 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 :