1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3  * A simple panel for objects (originally developed for Ponyscape, an Inkscape derivative)
4  *
5  * Authors:
6  *   Theodore Janeczko
7  *   Tweaked by Liam P White for use in Inkscape
8  *   Tavmjong Bah
9  *
10  * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
11  *               Tavmjong Bah 2017
12  *
13  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
14  */
15 
16 #include "objects.h"
17 
18 #include <gtkmm/icontheme.h>
19 #include <gtkmm/imagemenuitem.h>
20 #include <gtkmm/separatormenuitem.h>
21 #include <glibmm/main.h>
22 
23 #include "desktop-style.h"
24 #include "desktop.h"
25 #include "document-undo.h"
26 #include "document.h"
27 #include "filter-chemistry.h"
28 #include "inkscape.h"
29 #include "layer-manager.h"
30 #include "verbs.h"
31 
32 #include "helper/action.h"
33 #include "ui/icon-loader.h"
34 
35 #include "include/gtkmm_version.h"
36 
37 #include "object/filters/blend.h"
38 #include "object/filters/gaussian-blur.h"
39 #include "object/sp-clippath.h"
40 #include "object/sp-mask.h"
41 #include "object/sp-root.h"
42 #include "object/sp-shape.h"
43 #include "style.h"
44 
45 #include "ui/dialog-events.h"
46 #include "ui/icon-names.h"
47 #include "ui/selected-color.h"
48 #include "ui/shortcuts.h"
49 #include "ui/desktop/menu-icon-shift.h"
50 #include "ui/tools-switch.h"
51 #include "ui/tools/node-tool.h"
52 
53 #include "ui/widget/canvas.h"
54 #include "ui/widget/clipmaskicon.h"
55 #include "ui/widget/color-notebook.h"
56 #include "ui/widget/highlight-picker.h"
57 #include "ui/widget/imagetoggler.h"
58 #include "ui/widget/insertordericon.h"
59 #include "ui/widget/layertypeicon.h"
60 
61 #include "xml/node-observer.h"
62 
63 //#define DUMP_LAYERS 1
64 
65 namespace Inkscape {
66 namespace UI {
67 namespace Dialog {
68 
69 using Inkscape::XML::Node;
70 
71 /**
72  * Gets an instance of the Objects panel
73  */
getInstance()74 ObjectsPanel& ObjectsPanel::getInstance()
75 {
76     return *new ObjectsPanel();
77 }
78 
79 /**
80  * Column enumeration
81  */
82 enum {
83     COL_VISIBLE = 1,
84     COL_LOCKED,
85     COL_TYPE,
86 //    COL_INSERTORDER,
87     COL_CLIPMASK,
88     COL_HIGHLIGHT
89 };
90 
91 /**
92  * Button enumeration
93  */
94 enum {
95     BUTTON_NEW = 0,
96     BUTTON_RENAME,
97     BUTTON_TOP,
98     BUTTON_BOTTOM,
99     BUTTON_UP,
100     BUTTON_DOWN,
101     BUTTON_DUPLICATE,
102     BUTTON_DELETE,
103     BUTTON_SOLO,
104     BUTTON_SHOW_ALL,
105     BUTTON_HIDE_ALL,
106     BUTTON_LOCK_OTHERS,
107     BUTTON_LOCK_ALL,
108     BUTTON_UNLOCK_ALL,
109     BUTTON_SETCLIP,
110     BUTTON_CLIPGROUP,
111 //    BUTTON_SETINVCLIP,
112     BUTTON_UNSETCLIP,
113     BUTTON_SETMASK,
114     BUTTON_UNSETMASK,
115     BUTTON_GROUP,
116     BUTTON_UNGROUP,
117     BUTTON_COLLAPSE_ALL,
118     DRAGNDROP,
119     UPDATE_TREE
120 };
121 
122 /**
123  * Xml node observer for observing objects in the document
124  */
125 class ObjectsPanel::ObjectWatcher : public Inkscape::XML::NodeObserver {
126 public:
127     /**
128      * Creates a new object watcher
129      * @param pnl The panel to which the object watcher belongs
130      * @param obj The object to watch
131      */
ObjectWatcher(ObjectsPanel * pnl,SPObject * obj)132     ObjectWatcher(ObjectsPanel* pnl, SPObject* obj) :
133         _pnl(pnl),
134         _obj(obj),
135         _repr(obj->getRepr()),
136         _highlightAttr(g_quark_from_string("inkscape:highlight-color")),
137         _lockedAttr(g_quark_from_string("sodipodi:insensitive")),
138         _labelAttr(g_quark_from_string("inkscape:label")),
139         _groupAttr(g_quark_from_string("inkscape:groupmode")),
140         _styleAttr(g_quark_from_string("style")),
141         _clipAttr(g_quark_from_string("clip-path")),
142         _maskAttr(g_quark_from_string("mask"))
143     {
144         _repr->addObserver(*this);
145     }
146 
~ObjectWatcher()147     ~ObjectWatcher() override {
148         _repr->removeObserver(*this);
149     }
150 
notifyChildAdded(Node &,Node &,Node *)151     void notifyChildAdded( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override
152     {
153         if ( _pnl && _obj ) {
154             _pnl->_objectsChangedWrapper( _obj );
155         }
156     }
notifyChildRemoved(Node &,Node &,Node *)157     void notifyChildRemoved( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override
158     {
159         if ( _pnl && _obj ) {
160             _pnl->_objectsChangedWrapper( _obj );
161         }
162     }
notifyChildOrderChanged(Node &,Node &,Node *,Node *)163     void notifyChildOrderChanged( Node &/*node*/, Node &/*child*/, Node */*old_prev*/, Node */*new_prev*/ ) override
164     {
165         if ( _pnl && _obj ) {
166             _pnl->_objectsChangedWrapper( _obj );
167         }
168     }
notifyContentChanged(Node &,Util::ptr_shared,Util::ptr_shared)169     void notifyContentChanged( Node &/*node*/, Util::ptr_shared /*old_content*/, Util::ptr_shared /*new_content*/ ) override {}
notifyAttributeChanged(Node & node,GQuark name,Util::ptr_shared,Util::ptr_shared)170     void notifyAttributeChanged( Node &node, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ ) override {
171         /* Weird things happen on undo! we get notified about the child being removed, but after that we still get
172          * notified for attributes being changed on this XML node! In that case the corresponding SPObject might already
173          * have been deleted and the pointer to might be invalid, leading to a segfault if we're not carefull.
174          * So after we initiated the update of the treeview using _objectsChangedWrapper() in notifyChildRemoved(), the
175          * _pending_update flag is set, and we will no longer process any notifyAttributeChanged()
176          * Reproducing the crash: new document -> open objects panel -> draw freehand line -> undo -> segfault (but only
177          * if we don't check for _pending_update) */
178         if ( _pnl && (!_pnl->_pending_update) && _obj ) {
179             if ( name == _lockedAttr || name == _labelAttr || name == _highlightAttr || name == _groupAttr || name == _styleAttr || name == _clipAttr || name == _maskAttr ) {
180                 _pnl->_updateObject(_obj, name == _highlightAttr);
181                 if ( name == _styleAttr ) {
182                     _pnl->_updateComposite();
183                 }
184             }
185         }
186     }
187 
188     /**
189      * Objects panel to which this watcher belongs
190      */
191     ObjectsPanel* _pnl;
192 
193     /**
194      * The object that is being observed
195      */
196     SPObject* _obj;
197 
198     /**
199      * The xml representation of the object that is being observed
200      */
201     Inkscape::XML::Node* _repr;
202 
203     /* These are quarks which define the attributes that we are observing */
204     GQuark _highlightAttr;
205     GQuark _lockedAttr;
206     GQuark _labelAttr;
207     GQuark _groupAttr;
208     GQuark _styleAttr;
209     GQuark _clipAttr;
210     GQuark _maskAttr;
211 };
212 
213 class ObjectsPanel::InternalUIBounce
214 {
215 public:
216     int _actionCode;
217     sigc::connection _signal;
218 };
219 
220 class ObjectsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord
221 {
222 public:
223 
ModelColumns()224     ModelColumns()
225     {
226         add(_colObject);
227         add(_colVisible);
228         add(_colLocked);
229         add(_colLabel);
230         add(_colType);
231         add(_colHighlight);
232         add(_colClipMask);
233         add(_colPrevSelectionState);
234         //add(_colInsertOrder);
235     }
236     ~ModelColumns() override = default;
237 
238     Gtk::TreeModelColumn<SPItem*> _colObject;
239     Gtk::TreeModelColumn<Glib::ustring> _colLabel;
240     Gtk::TreeModelColumn<bool> _colVisible;
241     Gtk::TreeModelColumn<bool> _colLocked;
242     Gtk::TreeModelColumn<int> _colType;
243     Gtk::TreeModelColumn<guint32> _colHighlight;
244     Gtk::TreeModelColumn<int> _colClipMask;
245     Gtk::TreeModelColumn<bool> _colPrevSelectionState;
246     //Gtk::TreeModelColumn<int> _colInsertOrder;
247 };
248 
249 /**
250  * Stylizes a button using the given icon name and tooltip
251  */
_styleButton(Gtk::Button & btn,char const * iconName,char const * tooltip)252 void ObjectsPanel::_styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip)
253 {
254     auto child = Glib::wrap(sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR));
255     child->show();
256     btn.add(*child);
257     btn.set_relief(Gtk::RELIEF_NONE);
258     btn.set_tooltip_text(tooltip);
259 }
260 
261 /**
262  * Adds an item to the pop-up (right-click) menu
263  * @param desktop The active destktop
264  * @param code Action code
265  * @param id Button id for callback function
266  * @return The generated menu item
267  */
_addPopupItem(SPDesktop * desktop,unsigned int code,int id)268 Gtk::MenuItem& ObjectsPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, int id )
269 {
270     Verb *verb = Verb::get( code );
271     g_assert(verb);
272     SPAction *action = verb->get_action(Inkscape::ActionContext(desktop));
273 
274     Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem());
275 
276     Gtk::Label *label = Gtk::manage(new Gtk::Label(action->name, true));
277     label->set_xalign(0.0);
278 
279     if (_show_contextmenu_icons && action->image) {
280         item->set_name("ImageMenuItem");  // custom name to identify our "ImageMenuItems"
281         Gtk::Image *icon = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU));
282 
283         // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can only hold one child
284         Gtk::Box *box = Gtk::manage(new Gtk::Box());
285         box->pack_start(*icon, false, false, 0);
286         box->pack_start(*label, true,  true,  0);
287         item->add(*box);
288     } else {
289         item->add(*label);
290     }
291 
292     item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_takeAction), id));
293     _popupMenu.append(*item);
294 
295     return *item;
296 }
297 
298 /**
299  * Attach a watcher to the XML node of an item, which will signal us in case of changes to that item or node
300  * @param item The item of which the XML node is to be watched
301  */
_addWatcher(SPItem * item)302 void ObjectsPanel::_addWatcher(SPItem *item) {
303     bool used = true; // Any newly created watcher is obviously being used
304     auto iter = _objectWatchers.find(item);
305     if (iter == _objectWatchers.end()) { // If not found then watcher doesn't exist yet
306         ObjectsPanel::ObjectWatcher *w = new ObjectsPanel::ObjectWatcher(this, item);
307         _objectWatchers.emplace(item, std::make_pair(w, used));
308     } else { // Found; no need to create a new watcher; just flag it as "in use"
309         (*iter).second.second = used;
310     }
311 }
312 
313 /**
314  * Delete the watchers, which signal us in case of changes to the item being watched
315  * @param only_unused Only delete those watchers that are no longer in use
316  */
_removeWatchers(bool only_unused=false)317 void ObjectsPanel::_removeWatchers(bool only_unused = false) {
318     // Delete all watchers (optionally only those which are not in use)
319     auto iter = _objectWatchers.begin();
320     while (iter != _objectWatchers.end()) {
321         bool used = (*iter).second.second;
322         bool delete_watcher = (!only_unused) || (only_unused && !used);
323         if ( delete_watcher ) {
324             ObjectsPanel::ObjectWatcher *w = (*iter).second.first;
325             delete w;
326             iter = _objectWatchers.erase(iter);
327         } else {
328             // It must be in use, so the used "field" should be set to true;
329             // However, when _removeWatchers is being called, we will already have processed the complete queue ...
330             g_assert(_tree_update_queue.empty());
331             // .. and we can preemptively flag it as unused for the processing of the next queue
332             (*iter).second.second = false; // It will be set to true again by _addWatcher, if in use
333             iter++;
334         }
335     }
336 }
337 /**
338  * Call function for asynchronous invocation of _objectsChanged
339  */
_objectsChangedWrapper(SPObject *)340 void ObjectsPanel::_objectsChangedWrapper(SPObject */*obj*/) {
341     // We used to call _objectsChanged with a reference to _obj,
342     // but since _obj wasn't used, I'm dropping that for now
343     _takeAction(UPDATE_TREE);
344 }
345 
346 /**
347  * Callback function for when an object changes.  Essentially refreshes the entire tree
348  * @param obj Object which was changed (currently not used as the entire tree is recreated)
349  */
_objectsChanged(SPObject *)350 void ObjectsPanel::_objectsChanged(SPObject */*obj*/)
351 {
352     if (_desktop) {
353         //Get the current document's root and use that to enumerate the tree
354         SPDocument* document = _desktop->doc();
355         SPRoot* root = document->getRoot();
356         if ( root ) {
357             _selectedConnection.block(); // Will be unblocked after the queue has been processed fully
358             _documentChangedCurrentLayer.block();
359 
360             //Clear the tree store
361             _store->clear(); // This will increment it's stamp, making all old iterators
362             _tree_cache.clear(); // invalid. So we will also clear our own cache, as well
363             _tree_update_queue.clear(); // as any remaining update queue
364 
365             // Temporarily detach the TreeStore from the TreeView to slightly reduce flickering, and to speed up
366             // Note: if we truly want to eliminate the flickering, we should implement double buffering on the _store,
367             // but maybe this is a bit too much effort/bloat for too little gain?
368             _tree.unset_model();
369 
370             //Add all items recursively; we will do this asynchronously, by first filling a queue, which is rather fast
371             _queueObject( root, nullptr );
372             //However, the processing of this queue is slow, so this is done at a low priority and in small chunks. Using
373             //only small chunks keeps Inkscape responsive, for example while using the spray tool. After processing each
374             //of the chunks, Inkscape will check if there are other tasks with a high priority, for example when user is
375             //spraying. If so, the sprayed objects will be added first, and the whole updating will be restarted before
376             //it even finished.
377             _paths_to_be_expanded.clear();
378             _processQueue_sig.disconnect(); // Might be needed in case objectsChanged is called directly, and not through objectsChangedWrapper()
379             _processQueue_sig = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_processQueue), 0, Glib::PRIORITY_DEFAULT_IDLE+100);
380         }
381     }
382 }
383 
384 /**
385  * Recursively adds the children of the given item to the tree
386  * @param obj Root object to add to the tree
387  * @param parentRow Parent tree row (or NULL if adding to tree root)
388  */
_queueObject(SPObject * obj,Gtk::TreeModel::Row * parentRow)389 void ObjectsPanel::_queueObject(SPObject* obj, Gtk::TreeModel::Row* parentRow)
390 {
391     bool already_expanded = false;
392 
393     for(auto& child: obj->children) {
394         if (SP_IS_ITEM(&child)) {
395             //Add the item to the tree, basically only creating an empty row in the tree view
396             Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend();
397 
398             //Add the item to a queue, so we can fill in the data in each row asynchronously
399             //at a later stage. See the comments in _objectsChanged() for more details
400             bool expand = SP_IS_GROUP(obj) && SP_GROUP(obj)->expanded() && (not already_expanded);
401             _tree_update_queue.emplace_back(SP_ITEM(&child), iter, expand);
402 
403             already_expanded = expand || already_expanded; // We need to expand only a single child in each group
404 
405             //If the item is a group, recursively add its children
406             if (SP_IS_GROUP(&child)) {
407                 Gtk::TreeModel::Row row = *iter;
408                 _queueObject(&child, &row);
409             }
410         }
411     }
412 }
413 
414 /**
415  * Walks through the queue in small chunks, and fills in the rows in the tree view accordingly
416  * @return False if the queue has been fully emptied
417  */
_processQueue()418 bool ObjectsPanel::_processQueue() {
419     auto *desktop = getDesktop();
420     if (!desktop) {
421         return false;
422     }
423 
424     auto queue_iter = _tree_update_queue.begin();
425     auto queue_end  = _tree_update_queue.end();
426     int count = 0;
427 
428     while (queue_iter != queue_end) {
429         //The queue is a list of tuples; expand the tuples
430         SPItem *item                    = std::get<0>(*queue_iter);
431         Gtk::TreeModel::iterator iter   = std::get<1>(*queue_iter);
432         bool expanded                   = std::get<2>(*queue_iter);
433         //Add the object to the tree view and tree cache
434         _addObjectToTree(item, *iter, expanded);
435         _tree_cache.emplace(item, *iter);
436 
437         /* Update the watchers; No watcher shall be deleted before the processing of the queue has
438          * finished; we need to keep watching for items that might have been deleted while the queue,
439          * which is being processed on idle, was not yet empty. This is because when an item is deleted, the
440          * queue is still holding a pointer to it. The NotifyChildRemoved method of the watcher will stop the
441          * processing of the queue and prevent a segmentation fault, but only if there is a watcher in place*/
442         _addWatcher(item);
443 
444         queue_iter = _tree_update_queue.erase(queue_iter);
445         count++;
446         if (count == 100 && (!_tree_update_queue.empty())) {
447             return true; // we have not yet reached the end of the queue, so return true to keep the timeout signal alive
448         }
449     }
450 
451     //We have reached the end of the queue, and it is safe to remove any watchers
452     _removeWatchers(true); // ... but only remove those that are no longer in use
453 
454     // Now we can bring the tree view back to life safely
455     _tree.set_model(_store); // Attach the store again to the tree view this sets search columns as -1
456     _tree.set_search_column(_model->_colLabel);//set search column again
457 
458     // Expand the tree; this is kept outside of _addObjectToTree() and _processQueue() to allow
459     // temporarily detaching the store from the tree, which slightly reduces flickering
460     for (auto path: _paths_to_be_expanded) {
461         _tree.expand_to_path(path);
462         _tree.collapse_row(path);
463     }
464 
465     _blockAllSignals(false);
466     _objectsSelected(desktop->selection); //Set the tree selection; will also invoke _checkTreeSelection()
467     _pending_update = false;
468     return false; // Return false to kill the timeout signal that kept calling _processQueue
469 }
470 
471 /**
472  * Fills in the details of an item in the already existing row of the tree view
473  * @param item Item of which the name, visibility, lock status, etc, will be filled in
474  * @param row Row where the item is residing
475  * @param expanded True if the item is part of a group that is shown as expanded in the tree view
476  */
_addObjectToTree(SPItem * item,const Gtk::TreeModel::Row & row,bool expanded)477 void ObjectsPanel::_addObjectToTree(SPItem* item, const Gtk::TreeModel::Row &row, bool expanded)
478 {
479     SPGroup * group = SP_IS_GROUP(item) ? SP_GROUP(item) : nullptr;
480 
481     row[_model->_colObject] = item;
482     gchar const * label = item->label() ? item->label() : item->getId();
483     row[_model->_colLabel] = label ? label : item->defaultLabel();
484     row[_model->_colVisible] = !item->isHidden();
485     row[_model->_colLocked] = !item->isSensitive();
486     row[_model->_colType] = group ? (group->layerMode() == SPGroup::LAYER ? 2 : 1) : 0;
487     row[_model->_colHighlight] = item->isHighlightSet() ? item->highlight_color() : item->highlight_color() & 0xffffff00;
488     row[_model->_colClipMask] = item ? (
489         (item->getClipObject() ? 1 : 0) |
490         (item->getMaskObject() ? 2 : 0)
491     ) : 0;
492     row[_model->_colPrevSelectionState] = false;
493     //row[_model->_colInsertOrder] = group ? (group->insertBottom() ? 2 : 1) : 0;
494 
495     //If our parent object is a group and it's expanded, expand the tree
496     if (expanded) {
497         _paths_to_be_expanded.emplace_back(_store->get_path(row));
498     }
499 }
500 
501 /**
502  * Updates an item in the tree and optionally recursively updates the item's children
503  * @param obj The item to update in the tree
504  * @param recurse Whether to recurse through the item's children
505  */
_updateObject(SPObject * obj,bool recurse)506 void ObjectsPanel::_updateObject( SPObject *obj, bool recurse ) {
507     Gtk::TreeModel::iterator tree_iter;
508     if (_findInTreeCache(SP_ITEM(obj), tree_iter)) {
509         Gtk::TreeModel::Row row = *tree_iter;
510 
511         //We found our item in the tree; now update it!
512         SPItem * item = SP_IS_ITEM(obj) ? SP_ITEM(obj) : nullptr;
513         SPGroup * group = SP_IS_GROUP(obj) ? SP_GROUP(obj) : nullptr;
514 
515         gchar const * label = obj->label() ? obj->label() : obj->getId();
516         row[_model->_colLabel] = label ? label : obj->defaultLabel();
517         row[_model->_colVisible] = item ? !item->isHidden() : false;
518         row[_model->_colLocked] = item ? !item->isSensitive() : false;
519         row[_model->_colType] = group ? (group->layerMode() == SPGroup::LAYER ? 2 : 1) : 0;
520         row[_model->_colHighlight] = item ? (item->isHighlightSet() ? item->highlight_color() : item->highlight_color() & 0xffffff00) : 0;
521         row[_model->_colClipMask] = item ? (
522             (item->getClipObject() ? 1 : 0) |
523             (item->getMaskObject() ? 2 : 0)
524         ) : 0;
525         //row[_model->_colInsertOrder] = group ? (group->insertBottom() ? 2 : 1) : 0;
526 
527         if (recurse){
528             for (auto& iter: obj->children) {
529                 _updateObject(&iter, recurse);
530             }
531         }
532     }
533 }
534 
535 /**
536  * Updates the composite controls for the selected item
537  */
_updateComposite()538 void ObjectsPanel::_updateComposite() {
539     if (!_blockCompositeUpdate)
540     {
541         //Set the default values
542         bool setValues = true;
543 
544         //Get/set the values
545         _tree.get_selection()->selected_foreach_iter(sigc::bind<bool *>(sigc::mem_fun(*this, &ObjectsPanel::_compositingChanged), &setValues));
546 
547     }
548 }
549 
550 /**
551  * Sets the compositing values for the first selected item in the tree
552  * @param iter Current tree item
553  * @param setValues Whether to set the compositing values
554  */
_compositingChanged(const Gtk::TreeModel::iterator & iter,bool * setValues)555 void ObjectsPanel::_compositingChanged( const Gtk::TreeModel::iterator& iter, bool *setValues )
556 {
557     if (iter) {
558         Gtk::TreeModel::Row row = *iter;
559         SPItem *item = row[_model->_colObject];
560         if (*setValues)
561         {
562             _setCompositingValues(item);
563             *setValues = false;
564         }
565     }
566 }
567 
568 /**
569  * Occurs when the current desktop selection changes
570  * @param sel The current selection
571  */
_objectsSelected(Selection * sel)572 void ObjectsPanel::_objectsSelected( Selection *sel ) {
573 
574     bool setOpacity = true;
575     _selectedConnection.block();
576 
577     _tree.get_selection()->unselect_all();
578     _store->foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_clearPrevSelectionState));
579 
580     SPItem *item = nullptr;
581     auto items = sel->items();
582     for(auto i=items.begin(); i!=items.end(); ++i){
583         item = *i;
584         if (setOpacity)
585         {
586             _setCompositingValues(item);
587             setOpacity = false;
588         }
589         _updateObjectSelected(item, (*i)==items.back(), false);
590     }
591     if (!item) {
592         if (_desktop->currentLayer() && SP_IS_ITEM(_desktop->currentLayer())) {
593             item = SP_ITEM(_desktop->currentLayer());
594             _setCompositingValues(item);
595             _updateObjectSelected(item, false, true);
596         }
597     }
598     _selectedConnection.unblock();
599     _checkTreeSelection();
600 }
601 
602 /**
603  * Helper function for setting the compositing values
604  * @param item Item to use for setting the compositing values
605  */
_setCompositingValues(SPItem * item)606 void ObjectsPanel::_setCompositingValues(SPItem *item)
607 {
608     // Block the connections to avoid interference
609     _isolationConnection.block();
610     _opacityConnection.block();
611     _blendConnection.block();
612     _blurConnection.block();
613 
614     // Set the isolation
615     auto isolation = item->style->isolation.set ? item->style->isolation.value : SP_CSS_ISOLATION_AUTO;
616     _filter_modifier.set_isolation_mode(isolation, true);
617     // Set the opacity
618     double opacity = (item->style->opacity.set ? SP_SCALE24_TO_FLOAT(item->style->opacity.value) : 1);
619     opacity *= 100; // Display in percent.
620     _filter_modifier.set_opacity_value(opacity);
621     // Set the blend mode
622     if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) {
623         _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, true);
624     } else {
625         _filter_modifier.set_blend_mode(item->style->mix_blend_mode.value, true);
626     }
627     if (_filter_modifier.get_blend_mode() == SP_CSS_BLEND_NORMAL) {
628         if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) {
629             auto blend = filter_get_legacy_blend(item);
630             _filter_modifier.set_blend_mode(blend, true);
631         }
632     }
633     SPGaussianBlur *spblur = nullptr;
634     if (item->style->getFilter()) {
635         for (auto& primitive_obj: item->style->getFilter()->children) {
636             if (!SP_IS_FILTER_PRIMITIVE(&primitive_obj)) {
637                 break;
638             }
639             if (SP_IS_GAUSSIANBLUR(&primitive_obj) && !spblur) {
640                 //Get the blur value
641                 spblur = SP_GAUSSIANBLUR(&primitive_obj);
642             }
643         }
644     }
645 
646     //Set the blur value
647     double blur_value = 0;
648     if (spblur) {
649         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
650         if (bbox) {
651             double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y];   // fixme: this is only half the perimeter, is that correct?
652             blur_value = spblur->stdDeviation.getNumber() * 400 / perimeter;
653         }
654     }
655     _filter_modifier.set_blur_value(blur_value);
656 
657     //Unblock connections
658     _isolationConnection.unblock();
659     _blurConnection.unblock();
660     _blendConnection.unblock();
661     _opacityConnection.unblock();
662 }
663 
664 // See the comment in objects.h for _tree_cache
665 /**
666  * Find the specified item in the tree cache
667  * @param iter Current tree item
668  * @param tree_iter Tree_iter will point to the row in which the tree item was found
669  * @return True if found
670  */
_findInTreeCache(SPItem * item,Gtk::TreeModel::iterator & tree_iter)671 bool ObjectsPanel::_findInTreeCache(SPItem* item, Gtk::TreeModel::iterator &tree_iter) {
672     if (not item) {
673         return false;
674     }
675 
676     try {
677         tree_iter = _tree_cache.at(item);
678     }
679     catch (std::out_of_range) {
680         // Apparently, item cannot be found in the tree_cache, which could mean that
681         // - the tree and/or tree_cache are out-dated or in the process of being updated.
682         // - a layer is selected, which is not visible in the objects panel (see _objectsSelected())
683         // Anyway, this doesn't seem all that critical, so no warnings; just return false
684         return false;
685     }
686 
687     /* If the row in the tree has been deleted, and an old tree_cache is being used, then we will
688      * get a segmentation fault crash somewhere here; so make sure iters don't linger around!
689      * We can only check the validity as done below, but this is rather slow according to the
690      * documentation (adds 0.25 s for a 2k long tree). But better safe than sorry
691      */
692     if (not _store->iter_is_valid(tree_iter)) {
693         g_critical("Invalid iterator to Gtk::tree in objects panel; just prevented a segfault!");
694         return false;
695     }
696 
697     return true;
698 }
699 
700 
701 /**
702  * Find the specified item in the tree store and (de)select it, optionally scrolling to the item
703  * @param item Item to select in the tree
704  * @param scrollto Whether to scroll to the item
705  * @param expand If true, the path in the tree towards item will be expanded
706  */
_updateObjectSelected(SPItem * item,bool scrollto,bool expand)707 void ObjectsPanel::_updateObjectSelected(SPItem* item, bool scrollto, bool expand)
708 {
709     Gtk::TreeModel::iterator tree_iter;
710     if (_findInTreeCache(item, tree_iter)) {
711         Gtk::TreeModel::Row row = *tree_iter;
712 
713         //We found the item! Expand to the path and select it in the tree.
714         Gtk::TreePath path = _store->get_path(tree_iter);
715         _tree.expand_to_path( path );
716         if (!expand)
717             // but don't expand itself, just the path
718             _tree.collapse_row(path);
719 
720         Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection();
721 
722         select->select(tree_iter);
723         row[_model->_colPrevSelectionState] = true;
724         if (scrollto) {
725             //Scroll to the item in the tree
726             _tree.scroll_to_row(path, 0.5);
727         }
728     }
729 }
730 
731 /**
732  * Pushes the current tree selection to the canvas
733  */
_pushTreeSelectionToCurrent()734 void ObjectsPanel::_pushTreeSelectionToCurrent()
735 {
736     if ( _desktop && _desktop->currentRoot() ) {
737         //block connections for selection and compositing values to prevent interference
738         _selectionChangedConnection.block();
739         _documentChangedCurrentLayer.block();
740         //Clear the selection and then iterate over the tree selection, pushing each item to the desktop
741         _desktop->selection->clear();
742         if (_tree.get_selection()->count_selected_rows() == 0) {
743             _store->foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_clearPrevSelectionState));
744         }
745         bool setOpacity = true;
746         bool first_pass = true;
747         _store->foreach_iter(sigc::bind<bool *>(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass));
748         first_pass = false;
749         _store->foreach_iter(sigc::bind<bool *>(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass));
750 
751         //unblock connections, unless we were already blocking them beforehand
752         _selectionChangedConnection.unblock();
753         _documentChangedCurrentLayer.unblock();
754 
755         _checkTreeSelection();
756     }
757 }
758 
759 /**
760  * Helper function for pushing the current tree selection to the current desktop
761  * @param iter Current tree item
762  * @param setCompositingValues Whether to set the compositing values
763  */
_selectItemCallback(const Gtk::TreeModel::iterator & iter,bool * setCompositingValues,bool * first_pass)764 bool ObjectsPanel::_selectItemCallback(const Gtk::TreeModel::iterator& iter, bool *setCompositingValues, bool *first_pass)
765 {
766     Gtk::TreeModel::Row row = *iter;
767     bool selected = _tree.get_selection()->is_selected(iter);
768     if (selected) { // All items selected in the treeview will be added to the current selection
769         /* Adding/removing only the items that were selected or deselected since the previous call to _pushTreeSelectionToCurrent()
770          * is very slow on large documents, because _desktop->selection->remove(item) needs to traverse the whole ObjectSet to find
771          * the item to be removed. When all N objects are selected in a document, clearing the whole selection would require O(N^2)
772          * That's why we simply clear the complete selection using _desktop->selection->clear(), and re-add all items one by one.
773          * This is much faster.
774          */
775 
776         /* On the first pass, we will add only the items that were selected before too. Then, on the second pass, we will add the
777          * newly selected items such that the last selected items will be actually last. This is needed for example when the user
778          * wants to align relative to the last selected item.
779          */
780         if (*first_pass == row[_model->_colPrevSelectionState]) {
781             SPItem *item = row[_model->_colObject];
782             if (!SP_IS_GROUP(item) || SP_GROUP(item)->layerMode() != SPGroup::LAYER) {
783                 //If the item is not a layer, then select it and set the current layer to its parent (if it's the first item)
784                 if (_desktop->selection->isEmpty()) {
785                     _desktop->setCurrentLayer(item->parent);
786                 }
787                 _desktop->selection->add(item);
788             } else {
789                 //If the item is a layer, set the current layer
790                 if (_desktop->selection->isEmpty()) {
791                     _desktop->setCurrentLayer(item);
792                 }
793             }
794             if (*setCompositingValues) {
795                 //Only set the compositing values for the first item <-- TODO: We have this comment here, but this has not actually been implemented?
796                 _setCompositingValues(item);
797                 *setCompositingValues = false;
798             }
799         }
800     }
801 
802     if (not *first_pass) {
803         row[_model->_colPrevSelectionState] = selected;
804     }
805 
806     return false;
807 }
808 
_clearPrevSelectionState(const Gtk::TreeModel::iterator & iter)809 bool ObjectsPanel::_clearPrevSelectionState( const Gtk::TreeModel::iterator& iter) {
810     Gtk::TreeModel::Row row = *iter;
811     row[_model->_colPrevSelectionState] = false;
812     return false;
813 }
814 
815 /**
816  * Handles button sensitivity
817  */
_checkTreeSelection()818 void ObjectsPanel::_checkTreeSelection()
819 {
820     bool sensitive = _tree.get_selection()->count_selected_rows() > 0;
821     //TODO: top/bottom sensitivity
822     bool sensitiveNonTop = true;
823     bool sensitiveNonBottom = true;
824 
825     for (auto & it : _watching) {
826         it->set_sensitive( sensitive );
827     }
828     for (auto & it : _watchingNonTop) {
829         it->set_sensitive( sensitiveNonTop );
830     }
831     for (auto & it : _watchingNonBottom) {
832         it->set_sensitive( sensitiveNonBottom );
833     }
834 
835     _tree.set_reorderable(sensitive); // Reorderable means that we allow drag-and-drop, but we only allow that when at least one row is selected
836 }
837 
838 /**
839  * Sets visibility of items in the tree
840  * @param iter Current item in the tree
841  * @param visible Whether the item should be visible or not
842  */
_setVisibleIter(const Gtk::TreeModel::iterator & iter,const bool visible)843 void ObjectsPanel::_setVisibleIter( const Gtk::TreeModel::iterator& iter, const bool visible )
844 {
845     Gtk::TreeModel::Row row = *iter;
846     SPItem* item = row[_model->_colObject];
847     if (item)
848     {
849         item->setHidden( !visible );
850         row[_model->_colVisible] = visible;
851         item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
852     }
853 }
854 
855 /**
856  * Sets sensitivity of items in the tree
857  * @param iter Current item in the tree
858  * @param locked Whether the item should be locked
859  */
_setLockedIter(const Gtk::TreeModel::iterator & iter,const bool locked)860 void ObjectsPanel::_setLockedIter( const Gtk::TreeModel::iterator& iter, const bool locked )
861 {
862     Gtk::TreeModel::Row row = *iter;
863     SPItem* item = row[_model->_colObject];
864     if (item)
865     {
866         item->setLocked( locked );
867         row[_model->_colLocked] = locked;
868         item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
869     }
870 }
871 
872 /**
873  * Handles keyboard events
874  * @param event Keyboard event passed in from GDK
875  * @return Whether the event should be eaten (om nom nom)
876  */
_handleKeyEvent(GdkEventKey * event)877 bool ObjectsPanel::_handleKeyEvent(GdkEventKey *event)
878 {
879     if (!_desktop)
880         return false;
881 
882     Gtk::AccelKey shortcut = Inkscape::Shortcuts::get_from_event(event);
883 
884     switch (shortcut.get_key()) {
885         // how to get users key binding for the action “start-interactive-search” ??
886         // ctrl+f is just the default
887         case GDK_KEY_f:
888             if (shortcut.get_mod() | Gdk::CONTROL_MASK) return false;
889             break;
890         // shall we slurp ctrl+w to close panel?
891 
892         // defocus:
893         case GDK_KEY_Escape:
894             if (_desktop->canvas) {
895                 _desktop->canvas->grab_focus();
896                 return true;
897             }
898             break;
899     }
900 
901     // invoke user defined shortcuts first
902     bool done = Inkscape::Shortcuts::getInstance().invoke_verb(event, _desktop);
903     if (done) {
904         return true;
905     }
906 
907     // handle events for the treeview
908     //  bool empty = _desktop->selection->isEmpty();
909 
910     switch (Inkscape::UI::Tools::get_latin_keyval(event)) {
911         case GDK_KEY_Return:
912         case GDK_KEY_KP_Enter:
913         {
914             Gtk::TreeModel::Path path;
915             Gtk::TreeViewColumn *focus_column = nullptr;
916 
917             _tree.get_cursor(path, focus_column);
918             if (focus_column == _name_column && !_text_renderer->property_editable()) {
919                 //Rename item
920                 _text_renderer->property_editable() = true;
921                 _tree.set_cursor(path, *_name_column, true);
922                 grab_focus();
923                 return true;
924             }
925             return false;
926             break;
927         }
928     }
929     return false;
930 }
931 
932 /**
933  * Handles mouse events
934  * @param event Mouse event from GDK
935  * @return whether to eat the event (om nom nom)
936  */
_handleButtonEvent(GdkEventButton * event)937 bool ObjectsPanel::_handleButtonEvent(GdkEventButton* event)
938 {
939     static unsigned doubleclick = 0;
940     static bool overVisible = false;
941 
942     //Right mouse button was clicked, launch the pop-up menu
943     if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 3) ) {
944         Gtk::TreeModel::Path path;
945         int x = static_cast<int>(event->x);
946         int y = static_cast<int>(event->y);
947         if ( _tree.get_path_at_pos( x, y, path ) ) {
948             _checkTreeSelection();
949             _popupMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
950 
951             if (_tree.get_selection()->is_selected(path)) {
952                 return true;
953             }
954         }
955     }
956 
957     //Left mouse button was pressed!  In order to handle multiple item drag & drop,
958     //we need to defer selection by setting the select function so that the tree doesn't
959     //automatically select anything.  In order to handle multiple item icon clicking,
960     //we need to eat the event.  There might be a better way to do both of these...
961     if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1)) {
962         overVisible = false;
963         Gtk::TreeModel::Path path;
964         Gtk::TreeViewColumn* col = nullptr;
965         int x = static_cast<int>(event->x);
966         int y = static_cast<int>(event->y);
967         int x2 = 0;
968         int y2 = 0;
969         if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) {
970             if (col == _tree.get_column(COL_VISIBLE-1)) {
971                 //Click on visible column, eat this event to keep row selection
972                 overVisible = true;
973                 return true;
974             } else if (col == _tree.get_column(COL_LOCKED-1) ||
975                     col == _tree.get_column(COL_TYPE-1) ||
976                         //col == _tree.get_column(COL_INSERTORDER - 1) ||
977                     col == _tree.get_column(COL_HIGHLIGHT-1)) {
978                 //Click on an icon column, eat this event to keep row selection
979                 return true;
980             } else if ( !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) & _tree.get_selection()->is_selected(path) ) {
981                 //Click on a selected item with no modifiers, defer selection to the mouse-up by
982                 //setting the select function to _noSelection
983                 _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &ObjectsPanel::_noSelection));
984                 _defer_target = path;
985             }
986         }
987     }
988 
989     //Restore the selection function to allow tree selection on mouse button release
990     if ( event->type == GDK_BUTTON_RELEASE) {
991         _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &ObjectsPanel::_rowSelectFunction));
992     }
993 
994     //CellRenderers do not have good support for dealing with multiple items, so
995     //we handle all events on them here
996     if ( (event->type == GDK_BUTTON_RELEASE) && (event->button == 1)) {
997 
998         Gtk::TreeModel::Path path;
999         Gtk::TreeViewColumn* col = nullptr;
1000         int x = static_cast<int>(event->x);
1001         int y = static_cast<int>(event->y);
1002         int x2 = 0;
1003         int y2 = 0;
1004         if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) {
1005             if (_defer_target) {
1006                 //We had deferred a selection target, select it here (assuming no drag & drop)
1007                 if (_defer_target == path && !(event->x == 0 && event->y == 0))
1008                 {
1009                     _tree.set_cursor(path, *col, false);
1010                 }
1011                 _defer_target = Gtk::TreeModel::Path();
1012             }
1013             else {
1014                 if (event->state & GDK_SHIFT_MASK) {
1015                     // Shift left click on the visible/lock columns toggles "solo" mode
1016                     if (col == _tree.get_column(COL_VISIBLE - 1)) {
1017                         _takeAction(BUTTON_SOLO);
1018                     } else if (col == _tree.get_column(COL_LOCKED - 1)) {
1019                         _takeAction(BUTTON_LOCK_OTHERS);
1020                     }
1021                 } else if (event->state & GDK_MOD1_MASK) {
1022                     // Alt+left click on the visible/lock columns toggles "solo" mode and preserves selection
1023                     Gtk::TreeModel::iterator iter = _store->get_iter(path);
1024                     if (_store->iter_is_valid(iter)) {
1025                         Gtk::TreeModel::Row row = *iter;
1026                         SPItem *item = row[_model->_colObject];
1027                         if (col == _tree.get_column(COL_VISIBLE - 1)) {
1028                             _desktop->toggleLayerSolo( item );
1029                             DocumentUndo::maybeDone(_desktop->doc(), "layer:solo", SP_VERB_LAYER_SOLO, _("Toggle layer solo"));
1030                         } else if (col == _tree.get_column(COL_LOCKED - 1)) {
1031                             _desktop->toggleLockOtherLayers( item );
1032                             DocumentUndo::maybeDone(_desktop->doc(), "layer:lockothers", SP_VERB_LAYER_LOCK_OTHERS, _("Lock other layers"));
1033                         }
1034                     }
1035                 } else {
1036                     Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path);
1037                     Gtk::TreeModel::Row row = *iter;
1038 
1039                     SPItem* item = row[_model->_colObject];
1040 
1041                     if (col == _tree.get_column(COL_VISIBLE - 1)) {
1042                         if (overVisible) {
1043                             //Toggle visibility
1044                             bool newValue = !row[_model->_colVisible];
1045                             if (_tree.get_selection()->is_selected(path))
1046                             {
1047                                 //If the current row is selected, toggle the visibility
1048                                 //for all selected items
1049                                 _tree.get_selection()->selected_foreach_iter(sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setVisibleIter), newValue));
1050                             }
1051                             else
1052                             {
1053                                 //If the current row is not selected, toggle just its visibility
1054                                 row[_model->_colVisible] = newValue;
1055                                 item->setHidden(!newValue);
1056                                 item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
1057                             }
1058                             DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS,
1059                                             newValue? _("Unhide objects") : _("Hide objects"));
1060                             overVisible = false;
1061                         }
1062                     } else if (col == _tree.get_column(COL_LOCKED - 1)) {
1063                         //Toggle locking
1064                         bool newValue = !row[_model->_colLocked];
1065                         if (_tree.get_selection()->is_selected(path))
1066                         {
1067                             //If the current row is selected, toggle the sensitivity for
1068                             //all selected items
1069                             _tree.get_selection()->selected_foreach_iter(sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setLockedIter), newValue));
1070                         }
1071                         else
1072                         {
1073                             //If the current row is not selected, toggle just its sensitivity
1074                             row[_model->_colLocked] = newValue;
1075                             item->setLocked( newValue );
1076                             item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
1077                         }
1078                         DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS,
1079                                             newValue? _("Lock objects") : _("Unlock objects"));
1080 
1081                     } else if (col == _tree.get_column(COL_TYPE - 1)) {
1082                         if (SP_IS_GROUP(item))
1083                         {
1084                             //Toggle the current item between a group and a layer
1085                             SPGroup * g = SP_GROUP(item);
1086                             bool newValue = g->layerMode() == SPGroup::LAYER;
1087                             row[_model->_colType] = newValue ? 1: 2;
1088                             g->setLayerMode(newValue ? SPGroup::GROUP : SPGroup::LAYER);
1089                             g->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
1090                             DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS,
1091                                             newValue? _("Layer to group") : _("Group to layer"));
1092                             _pushTreeSelectionToCurrent();
1093                         }
1094                     } /*else if (col == _tree.get_column(COL_INSERTORDER - 1)) {
1095                         if (SP_IS_GROUP(item))
1096                         {
1097                             //Toggle the current item's insert order
1098                             SPGroup * g = SP_GROUP(item);
1099                             bool newValue = !g->insertBottom();
1100                             row[_model->_colInsertOrder] = newValue ? 2: 1;
1101                             g->setInsertBottom(newValue);
1102                             g->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
1103                             DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS,
1104                                             newValue? _("Set insert mode bottom") : _("Set insert mode top"));
1105                         }
1106                     }*/ else if (col == _tree.get_column(COL_HIGHLIGHT - 1)) {
1107                         //Clear the highlight targets
1108                         _highlight_target.clear();
1109                         if (_tree.get_selection()->is_selected(path))
1110                         {
1111                             //If the current item is selected, store all selected items
1112                             //in the highlight source
1113                             _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_storeHighlightTarget));
1114                         } else {
1115                             //If the current item is not selected, store only it in the highlight source
1116                             _storeHighlightTarget(iter);
1117                         }
1118                         if (_selectedColor)
1119                         {
1120                             //Set up the color selector
1121                             SPColor color;
1122                             color.set( row[_model->_colHighlight] );
1123                             _selectedColor->setColorAlpha(color, SP_RGBA32_A_F(row[_model->_colHighlight]));
1124                         }
1125                         //Show the color selector dialog
1126                         _colorSelectorDialog.show();
1127                     }
1128                 }
1129             }
1130         }
1131     }
1132 
1133     //Second mouse button press, set double click status for when the mouse is released
1134     if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) {
1135         doubleclick = 1;
1136     }
1137 
1138     //Double click on mouse button release, if we're over the label column, edit
1139     //the item name
1140     if ( event->type == GDK_BUTTON_RELEASE && doubleclick) {
1141         doubleclick = 0;
1142         Gtk::TreeModel::Path path;
1143         Gtk::TreeViewColumn* col = nullptr;
1144         int x = static_cast<int>(event->x);
1145         int y = static_cast<int>(event->y);
1146         int x2 = 0;
1147         int y2 = 0;
1148         if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) && col == _name_column) {
1149             // Double click on the Layer name, enable editing
1150             _text_renderer->property_editable() = true;
1151             _tree.set_cursor (path, *_name_column, true);
1152             grab_focus();
1153         }
1154     }
1155 
1156     return false;
1157 }
1158 
1159 /**
1160  * Stores items in the highlight target vector to manipulate with the color selector
1161  * @param iter Current tree item to store
1162  */
_storeHighlightTarget(const Gtk::TreeModel::iterator & iter)1163 void ObjectsPanel::_storeHighlightTarget(const Gtk::TreeModel::iterator& iter)
1164 {
1165     Gtk::TreeModel::Row row = *iter;
1166     SPItem* item = row[_model->_colObject];
1167     if (item)
1168     {
1169         _highlight_target.push_back(item);
1170     }
1171 }
1172 
1173 /*
1174  * Drag and drop within the tree
1175  */
_handleDragDrop(const Glib::RefPtr<Gdk::DragContext> &,int x,int y,guint)1176 bool ObjectsPanel::_handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/)
1177 {
1178     //Set up our defaults and clear the source vector
1179     _dnd_into = false;
1180     _dnd_target = nullptr;
1181     _dnd_source.clear();
1182     _dnd_source_includes_layer = false;
1183 
1184     //Add all selected items to the source vector
1185     _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_storeDragSource));
1186 
1187     bool cancel_dnd = false;
1188     bool dnd_to_top_at_end = false;
1189 
1190     Gtk::TreeModel::Path target_path;
1191     Gtk::TreeViewDropPosition pos;
1192     if (_tree.get_dest_row_at_pos(x, y, target_path, pos)) {
1193         // SPItem::moveTo() will be used to move the selected items to their new position, but
1194         // moveTo() can only "drop before"; we therefore need to find the next path and drop
1195         // the selection just before it, instead of "dropping after" the target path
1196         if (pos == Gtk::TREE_VIEW_DROP_AFTER) {
1197             Gtk::TreeModel::Path next_path = target_path;
1198             if (_tree.row_expanded(next_path)) {
1199                 next_path.down(); // The next path is at a lower level in the hierarchy, i.e. in a layer or group
1200             } else {
1201                 next_path.next(); // The next path is at the same level
1202             }
1203             // A next path might however not be present, if we're dropping at the end of the tree view
1204             if (_store->iter_is_valid(_store->get_iter(next_path))) {
1205                 target_path = next_path;
1206             } else {
1207                 // Dragging to the "end" of the treeview ; we'll get the parent group or layer of the last
1208                 // item, and drop into that parent
1209                 Gtk::TreeModel::Path up_path = target_path;
1210                 up_path.up();
1211                 if (_store->iter_is_valid(_store->get_iter(up_path))) {
1212                     // Drop into the parent of the last item
1213                     target_path = up_path;
1214                     _dnd_into = true;
1215                 } else {
1216                     // Drop into the top level, completely at the end of the treeview;
1217                     dnd_to_top_at_end = true;
1218                 }
1219             }
1220         }
1221 
1222         if (dnd_to_top_at_end) {
1223             g_assert(_dnd_target == nullptr);
1224         } else {
1225             // Find the SPItem corresponding to the target_path/row at which we're dropping our selection
1226             Gtk::TreeModel::iterator iter = _store->get_iter(target_path);
1227             if (_store->iter_is_valid(iter)) {
1228                 Gtk::TreeModel::Row row = *iter;
1229                 _dnd_target = row[_model->_colObject]; //Set the drop target
1230                 if ((pos == Gtk::TREE_VIEW_DROP_INTO_OR_BEFORE) || (pos == Gtk::TREE_VIEW_DROP_INTO_OR_AFTER)) {
1231                     // Trying to drop into a layer or group
1232                     if (SP_IS_GROUP(_dnd_target)) {
1233                         _dnd_into = true;
1234                     } else {
1235                         // If the target is not a group (or layer), then we cannot drop into it (unless we
1236                         // would create a group on the fly), so we will cancel the drag and drop action.
1237                         cancel_dnd = true;
1238                     }
1239                 }
1240                 // If the source selection contains a layer however, then it can not be dropped ...
1241                 bool c1 = target_path.size() > 1;                   // .. below the top-level
1242                 bool c2 = SP_IS_GROUP(_dnd_target) && _dnd_into;   // .. or in any group (at the top level)
1243                 if (SP_IS_GROUP(_dnd_target) && SP_GROUP(_dnd_target)->layerMode() != SPGroup::LAYER &&
1244                     _dnd_source_includes_layer &&
1245                     (c1 || c2))
1246                 {
1247                     cancel_dnd = true;
1248                 }
1249             } else {
1250                 cancel_dnd = true;
1251             }
1252         }
1253     }
1254 
1255     if (not cancel_dnd) {
1256         _takeAction(DRAGNDROP);
1257     }
1258 
1259     return true; // If True: then we're signaling here that nothing needs to be done by the TreeView; we're updating ourselves..
1260 }
1261 
1262 /**
1263  * Stores all selected items as the drag source
1264  * @param iter Current tree item
1265  */
_storeDragSource(const Gtk::TreeModel::iterator & iter)1266 void ObjectsPanel::_storeDragSource(const Gtk::TreeModel::iterator& iter)
1267 {
1268     Gtk::TreeModel::Row row = *iter;
1269     SPItem* item = row[_model->_colObject];
1270     if (item) {
1271         _dnd_source.push_back(item);
1272         if (SP_IS_GROUP(item) && (SP_GROUP(item)->layerMode() == SPGroup::LAYER)) {
1273             _dnd_source_includes_layer = true;
1274         }
1275     }
1276 }
1277 
1278 /*
1279  * Move a selection of items in response to a drag & drop action
1280  */
_doTreeMove()1281 void ObjectsPanel::_doTreeMove( )
1282 {
1283     g_assert(_desktop != nullptr);
1284     g_assert(_document != nullptr);
1285 
1286     std::vector<gchar *> idvector;
1287 
1288     //Clear the desktop selection
1289     _desktop->selection->clear();
1290     while (!_dnd_source.empty())
1291     {
1292         SPItem *obj = _dnd_source.back();
1293         _dnd_source.pop_back();
1294 
1295         if (obj != _dnd_target) {
1296             //Store the object id (for selection later) and move the object
1297             idvector.push_back(g_strdup(obj->getId()));
1298             obj->moveTo(_dnd_target, _dnd_into);
1299         }
1300     }
1301     //Select items
1302     while (!idvector.empty()) {
1303         //Grab the id from the vector, get the item in the document and select it
1304         gchar * id = idvector.back();
1305         idvector.pop_back();
1306         SPObject *obj = _document->getObjectById(id);
1307         g_free(id);
1308         if (obj && SP_IS_ITEM(obj)) {
1309             SPItem *item = SP_ITEM(obj);
1310             if (!SP_IS_GROUP(item) || SP_GROUP(item)->layerMode() != SPGroup::LAYER)
1311             {
1312                 if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item->parent);
1313                 _desktop->selection->add(item);
1314             }
1315             else
1316             {
1317                 if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item);
1318             }
1319         }
1320     }
1321 
1322     DocumentUndo::done( _desktop->doc() , SP_VERB_NONE,
1323                                             _("Moved objects"));
1324 }
1325 
1326 /**
1327  * Prevents the treeview from emiting and responding to most signals; needed when it's not up to date
1328  */
_blockAllSignals(bool should_block=true)1329 void ObjectsPanel::_blockAllSignals(bool should_block = true) {
1330 
1331     // incoming signals
1332     _documentChangedCurrentLayer.block(should_block);
1333     _isolationConnection.block(should_block);
1334     _opacityConnection.block(should_block);
1335     _blendConnection.block(should_block);
1336     _blurConnection.block(should_block);
1337     if (_pending && should_block) {
1338         // Kill any pending UI event, e.g. a delete or drag 'n drop action, which could
1339         // become unpredictable after the tree has been updated
1340         _pending->_signal.disconnect();
1341     }
1342 
1343     _selectionChangedConnection.block(should_block);
1344     // outgoing signal
1345     _selectedConnection.block(should_block);
1346 
1347     // These are not blocked: desktopChangeConn, _documentChangedConnection
1348 }
1349 
1350 /**
1351  * Fires the action verb
1352  */
_fireAction(unsigned int code)1353 void ObjectsPanel::_fireAction( unsigned int code )
1354 {
1355     if ( _desktop ) {
1356         Verb *verb = Verb::get( code );
1357         if ( verb ) {
1358             SPAction *action = verb->get_action(_desktop);
1359             if ( action ) {
1360                 sp_action_perform( action, nullptr );
1361             }
1362         }
1363     }
1364 }
1365 
_executeUpdate()1366 bool ObjectsPanel::_executeUpdate() {
1367     _objectsChanged(nullptr);
1368     return false;
1369 }
1370 
1371 /**
1372  * Executes the given button action during the idle time
1373  */
_takeAction(int val)1374 void ObjectsPanel::_takeAction( int val )
1375 {
1376     if (val == UPDATE_TREE) {
1377         _pending_update = true;
1378         // We might already have been updating the tree, but new data is available now
1379         // so we will then first cancel the old update before scheduling a new one
1380         _processQueue_sig.disconnect();
1381         _executeUpdate_sig.disconnect();
1382         _blockAllSignals(true);
1383         //_store->clear();
1384         _tree_cache.clear();
1385         _executeUpdate_sig = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_executeUpdate), 500, Glib::PRIORITY_DEFAULT_IDLE+50);
1386         // In the spray tool, updating the tree competes in priority with the redrawing of the canvas,
1387         // see SPCanvas::addIdle(), which is set to UPDATE_PRIORITY (=G_PRIORITY_DEFAULT_IDLE). We
1388         // should take a lower priority (= higher value) to keep the spray tool updating longer, and to prevent
1389         // the objects-panel from clogging the processor; however, once the spraying slows down, the tree might
1390         // get updated anyway.
1391     } else if ( !_pending ) {
1392         _pending = new InternalUIBounce();
1393         _pending->_actionCode = val;
1394         _pending->_signal = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_executeAction), 0 );
1395     }
1396 }
1397 
1398 /**
1399  * Executes the pending button action
1400  */
_executeAction()1401 bool ObjectsPanel::_executeAction()
1402 {
1403     // Make sure selected layer hasn't changed since the action was triggered
1404     if ( _document && _pending)
1405     {
1406         int val = _pending->_actionCode;
1407 //        SPObject* target = _pending->_target;
1408 
1409         switch ( val ) {
1410             case BUTTON_NEW:
1411             {
1412                 _fireAction( SP_VERB_LAYER_NEW );
1413             }
1414             break;
1415             case BUTTON_RENAME:
1416             {
1417                 _fireAction( SP_VERB_LAYER_RENAME );
1418             }
1419             break;
1420             case BUTTON_TOP:
1421             {
1422                 if (_desktop->selection->isEmpty())
1423                 {
1424                     _fireAction( SP_VERB_LAYER_TO_TOP );
1425                 }
1426                 else
1427                 {
1428                     _fireAction( SP_VERB_SELECTION_TO_FRONT);
1429                 }
1430             }
1431             break;
1432             case BUTTON_BOTTOM:
1433             {
1434                 if (_desktop->selection->isEmpty())
1435                 {
1436                     _fireAction( SP_VERB_LAYER_TO_BOTTOM );
1437                 }
1438                 else
1439                 {
1440                     _fireAction( SP_VERB_SELECTION_TO_BACK);
1441                 }
1442             }
1443             break;
1444             case BUTTON_UP:
1445             {
1446                 if (_desktop->selection->isEmpty())
1447                 {
1448                     _fireAction( SP_VERB_LAYER_RAISE );
1449                 }
1450                 else
1451                 {
1452                     _fireAction( SP_VERB_SELECTION_STACK_UP );
1453                 }
1454             }
1455             break;
1456             case BUTTON_DOWN:
1457             {
1458                 if (_desktop->selection->isEmpty())
1459                 {
1460                     _fireAction( SP_VERB_LAYER_LOWER );
1461                 }
1462                 else
1463                 {
1464                     _fireAction( SP_VERB_SELECTION_STACK_DOWN );
1465                 }
1466             }
1467             break;
1468             case BUTTON_DUPLICATE:
1469             {
1470                 if (_desktop->selection->isEmpty())
1471                 {
1472                     _fireAction( SP_VERB_LAYER_DUPLICATE );
1473                 }
1474                 else
1475                 {
1476                     _fireAction( SP_VERB_EDIT_DUPLICATE );
1477                 }
1478             }
1479             break;
1480             case BUTTON_DELETE:
1481             {
1482                 if (_desktop->selection->isEmpty())
1483                 {
1484                     _fireAction( SP_VERB_LAYER_DELETE );
1485                 }
1486                 else
1487                 {
1488                     _fireAction( SP_VERB_EDIT_DELETE );
1489                 }
1490             }
1491             break;
1492             case BUTTON_SOLO:
1493             {
1494                 _fireAction( SP_VERB_LAYER_SOLO );
1495             }
1496             break;
1497             case BUTTON_SHOW_ALL:
1498             {
1499                 _fireAction( SP_VERB_LAYER_SHOW_ALL );
1500             }
1501             break;
1502             case BUTTON_HIDE_ALL:
1503             {
1504                 _fireAction( SP_VERB_LAYER_HIDE_ALL );
1505             }
1506             break;
1507             case BUTTON_LOCK_OTHERS:
1508             {
1509                 _fireAction( SP_VERB_LAYER_LOCK_OTHERS );
1510             }
1511             break;
1512             case BUTTON_LOCK_ALL:
1513             {
1514                 _fireAction( SP_VERB_LAYER_LOCK_ALL );
1515             }
1516             break;
1517             case BUTTON_UNLOCK_ALL:
1518             {
1519                 _fireAction( SP_VERB_LAYER_UNLOCK_ALL );
1520             }
1521             break;
1522             case BUTTON_CLIPGROUP:
1523             {
1524                _fireAction ( SP_VERB_OBJECT_CREATE_CLIP_GROUP );
1525             }
1526             case BUTTON_SETCLIP:
1527             {
1528                 _fireAction( SP_VERB_OBJECT_SET_CLIPPATH );
1529             }
1530             break;
1531             case BUTTON_UNSETCLIP:
1532             {
1533                 _fireAction( SP_VERB_OBJECT_UNSET_CLIPPATH );
1534             }
1535             break;
1536             case BUTTON_SETMASK:
1537             {
1538                 _fireAction( SP_VERB_OBJECT_SET_MASK );
1539             }
1540             break;
1541             case BUTTON_UNSETMASK:
1542             {
1543                 _fireAction( SP_VERB_OBJECT_UNSET_MASK );
1544             }
1545             break;
1546             case BUTTON_GROUP:
1547             {
1548                 _fireAction( SP_VERB_SELECTION_GROUP );
1549             }
1550             break;
1551             case BUTTON_UNGROUP:
1552             {
1553                 _fireAction( SP_VERB_SELECTION_UNGROUP );
1554             }
1555             break;
1556             case BUTTON_COLLAPSE_ALL:
1557             {
1558                 for (auto& obj: _document->getRoot()->children) {
1559                     if (SP_IS_GROUP(&obj)) {
1560                         _setCollapsed(SP_GROUP(&obj));
1561                     }
1562                 }
1563                 _objectsChanged(_document->getRoot());
1564             }
1565             break;
1566             case DRAGNDROP:
1567             {
1568                 _doTreeMove( );
1569                 // The notifyChildOrderChanged signal will ensure that the TreeView gets updated
1570             }
1571             break;
1572         }
1573 
1574         delete _pending;
1575         _pending = nullptr;
1576     }
1577 
1578     return false;
1579 }
1580 
1581 /**
1582  * Handles an unsuccessful item label edit (escape pressed, etc.)
1583  */
_handleEditingCancelled()1584 void ObjectsPanel::_handleEditingCancelled()
1585 {
1586     _text_renderer->property_editable() = false;
1587 }
1588 
1589 /**
1590  * Handle a successful item label edit
1591  * @param path Tree path of the item currently being edited
1592  * @param new_text New label text
1593  */
_handleEdited(const Glib::ustring & path,const Glib::ustring & new_text)1594 void ObjectsPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text)
1595 {
1596     Gtk::TreeModel::iterator iter = _tree.get_model()->get_iter(path);
1597     Gtk::TreeModel::Row row = *iter;
1598 
1599     _renameObject(row, new_text);
1600     _text_renderer->property_editable() = false;
1601 }
1602 
1603 /**
1604  * Renames an item in the tree
1605  * @param row Tree row
1606  * @param name New label to give to the item
1607  */
_renameObject(Gtk::TreeModel::Row row,const Glib::ustring & name)1608 void ObjectsPanel::_renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name)
1609 {
1610     if ( row && _desktop) {
1611         SPItem* item = row[_model->_colObject];
1612         if ( item ) {
1613             gchar const* oldLabel = item->label();
1614             if ( !name.empty() && (!oldLabel || name != oldLabel) ) {
1615                 item->setLabel(name.c_str());
1616                 DocumentUndo::done( _desktop->doc() , SP_VERB_NONE,
1617                                                     _("Rename object"));
1618             }
1619         }
1620     }
1621 }
1622 
1623 /**
1624  * A row selection function used by the tree that doesn't allow any new items to be selected.
1625  * Currently, this is used to allow multi-item drag & drop.
1626  */
_noSelection(Glib::RefPtr<Gtk::TreeModel> const &,Gtk::TreeModel::Path const &,bool)1627 bool ObjectsPanel::_noSelection( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool /*currentlySelected*/ )
1628 {
1629     return false;
1630 }
1631 
1632 /**
1633  * Default row selection function taken from the layers dialog
1634  */
_rowSelectFunction(Glib::RefPtr<Gtk::TreeModel> const &,Gtk::TreeModel::Path const &,bool currentlySelected)1635 bool ObjectsPanel::_rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool currentlySelected )
1636 {
1637     bool val = true;
1638     if ( !currentlySelected && _toggleEvent )
1639     {
1640         GdkEvent* event = gtk_get_current_event();
1641         if ( event ) {
1642             // (keep these checks separate, so we know when to call gdk_event_free()
1643             if ( event->type == GDK_BUTTON_PRESS ) {
1644                 GdkEventButton const* target = reinterpret_cast<GdkEventButton const*>(_toggleEvent);
1645                 GdkEventButton const* evtb = reinterpret_cast<GdkEventButton const*>(event);
1646 
1647                 if ( (evtb->window == target->window)
1648                      && (evtb->send_event == target->send_event)
1649                      && (evtb->time == target->time)
1650                      && (evtb->state == target->state)
1651                     )
1652                 {
1653                     // Ooooh! It's a magic one
1654                     val = false;
1655                 }
1656             }
1657             gdk_event_free(event);
1658         }
1659     }
1660     return val;
1661 }
1662 
1663 /**
1664  * Sets a group to be collapsed and recursively collapses its children
1665  * @param group The group to collapse
1666  */
_setCollapsed(SPGroup * group)1667 void ObjectsPanel::_setCollapsed(SPGroup * group)
1668 {
1669     group->setExpanded(false);
1670     group->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
1671     for (auto& iter: group->children) {
1672         if (SP_IS_GROUP(&iter)) {
1673             _setCollapsed(SP_GROUP(&iter));
1674         }
1675     }
1676 }
1677 
1678 /**
1679  * Sets a group to be expanded or collapsed
1680  * @param iter Current tree item
1681  * @param isexpanded Whether to expand or collapse
1682  */
_setExpanded(const Gtk::TreeModel::iterator & iter,const Gtk::TreeModel::Path &,bool isexpanded)1683 void ObjectsPanel::_setExpanded(const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& /*path*/, bool isexpanded)
1684 {
1685     Gtk::TreeModel::Row row = *iter;
1686 
1687     SPItem* item = row[_model->_colObject];
1688     if (item && SP_IS_GROUP(item))
1689     {
1690         if (isexpanded)
1691         {
1692             //If we're expanding, simply perform the expansion
1693             SP_GROUP(item)->setExpanded(isexpanded);
1694             item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
1695         }
1696         else
1697         {
1698             //If we're collapsing, we need to recursively collapse, so call our helper function
1699             _setCollapsed(SP_GROUP(item));
1700         }
1701     }
1702 }
1703 
1704 /**
1705  * Callback for when the highlight color is changed
1706  * @param csel Color selector
1707  * @param cp Objects panel
1708  */
_highlightPickerColorMod()1709 void ObjectsPanel::_highlightPickerColorMod()
1710 {
1711     SPColor color;
1712     float alpha = 0;
1713     _selectedColor->colorAlpha(color, alpha);
1714 
1715     guint32 rgba = color.toRGBA32( alpha );
1716 
1717     //Set the highlight color for all items in the _highlight_target (all selected items)
1718     for (auto target : _highlight_target)
1719     {
1720         target->setHighlightColor(rgba);
1721         target->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
1722     }
1723     DocumentUndo::maybeDone(SP_ACTIVE_DOCUMENT, "highlight", SP_VERB_DIALOG_OBJECTS, _("Set object highlight color"));
1724 }
1725 
1726 /**
1727  * Callback for when the opacity value is changed
1728  */
_opacityValueChanged()1729 void ObjectsPanel::_opacityValueChanged()
1730 {
1731     _blockCompositeUpdate = true;
1732     _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_opacityChangedIter));
1733     DocumentUndo::maybeDone(_document, "opacity", SP_VERB_DIALOG_OBJECTS, _("Set object opacity"));
1734     _blockCompositeUpdate = false;
1735 }
1736 
1737 /**
1738  * Change the opacity of the selected items in the tree
1739  * @param iter Current tree item
1740  */
_opacityChangedIter(const Gtk::TreeIter & iter)1741 void ObjectsPanel::_opacityChangedIter(const Gtk::TreeIter& iter)
1742 {
1743     Gtk::TreeModel::Row row = *iter;
1744     SPItem* item = row[_model->_colObject];
1745     if (item)
1746     {
1747         item->style->opacity.set = TRUE;
1748         item->style->opacity.value = SP_SCALE24_FROM_FLOAT(_filter_modifier.get_opacity_value() / 100);
1749         item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
1750     }
1751 }
1752 
1753 /**
1754  * Callback for when the isolation value is changed
1755  */
_isolationValueChanged()1756 void ObjectsPanel::_isolationValueChanged()
1757 {
1758     _blockCompositeUpdate = true;
1759     _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_isolationChangedIter));
1760     DocumentUndo::maybeDone(_document, "isolation", SP_VERB_DIALOG_OBJECTS, _("Set object isolation"));
1761     _blockCompositeUpdate = false;
1762 }
1763 
1764 /**
1765  * Change the isolation of the selected items in the tree
1766  * @param iter Current tree item
1767  */
_isolationChangedIter(const Gtk::TreeIter & iter)1768 void ObjectsPanel::_isolationChangedIter(const Gtk::TreeIter &iter)
1769 {
1770     Gtk::TreeModel::Row row = *iter;
1771     SPItem *item = row[_model->_colObject];
1772     if (item) {
1773         item->style->isolation.set = TRUE;
1774         item->style->isolation.value = _filter_modifier.get_isolation_mode();
1775         if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) {
1776             item->style->mix_blend_mode.set = TRUE;
1777             item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL;
1778             _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false);
1779         }
1780         item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
1781     }
1782 }
1783 
1784 /**
1785  * Callback for when the blend mode is changed
1786  */
_blendValueChanged()1787 void ObjectsPanel::_blendValueChanged()
1788 {
1789     _blockCompositeUpdate = true;
1790     _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_blendChangedIter));
1791     DocumentUndo::done(_document, SP_VERB_DIALOG_OBJECTS, _("Set object blend mode"));
1792     _blockCompositeUpdate = false;
1793 }
1794 
1795 /**
1796  * Sets the blend mode of the selected tree items
1797  * @param iter Current tree item
1798  * @param blendmode Blend mode to set
1799  */
_blendChangedIter(const Gtk::TreeIter & iter)1800 void ObjectsPanel::_blendChangedIter(const Gtk::TreeIter &iter)
1801 {
1802     Gtk::TreeModel::Row row = *iter;
1803     SPItem* item = row[_model->_colObject];
1804     if (item)
1805     {
1806         // < 1.0 filter based blend removal
1807         if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) {
1808             remove_filter_legacy_blend(item);
1809         }
1810         item->style->mix_blend_mode.set = TRUE;
1811         if (_filter_modifier.get_blend_mode() &&
1812             item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE)
1813         {
1814             item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL;
1815             _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false);
1816         } else {
1817             item->style->mix_blend_mode.value = _filter_modifier.get_blend_mode();
1818         }
1819         item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
1820     }
1821 }
1822 
1823 /**
1824  * Callback for when the blur value has changed
1825  */
_blurValueChanged()1826 void ObjectsPanel::_blurValueChanged()
1827 {
1828     _blockCompositeUpdate = true;
1829     _tree.get_selection()->selected_foreach_iter(sigc::bind<double>(sigc::mem_fun(*this, &ObjectsPanel::_blurChangedIter), _filter_modifier.get_blur_value()));
1830     DocumentUndo::maybeDone(_document, "blur", SP_VERB_DIALOG_OBJECTS, _("Set object blur"));
1831     _blockCompositeUpdate = false;
1832 }
1833 
1834 /**
1835  * Sets the blur value for the selected items in the tree
1836  * @param iter Current tree item
1837  * @param blur Blur value to set
1838  */
_blurChangedIter(const Gtk::TreeIter & iter,double blur)1839 void ObjectsPanel::_blurChangedIter(const Gtk::TreeIter& iter, double blur)
1840 {
1841     Gtk::TreeModel::Row row = *iter;
1842     SPItem* item = row[_model->_colObject];
1843     if (item)
1844     {
1845         //Since blur and blend are both filters, we need to set both at the same time
1846         SPStyle *style = item->style;
1847         if (style) {
1848             Geom::OptRect bbox = item->bounds(SPItem::GEOMETRIC_BBOX);
1849             double radius;
1850             if (bbox) {
1851                 double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y];   // fixme: this is only half the perimeter, is that correct?
1852                 radius = blur * perimeter / 400;
1853             } else {
1854                 radius = 0;
1855             }
1856 
1857             if (radius != 0) {
1858                 // The modify function expects radius to be in display pixels.
1859                 Geom::Affine i2d (item->i2dt_affine());
1860                 double expansion = i2d.descrim();
1861                 radius *= expansion;
1862                 SPFilter *filter = modify_filter_gaussian_blur_from_item(_document, item, radius);
1863                 sp_style_set_property_url(item, "filter", filter, false);
1864             } else if (item->style->filter.set && item->style->getFilter()) {
1865                 for (auto& primitive: item->style->getFilter()->children) {
1866                     if (!SP_IS_FILTER_PRIMITIVE(&primitive)) {
1867                         break;
1868                     }
1869                     if (SP_IS_GAUSSIANBLUR(&primitive)) {
1870                         primitive.deleteObject();
1871                         break;
1872                     }
1873                 }
1874                 if (!item->style->getFilter()->firstChild()) {
1875                     remove_filter(item, false);
1876                 }
1877             }
1878             item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
1879         }
1880     }
1881 }
1882 
1883 /**
1884  * Constructor
1885  */
ObjectsPanel()1886 ObjectsPanel::ObjectsPanel() :
1887     DialogBase("/dialogs/objects", SP_VERB_DIALOG_OBJECTS),
1888     _rootWatcher(nullptr),
1889     _desktop(nullptr),
1890     _document(nullptr),
1891     _model(nullptr),
1892     _pending(nullptr),
1893     _pending_update(false),
1894     _toggleEvent(nullptr),
1895     _defer_target(),
1896     _visibleHeader(C_("Visibility", "V")),
1897     _lockHeader(C_("Lock", "L")),
1898     _typeHeader(C_("Type", "T")),
1899     _clipmaskHeader(C_("Clip and mask", "CM")),
1900     _highlightHeader(C_("Highlight", "HL")),
1901     _nameHeader(_("Label")),
1902     _filter_modifier( UI::Widget::SimpleFilterModifier::ISOLATION   |
1903                       UI::Widget::SimpleFilterModifier::BLEND   |
1904                       UI::Widget::SimpleFilterModifier::BLUR    |
1905                       UI::Widget::SimpleFilterModifier::OPACITY ),
1906     _colorSelectorDialog("dialogs.colorpickerwindow"),
1907     _page(Gtk::ORIENTATION_VERTICAL)
1908 {
1909     //Create the tree model and store
1910     ModelColumns *zoop = new ModelColumns();
1911     _model = zoop;
1912 
1913     _store = Gtk::TreeStore::create( *zoop );
1914 
1915     //Set up the tree
1916     _tree.set_model( _store );
1917     _tree.set_headers_visible(true);
1918     _tree.set_reorderable(false); // Reorderable means that we allow drag-and-drop, but we only allow that when at least one row is selected
1919     _tree.enable_model_drag_dest (Gdk::ACTION_MOVE);
1920 
1921     //Create the column CellRenderers
1922     //Visible
1923     Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler(
1924         INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) );
1925     int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1;
1926     eyeRenderer->property_activatable() = true;
1927     Gtk::TreeViewColumn* col = _tree.get_column(visibleColNum);
1928     if ( col ) {
1929         col->add_attribute( eyeRenderer->property_active(), _model->_colVisible );
1930         // In order to get tooltips on header, we must create our own label.
1931         _visibleHeader.set_tooltip_text(_("Toggle visibility of Layer, Group, or Object."));
1932         _visibleHeader.show();
1933         col->set_widget( _visibleHeader );
1934     }
1935 
1936     //Locked
1937     Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler(
1938         INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")) );
1939     int lockedColNum = _tree.append_column("lock", *renderer) - 1;
1940     renderer->property_activatable() = true;
1941     col = _tree.get_column(lockedColNum);
1942     if ( col ) {
1943         col->add_attribute( renderer->property_active(), _model->_colLocked );
1944         _lockHeader.set_tooltip_text(_("Toggle lock of Layer, Group, or Object."));
1945         _lockHeader.show();
1946         col->set_widget( _lockHeader );
1947     }
1948 
1949     //Type
1950     Inkscape::UI::Widget::LayerTypeIcon * typeRenderer = Gtk::manage( new Inkscape::UI::Widget::LayerTypeIcon());
1951     int typeColNum = _tree.append_column("type", *typeRenderer) - 1;
1952     typeRenderer->property_activatable() = true;
1953     col = _tree.get_column(typeColNum);
1954     if ( col ) {
1955         col->add_attribute( typeRenderer->property_active(), _model->_colType );
1956         _typeHeader.set_tooltip_text(_("Type: Layer, Group, or Object. Clicking on Layer or Group icon, toggles between the two types."));
1957         _typeHeader.show();
1958         col->set_widget( _typeHeader );
1959     }
1960 
1961     //Insert order (LiamW: unused)
1962     /*Inkscape::UI::Widget::InsertOrderIcon * insertRenderer = Gtk::manage( new Inkscape::UI::Widget::InsertOrderIcon());
1963     int insertColNum = _tree.append_column("type", *insertRenderer) - 1;
1964     col = _tree.get_column(insertColNum);
1965     if ( col ) {
1966         col->add_attribute( insertRenderer->property_active(), _model->_colInsertOrder );
1967     }*/
1968 
1969     //Clip/mask
1970     Inkscape::UI::Widget::ClipMaskIcon * clipRenderer = Gtk::manage( new Inkscape::UI::Widget::ClipMaskIcon());
1971     int clipColNum = _tree.append_column("clipmask", *clipRenderer) - 1;
1972     col = _tree.get_column(clipColNum);
1973     if ( col ) {
1974         col->add_attribute( clipRenderer->property_active(), _model->_colClipMask );
1975         _clipmaskHeader.set_tooltip_text(_("Is object clipped and/or masked?"));
1976         _clipmaskHeader.show();
1977         col->set_widget( _clipmaskHeader );
1978     }
1979 
1980     //Highlight
1981     Inkscape::UI::Widget::HighlightPicker * highlightRenderer = Gtk::manage( new Inkscape::UI::Widget::HighlightPicker());
1982     int highlightColNum = _tree.append_column("highlight", *highlightRenderer) - 1;
1983     col = _tree.get_column(highlightColNum);
1984     if ( col ) {
1985         col->add_attribute( highlightRenderer->property_active(), _model->_colHighlight );
1986         _highlightHeader.set_tooltip_text(_("Highlight color of outline in Node tool. Click to set. If alpha is zero, use inherited color."));
1987         _highlightHeader.show();
1988         col->set_widget( _highlightHeader );
1989     }
1990 
1991     //Label
1992     _text_renderer = Gtk::manage(new Gtk::CellRendererText());
1993     int nameColNum = _tree.append_column("Name", *_text_renderer) - 1;
1994     _name_column = _tree.get_column(nameColNum);
1995     if( _name_column ) {
1996         _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel);
1997         _nameHeader.set_tooltip_text(_("Layer/Group/Object label (inkscape:label). Double-click to set. Default value is object 'id'."));
1998         _nameHeader.show();
1999         _name_column->set_widget( _nameHeader );
2000     }
2001 
2002     //Set the expander and search columns
2003     _tree.set_expander_column( *_tree.get_column(nameColNum) );
2004     _tree.set_search_column(_model->_colLabel);
2005     // use ctrl+f to start search
2006     _tree.set_enable_search(false);
2007 
2008     //Set up the tree selection
2009     _tree.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE);
2010     _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &ObjectsPanel::_pushTreeSelectionToCurrent) );
2011     _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &ObjectsPanel::_rowSelectFunction) );
2012 
2013     //Set up tree signals
2014     _tree.signal_button_press_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false );
2015     _tree.signal_button_release_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false );
2016     _tree.signal_key_press_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleKeyEvent), false );
2017     _tree.signal_drag_drop().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleDragDrop), false);
2018     _tree.signal_row_collapsed().connect( sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), false));
2019     _tree.signal_row_expanded().connect( sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), true));
2020 
2021     //Set up the label editing signals
2022     _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleEdited) );
2023     _text_renderer->signal_editing_canceled().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleEditingCancelled) );
2024 
2025     //Set up the scroller window and pack the page
2026     _scroller.add( _tree );
2027     _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC );
2028     _scroller.set_shadow_type(Gtk::SHADOW_IN);
2029     Gtk::Requisition sreq;
2030     Gtk::Requisition sreq_natural;
2031     _scroller.get_preferred_size(sreq_natural, sreq);
2032     int minHeight = 70;
2033     if (sreq.height < minHeight) {
2034         // Set a min height to see the layers when used with Ubuntu liboverlay-scrollbar
2035         _scroller.set_size_request(sreq.width, minHeight);
2036     }
2037 
2038     _page.pack_start( _scroller, Gtk::PACK_EXPAND_WIDGET );
2039 
2040     //Set up the compositing items
2041     _blendConnection   = _filter_modifier.signal_blend_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_blendValueChanged));
2042     _blurConnection    = _filter_modifier.signal_blur_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_blurValueChanged));
2043     _opacityConnection = _filter_modifier.signal_opacity_changed().connect(   sigc::mem_fun(*this, &ObjectsPanel::_opacityValueChanged));
2044     _isolationConnection = _filter_modifier.signal_isolation_changed().connect(
2045         sigc::mem_fun(*this, &ObjectsPanel::_isolationValueChanged));
2046     //Pack the compositing functions and the button row
2047     _page.pack_end(_filter_modifier, Gtk::PACK_SHRINK);
2048     _page.pack_end(_buttonsRow, Gtk::PACK_SHRINK);
2049 
2050     //Pack into the panel contents
2051     pack_start(_page, Gtk::PACK_EXPAND_WIDGET);
2052 
2053     SPDesktop* targetDesktop = getDesktop();
2054 
2055     //Set up the button row
2056 
2057 
2058     //Add object/layer
2059     Gtk::Button* btn = Gtk::manage( new Gtk::Button() );
2060     _styleButton(*btn, INKSCAPE_ICON("list-add"), _("Add layer..."));
2061     btn->set_relief(Gtk::RELIEF_NONE);
2062     btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_NEW) );
2063     _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK);
2064 
2065     //Remove object
2066     btn = Gtk::manage( new Gtk::Button() );
2067     _styleButton(*btn, INKSCAPE_ICON("list-remove"), _("Remove object"));
2068     btn->set_relief(Gtk::RELIEF_NONE);
2069     btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_DELETE) );
2070     _watching.push_back( btn );
2071     _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK);
2072 
2073     //Move to bottom
2074     btn = Gtk::manage( new Gtk::Button() );
2075     _styleButton(*btn, INKSCAPE_ICON("go-bottom"), _("Move To Bottom"));
2076     btn->set_relief(Gtk::RELIEF_NONE);
2077     btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_BOTTOM) );
2078     _watchingNonBottom.push_back( btn );
2079     _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK);
2080 
2081     //Move down
2082     btn = Gtk::manage( new Gtk::Button() );
2083     _styleButton(*btn, INKSCAPE_ICON("go-down"), _("Move Down"));
2084     btn->set_relief(Gtk::RELIEF_NONE);
2085     btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_DOWN) );
2086     _watchingNonBottom.push_back( btn );
2087     _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK);
2088 
2089     //Move up
2090     btn = Gtk::manage( new Gtk::Button() );
2091     _styleButton(*btn, INKSCAPE_ICON("go-up"), _("Move Up"));
2092     btn->set_relief(Gtk::RELIEF_NONE);
2093     btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_UP) );
2094     _watchingNonTop.push_back( btn );
2095     _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK);
2096 
2097     //Move to top
2098     btn = Gtk::manage( new Gtk::Button() );
2099     _styleButton(*btn, INKSCAPE_ICON("go-top"), _("Move To Top"));
2100     btn->set_relief(Gtk::RELIEF_NONE);
2101     btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_TOP) );
2102     _watchingNonTop.push_back( btn );
2103     _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK);
2104 
2105     //Collapse all
2106     btn = Gtk::manage( new Gtk::Button() );
2107     _styleButton(*btn, INKSCAPE_ICON("format-indent-less"), _("Collapse All"));
2108     btn->set_relief(Gtk::RELIEF_NONE);
2109     btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_COLLAPSE_ALL) );
2110     _watchingNonBottom.push_back( btn );
2111     _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK);
2112 
2113     _buttonsRow.pack_start(_buttonsSecondary, Gtk::PACK_EXPAND_WIDGET);
2114     _buttonsRow.pack_end(_buttonsPrimary, Gtk::PACK_EXPAND_WIDGET);
2115 
2116     _watching.push_back(&_filter_modifier);
2117 
2118     //Set up the pop-up menu
2119     // -------------------------------------------------------
2120     {
2121         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
2122         _show_contextmenu_icons = prefs->getBool("/theme/menuIcons_objects", true);
2123 
2124         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RENAME, (int)BUTTON_RENAME ) );
2125         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_NEW, (int)BUTTON_NEW ) );
2126 
2127         _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem()));
2128 
2129         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SOLO, (int)BUTTON_SOLO ) );
2130         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SHOW_ALL, (int)BUTTON_SHOW_ALL ) );
2131         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_HIDE_ALL, (int)BUTTON_HIDE_ALL ) );
2132 
2133         _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem()));
2134 
2135         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_OTHERS, (int)BUTTON_LOCK_OTHERS ) );
2136         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_ALL, (int)BUTTON_LOCK_ALL ) );
2137         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_UNLOCK_ALL, (int)BUTTON_UNLOCK_ALL ) );
2138 
2139         _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem()));
2140 
2141         _watchingNonTop.push_back( &_addPopupItem(targetDesktop, SP_VERB_SELECTION_STACK_UP, (int)BUTTON_UP) );
2142         _watchingNonBottom.push_back( &_addPopupItem(targetDesktop, SP_VERB_SELECTION_STACK_DOWN, (int)BUTTON_DOWN) );
2143 
2144         _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem()));
2145 
2146         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_SELECTION_GROUP, (int)BUTTON_GROUP ) );
2147         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_SELECTION_UNGROUP, (int)BUTTON_UNGROUP ) );
2148 
2149         _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem()));
2150 
2151         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_CLIPPATH, (int)BUTTON_SETCLIP ) );
2152 
2153         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_CREATE_CLIP_GROUP, (int)BUTTON_CLIPGROUP ) );
2154 
2155         //will never be implemented
2156         //_watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_INVERSE_CLIPPATH, (int)BUTTON_SETINVCLIP ) );
2157         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_UNSET_CLIPPATH, (int)BUTTON_UNSETCLIP ) );
2158 
2159         _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem()));
2160 
2161         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_MASK, (int)BUTTON_SETMASK ) );
2162         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_UNSET_MASK, (int)BUTTON_UNSETMASK ) );
2163 
2164         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_EDIT_DUPLICATE, (int)BUTTON_DUPLICATE ) );
2165         _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_EDIT_DELETE, (int)BUTTON_DELETE ) );
2166 
2167         _popupMenu.show_all_children();
2168 
2169         // Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items).
2170         _popupMenu.signal_map().connect(sigc::bind<Gtk::MenuShell *>(sigc::ptr_fun(shift_icons), &_popupMenu));
2171     }
2172 
2173     // -------------------------------------------------------
2174 
2175     //Set initial sensitivity of buttons
2176     for (auto & it : _watching) {
2177         it->set_sensitive( false );
2178     }
2179     for (auto & it : _watchingNonTop) {
2180         it->set_sensitive( false );
2181     }
2182     for (auto & it : _watchingNonBottom) {
2183         it->set_sensitive( false );
2184     }
2185 
2186     //Set up the color selection dialog
2187     _colorSelectorDialog.hide();
2188     _colorSelectorDialog.set_title (_("Select Highlight Color"));
2189     _colorSelectorDialog.set_border_width (4);
2190     _colorSelectorDialog.property_modal() = true;
2191     _selectedColor.reset(new Inkscape::UI::SelectedColor);
2192     Gtk::Widget *color_selector = Gtk::manage(new Inkscape::UI::Widget::ColorNotebook(*_selectedColor));
2193     _colorSelectorDialog.get_content_area()->pack_start (
2194               *color_selector, true, true, 0);
2195 
2196     _selectedColor->signal_dragged.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod));
2197     _selectedColor->signal_released.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod));
2198     _selectedColor->signal_changed.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod));
2199 
2200     color_selector->show();
2201 
2202     show_all_children();
2203 }
2204 
2205 /**
2206  * Callback method that will be called when the desktop is destroyed
2207  */
_desktopDestroyed(SPDesktop *)2208 void ObjectsPanel::_desktopDestroyed(SPDesktop* /*desktop*/) {
2209     // We need to make sure that we're not trying to update the tree after the desktop has vanished, e.g.
2210     // when closing Inkscape. Preferably, we would have done so in the destructor of the ObjectsPanel. But
2211     // as this destructor is never ever called, we will do this by attaching to the desktop_destroyed signal
2212     // instead
2213     _processQueue_sig.disconnect();
2214     _executeUpdate_sig.disconnect();
2215     _desktop = nullptr;
2216 }
2217 
2218 /**
2219  * Destructor
2220  */
~ObjectsPanel()2221 ObjectsPanel::~ObjectsPanel()
2222 {
2223     //Close the highlight selection dialog
2224     _colorSelectorDialog.hide();
2225 
2226     // Disconnect signals
2227     _desktopDestroyedConnection.disconnect();
2228     _documentChangedConnection.disconnect();
2229     _documentChangedCurrentLayer.disconnect();
2230     _selectionChangedConnection.disconnect();
2231     setDocument(nullptr, nullptr);
2232     _desktopDestroyed(_desktop);
2233 
2234     if ( _model )
2235     {
2236         delete _model;
2237         _model = nullptr;
2238     }
2239 
2240     if (_pending) {
2241         delete _pending;
2242         _pending = nullptr;
2243     }
2244 
2245     if ( _toggleEvent )
2246     {
2247         gdk_event_free( _toggleEvent );
2248         _toggleEvent = nullptr;
2249     }
2250 }
2251 
2252 /**
2253  * Sets the current document
2254  */
setDocument(SPDesktop *,SPDocument * document)2255 void ObjectsPanel::setDocument(SPDesktop* /*desktop*/, SPDocument* document)
2256 {
2257     //Clear all object watchers
2258     _removeWatchers();
2259 
2260     //Delete the root watcher
2261     if (_rootWatcher)
2262     {
2263         _rootWatcher->_repr->removeObserver(*_rootWatcher);
2264         delete _rootWatcher;
2265         _rootWatcher = nullptr;
2266     }
2267 
2268     _document = document;
2269 
2270     if (document && document->getRoot() && document->getRoot()->getRepr())
2271     {
2272         //Create a new root watcher for the document and then call _objectsChanged to fill the tree
2273         _rootWatcher = new ObjectsPanel::ObjectWatcher(this, document->getRoot());
2274         document->getRoot()->getRepr()->addObserver(*_rootWatcher);
2275         _objectsChanged(document->getRoot());
2276     }
2277 }
2278 
2279 /**
2280  * Set the current panel desktop
2281  */
update()2282 void ObjectsPanel::update()
2283 {
2284     if (!_app) {
2285         std::cerr << "ObjectsPanel::update(): _app is null" << std::endl;
2286         return;
2287     }
2288 
2289     SPDesktop *desktop = getDesktop();
2290 
2291     if ( desktop != _desktop ) {
2292         _documentChangedConnection.disconnect();
2293         _documentChangedCurrentLayer.disconnect();
2294         _selectionChangedConnection.disconnect();
2295         if ( _desktop ) {
2296             _desktop = nullptr;
2297         }
2298 
2299         _desktop = getDesktop();
2300         if ( _desktop ) {
2301             //Connect desktop signals
2302             _documentChangedConnection = _desktop->connectDocumentReplaced( sigc::mem_fun(*this, &ObjectsPanel::setDocument));
2303 
2304             _documentChangedCurrentLayer = _desktop->connectCurrentLayerChanged( sigc::mem_fun(*this, &ObjectsPanel::_objectsChangedWrapper));
2305 
2306             _selectionChangedConnection = _desktop->selection->connectChanged( sigc::mem_fun(*this, &ObjectsPanel::_objectsSelected));
2307 
2308             _desktopDestroyedConnection = _desktop->connectDestroy( sigc::mem_fun(*this, &ObjectsPanel::_desktopDestroyed));
2309 
2310             setDocument(_desktop, _desktop->doc());
2311         } else {
2312             setDocument(nullptr, nullptr);
2313         }
2314     }
2315 }
2316 } //namespace Dialogs
2317 } //namespace UI
2318 } //namespace Inkscape
2319 
2320 //should be okay to put these here because they are never referenced anywhere else
2321 using namespace Inkscape::UI::Tools;
2322 
setHighlightColor(guint32 const color)2323 void SPItem::setHighlightColor(guint32 const color)
2324 {
2325     g_free(_highlightColor);
2326     if (color & 0x000000ff)
2327     {
2328         _highlightColor = g_strdup_printf("%u", color);
2329     }
2330     else
2331     {
2332         _highlightColor = nullptr;
2333     }
2334 
2335     NodeTool *tool = nullptr;
2336     if (SP_ACTIVE_DESKTOP ) {
2337         Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context;
2338         if (INK_IS_NODE_TOOL(ec)) {
2339             tool = static_cast<NodeTool*>(ec);
2340             tools_switch(tool->getDesktop(), TOOLS_NODES);
2341         }
2342     }
2343 }
2344 
unsetHighlightColor()2345 void SPItem::unsetHighlightColor()
2346 {
2347     g_free(_highlightColor);
2348     _highlightColor = nullptr;
2349     NodeTool *tool = nullptr;
2350     if (SP_ACTIVE_DESKTOP ) {
2351         Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context;
2352         if (INK_IS_NODE_TOOL(ec)) {
2353             tool = static_cast<NodeTool*>(ec);
2354             tools_switch(tool->getDesktop(), TOOLS_NODES);
2355         }
2356     }
2357 }
2358 
2359 /*
2360   Local Variables:
2361   mode:c++
2362   c-file-style:"stroustrup"
2363   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
2364   indent-tabs-mode:nil
2365   fill-column:99
2366   End:
2367 */
2368 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
2369