Code

Node tool: fix Tab and Shift+Tab
[inkscape.git] / src / ui / tool / multi-path-manipulator.cpp
1 /** @file
2  * Multi path manipulator - implementation
3  */
4 /* Authors:
5  *   Krzysztof KosiƄski <tweenk.pl@gmail.com>
6  *   Abhishek Sharma
7  *
8  * Copyright (C) 2009 Authors
9  * Released under GNU GPL, read the file 'COPYING' for more information
10  */
12 #include <boost/shared_ptr.hpp>
13 #include <glib.h>
14 #include <glibmm/i18n.h>
15 #include "desktop.h"
16 #include "desktop-handles.h"
17 #include "document.h"
18 #include "live_effects/lpeobject.h"
19 #include "message-stack.h"
20 #include "preferences.h"
21 #include "sp-path.h"
22 #include "ui/tool/control-point-selection.h"
23 #include "ui/tool/event-utils.h"
24 #include "ui/tool/node.h"
25 #include "ui/tool/multi-path-manipulator.h"
26 #include "ui/tool/path-manipulator.h"
27 #include "util/unordered-containers.h"
29 #ifdef USE_GNU_HASHES
30 namespace __gnu_cxx {
31 template<>
32 struct hash<Inkscape::UI::NodeList::iterator> {
33     size_t operator()(Inkscape::UI::NodeList::iterator const &n) const {
34         return reinterpret_cast<size_t>(n.ptr());
35     }
36 };
37 } // namespace __gnu_cxx
38 #endif // USE_GNU_HASHES
40 namespace Inkscape {
41 namespace UI {
43 namespace {
45 struct hash_nodelist_iterator
46     : public std::unary_function<NodeList::iterator, std::size_t>
47 {
48     std::size_t operator()(NodeList::iterator i) const {
49         return INK_HASH<NodeList::iterator::pointer>()(&*i);
50     }
51 };
53 typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair;
54 typedef std::vector<IterPair> IterPairList;
55 typedef INK_UNORDERED_SET<NodeList::iterator, hash_nodelist_iterator> IterSet;
56 typedef std::multimap<double, IterPair> DistanceMap;
57 typedef std::pair<double, IterPair> DistanceMapItem;
59 /** Find pairs of selected endnodes suitable for joining. */
60 void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs)
61 {
62     IterSet join_iters;
63     DistanceMap dists;
65     // find all endnodes in selection
66     for (ControlPointSelection::iterator i = sel.begin(); i != sel.end(); ++i) {
67         Node *node = dynamic_cast<Node*>(*i);
68         if (!node) continue;
69         NodeList::iterator iter = NodeList::get_iterator(node);
70         if (!iter.next() || !iter.prev()) join_iters.insert(iter);
71     }
73     if (join_iters.size() < 2) return;
75     // Below we find the closest pairs. The algorithm is O(N^3).
76     // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs
77     // with their distances in a multimap (not worth it IMO).
78     while (join_iters.size() >= 2) {
79         double closest = DBL_MAX;
80         IterPair closest_pair;
81         for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) {
82             for (IterSet::iterator j = join_iters.begin(); j != i; ++j) {
83                 double dist = Geom::distance(**i, **j);
84                 if (dist < closest) {
85                     closest = dist;
86                     closest_pair = std::make_pair(*i, *j);
87                 }
88             }
89         }
90         pairs.push_back(closest_pair);
91         join_iters.erase(closest_pair.first);
92         join_iters.erase(closest_pair.second);
93     }
94 }
96 /** After this function, first should be at the end of path and second at the beginnning.
97  * @returns True if the nodes are in the same subpath */
98 bool prepare_join(IterPair &join_iters)
99 {
100     if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) {
101         if (join_iters.first.next()) // if first is begin, swap the iterators
102             std::swap(join_iters.first, join_iters.second);
103         return true;
104     }
106     NodeList &sp_first = NodeList::get(join_iters.first);
107     NodeList &sp_second = NodeList::get(join_iters.second);
108     if (join_iters.first.next()) { // first is begin
109         if (join_iters.second.next()) { // second is begin
110             sp_first.reverse();
111         } else { // second is end
112             std::swap(join_iters.first, join_iters.second);
113         }
114     } else { // first is end
115         if (join_iters.second.next()) { // second is begin
116             // do nothing
117         } else { // second is end
118             sp_second.reverse();
119         }
120     }
121     return false;
123 } // anonymous namespace
126 MultiPathManipulator::MultiPathManipulator(PathSharedData &data, sigc::connection &chg)
127     : PointManipulator(data.node_data.desktop, *data.node_data.selection)
128     , _path_data(data)
129     , _changed(chg)
131     _selection.signal_commit.connect(
132         sigc::mem_fun(*this, &MultiPathManipulator::_commit));
133     _selection.signal_point_changed.connect(
134         sigc::hide( sigc::hide(
135             signal_coords_changed.make_slot())));
138 MultiPathManipulator::~MultiPathManipulator()
140     _mmap.clear();
143 /** Remove empty manipulators. */
144 void MultiPathManipulator::cleanup()
146     for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
147         if (i->second->empty()) _mmap.erase(i++);
148         else ++i;
149     }
152 /** @brief Change the set of items to edit.
153  *
154  * This method attempts to preserve as much of the state as possible. */
155 void MultiPathManipulator::setItems(std::set<ShapeRecord> const &s)
157     std::set<ShapeRecord> shapes(s);
159     // iterate over currently edited items, modifying / removing them as necessary
160     for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) {
161         std::set<ShapeRecord>::iterator si = shapes.find(i->first);
162         if (si == shapes.end()) {
163             // This item is no longer supposed to be edited - remove its manipulator
164             _mmap.erase(i++);
165         } else {
166             ShapeRecord const &sr = i->first;
167             ShapeRecord const &sr_new = *si;
168             // if the shape record differs, replace the key only and modify other values
169             if (sr.edit_transform != sr_new.edit_transform ||
170                 sr.role != sr_new.role)
171             {
172                 boost::shared_ptr<PathManipulator> hold(i->second);
173                 if (sr.edit_transform != sr_new.edit_transform)
174                     hold->setControlsTransform(sr_new.edit_transform);
175                 if (sr.role != sr_new.role) {
176                     //hold->setOutlineColor(_getOutlineColor(sr_new.role));
177                 }
178                 _mmap.erase(sr);
179                 _mmap.insert(std::make_pair(sr_new, hold));
180             }
181             shapes.erase(si); // remove the processed record
182             ++i;
183         }
184     }
186     // add newly selected items
187     for (std::set<ShapeRecord>::iterator i = shapes.begin(); i != shapes.end(); ++i) {
188         ShapeRecord const &r = *i;
189         if (!SP_IS_PATH(r.item) && !IS_LIVEPATHEFFECT(r.item)) continue;
190         boost::shared_ptr<PathManipulator> newpm(new PathManipulator(*this, (SPPath*) r.item,
191             r.edit_transform, _getOutlineColor(r.role), r.lpe_key));
192         newpm->showHandles(_show_handles);
193         // always show outlines for clips and masks
194         newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL);
195         newpm->showPathDirection(_show_path_direction);
196         newpm->setLiveOutline(_live_outline);
197         newpm->setLiveObjects(_live_objects);
198         _mmap.insert(std::make_pair(r, newpm));
199     }
202 void MultiPathManipulator::selectSubpaths()
204     if (_selection.empty()) {
205         _selection.selectAll();
206     } else {
207         invokeForAll(&PathManipulator::selectSubpaths);
208     }
211 void MultiPathManipulator::shiftSelection(int dir)
213     if (empty()) return;
215     // 1. find last selected node
216     // 2. select the next node; if the last node or nothing is selected,
217     //    select first node
218     MapType::iterator last_i;
219     SubpathList::iterator last_j;
220     NodeList::iterator last_k;
221     bool anything_found = false;
223     for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
224         SubpathList &sp = i->second->subpathList();
225         for (SubpathList::iterator j = sp.begin(); j != sp.end(); ++j) {
226             for (NodeList::iterator k = (*j)->begin(); k != (*j)->end(); ++k) {
227                 if (k->selected()) {
228                     last_i = i;
229                     last_j = j;
230                     last_k = k;
231                     anything_found = true;
232                     // when tabbing backwards, we want the first node
233                     if (dir == -1) goto exit_loop;
234                 }
235             }
236         }
237     }
238     exit_loop:
240     // NOTE: we should not assume the _selection contains only nodes
241     // in future it might also contain handles and other types of control points
242     // this is why we use a flag instead in the loop above, instead of calling
243     // selection.empty()
244     if (!anything_found) {
245         // select first / last node
246         // this should never fail because there must be at least 1 non-empty manipulator
247         if (dir == 1) {
248             _selection.insert((*_mmap.begin()->second->subpathList().begin())->begin().ptr());
249         } else {
250             _selection.insert((--(*--(--_mmap.end())->second->subpathList().end())->end()).ptr());
251         }
252         return;
253     }
255     // three levels deep - w00t!
256     if (dir == 1) {
257         if (++last_k == (*last_j)->end()) {
258             // here, last_k points to the node to be selected
259             ++last_j;
260             if (last_j == last_i->second->subpathList().end()) {
261                 ++last_i;
262                 if (last_i == _mmap.end()) {
263                     last_i = _mmap.begin();
264                 }
265                 last_j = last_i->second->subpathList().begin();
266             }
267             last_k = (*last_j)->begin();
268         }
269     } else {
270         if (!last_k || last_k == (*last_j)->begin()) {
271             if (last_j == last_i->second->subpathList().begin()) {
272                 if (last_i == _mmap.begin()) {
273                     last_i = _mmap.end();
274                 }
275                 --last_i;
276                 last_j = last_i->second->subpathList().end();
277             }
278             --last_j;
279             last_k = (*last_j)->end();
280         }
281         --last_k;
282     }
283     _selection.clear();
284     _selection.insert(last_k.ptr());
287 void MultiPathManipulator::invertSelectionInSubpaths()
289     invokeForAll(&PathManipulator::invertSelectionInSubpaths);
292 void MultiPathManipulator::setNodeType(NodeType type)
294     if (_selection.empty()) return;
296     // When all selected nodes are already cusp, retract their handles
297     bool retract_handles = (type == NODE_CUSP);
299     for (ControlPointSelection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
300         Node *node = dynamic_cast<Node*>(*i);
301         if (node) {
302             retract_handles &= (node->type() == NODE_CUSP);
303             node->setType(type);
304         }
305     }
307     if (retract_handles) {
308         for (ControlPointSelection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
309             Node *node = dynamic_cast<Node*>(*i);
310             if (node) {
311                 node->front()->retract();
312                 node->back()->retract();
313             }
314         }
315     }
317     _done(retract_handles ? _("Retract handles") : _("Change node type"));
320 void MultiPathManipulator::setSegmentType(SegmentType type)
322     if (_selection.empty()) return;
323     invokeForAll(&PathManipulator::setSegmentType, type);
324     if (type == SEGMENT_STRAIGHT) {
325         _done(_("Straighten segments"));
326     } else {
327         _done(_("Make segments curves"));
328     }
331 void MultiPathManipulator::insertNodes()
333     invokeForAll(&PathManipulator::insertNodes);
334     _done(_("Add nodes"));
337 void MultiPathManipulator::duplicateNodes()
339     invokeForAll(&PathManipulator::duplicateNodes);
340     _done(_("Duplicate nodes"));
343 void MultiPathManipulator::joinNodes()
345     invokeForAll(&PathManipulator::hideDragPoint);
346     // Node join has two parts. In the first one we join two subpaths by fusing endpoints
347     // into one. In the second we fuse nodes in each subpath.
348     IterPairList joins;
349     NodeList::iterator preserve_pos;
350     Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point);
351     if (mouseover_node) {
352         preserve_pos = NodeList::get_iterator(mouseover_node);
353     }
354     find_join_iterators(_selection, joins);
356     for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
357         bool same_path = prepare_join(*i);
358         NodeList &sp_first = NodeList::get(i->first);
359         NodeList &sp_second = NodeList::get(i->second);
360         i->first->setType(NODE_CUSP, false);
362         Geom::Point joined_pos, pos_handle_front, pos_handle_back;
363         pos_handle_front = *i->second->front();
364         pos_handle_back = *i->first->back();
366         // When we encounter the mouseover node, we unset the iterator - it will be invalidated
367         if (i->first == preserve_pos) {
368             joined_pos = *i->first;
369             preserve_pos = NodeList::iterator();
370         } else if (i->second == preserve_pos) {
371             joined_pos = *i->second;
372             preserve_pos = NodeList::iterator();
373         } else {
374             joined_pos = Geom::middle_point(*i->first, *i->second);
375         }
377         // if the handles aren't degenerate, don't move them
378         i->first->move(joined_pos);
379         Node *joined_node = i->first.ptr();
380         if (!i->second->front()->isDegenerate()) {
381             joined_node->front()->setPosition(pos_handle_front);
382         }
383         if (!i->first->back()->isDegenerate()) {
384             joined_node->back()->setPosition(pos_handle_back);
385         }
386         sp_second.erase(i->second);
388         if (same_path) {
389             sp_first.setClosed(true);
390         } else {
391             sp_first.splice(sp_first.end(), sp_second);
392             sp_second.kill();
393         }
394         _selection.insert(i->first.ptr());
395     }
397     if (joins.empty()) {
398         // Second part replaces contiguous selections of nodes with single nodes
399         invokeForAll(&PathManipulator::weldNodes, preserve_pos);
400     }
402     _doneWithCleanup(_("Join nodes"));
405 void MultiPathManipulator::breakNodes()
407     if (_selection.empty()) return;
408     invokeForAll(&PathManipulator::breakNodes);
409     _done(_("Break nodes"));
412 void MultiPathManipulator::deleteNodes(bool keep_shape)
414     if (_selection.empty()) return;
415     invokeForAll(&PathManipulator::deleteNodes, keep_shape);
416     _doneWithCleanup(_("Delete nodes"));
419 /** Join selected endpoints to create segments. */
420 void MultiPathManipulator::joinSegments()
422     IterPairList joins;
423     find_join_iterators(_selection, joins);
425     for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
426         bool same_path = prepare_join(*i);
427         NodeList &sp_first = NodeList::get(i->first);
428         NodeList &sp_second = NodeList::get(i->second);
429         i->first->setType(NODE_CUSP, false);
430         i->second->setType(NODE_CUSP, false);
431         if (same_path) {
432             sp_first.setClosed(true);
433         } else {
434             sp_first.splice(sp_first.end(), sp_second);
435             sp_second.kill();
436         }
437     }
439     if (joins.empty()) {
440         invokeForAll(&PathManipulator::weldSegments);
441     }
442     _doneWithCleanup("Join segments");
445 void MultiPathManipulator::deleteSegments()
447     if (_selection.empty()) return;
448     invokeForAll(&PathManipulator::deleteSegments);
449     _doneWithCleanup("Delete segments");
452 void MultiPathManipulator::alignNodes(Geom::Dim2 d)
454     _selection.align(d);
455     if (d == Geom::X) {
456         _done("Align nodes to a horizontal line");
457     } else {
458         _done("Align nodes to a vertical line");
459     }
462 void MultiPathManipulator::distributeNodes(Geom::Dim2 d)
464     _selection.distribute(d);
465     if (d == Geom::X) {
466         _done("Distrubute nodes horizontally");
467     } else {
468         _done("Distribute nodes vertically");
469     }
472 void MultiPathManipulator::reverseSubpaths()
474     if (_selection.empty()) {
475         invokeForAll(&PathManipulator::reverseSubpaths, false);
476         _done("Reverse subpaths");
477     } else {
478         invokeForAll(&PathManipulator::reverseSubpaths, true);
479         _done("Reverse selected subpaths");
480     }
483 void MultiPathManipulator::move(Geom::Point const &delta)
485     _selection.transform(Geom::Translate(delta));
486     _done("Move nodes");
489 void MultiPathManipulator::showOutline(bool show)
491     for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
492         // always show outlines for clipping paths and masks
493         i->second->showOutline(show || i->first.role != SHAPE_ROLE_NORMAL);
494     }
495     _show_outline = show;
498 void MultiPathManipulator::showHandles(bool show)
500     invokeForAll(&PathManipulator::showHandles, show);
501     _show_handles = show;
504 void MultiPathManipulator::showPathDirection(bool show)
506     invokeForAll(&PathManipulator::showPathDirection, show);
507     _show_path_direction = show;
510 /** @brief Set live outline update status
511  * When set to true, outline will be updated continuously when dragging
512  * or transforming nodes. Otherwise it will only update when changes are committed
513  * to XML. */
514 void MultiPathManipulator::setLiveOutline(bool set)
516     invokeForAll(&PathManipulator::setLiveOutline, set);
517     _live_outline = set;
520 /** @brief Set live object update status
521  * When set to true, objects will be updated continuously when dragging
522  * or transforming nodes. Otherwise they will only update when changes are committed
523  * to XML. */
524 void MultiPathManipulator::setLiveObjects(bool set)
526     invokeForAll(&PathManipulator::setLiveObjects, set);
527     _live_objects = set;
530 void MultiPathManipulator::updateOutlineColors()
532     //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
533     //    i->second->setOutlineColor(_getOutlineColor(i->first.role));
534     //}
537 bool MultiPathManipulator::event(GdkEvent *event)
539     _tracker.event(event);
540     guint key = 0;
541     if (event->type == GDK_KEY_PRESS) {
542         key = shortcut_key(event->key);
543     }
545     // Single handle adjustments go here.
546     if (_selection.size() == 1 && event->type == GDK_KEY_PRESS) {
547         do {
548             Node *n = dynamic_cast<Node *>(*_selection.begin());
549             if (!n) break;
551             PathManipulator &pm = n->nodeList().subpathList().pm();
553             int which = 0;
554             if (_tracker.rightAlt() || _tracker.rightControl()) {
555                 which = 1;
556             }
557             if (_tracker.leftAlt() || _tracker.leftControl()) {
558                 if (which != 0) break; // ambiguous
559                 which = -1;
560             }
561             if (which == 0) break; // no handle chosen
562             bool one_pixel = _tracker.leftAlt() || _tracker.rightAlt();
563             bool handled = true;
565             switch (key) {
566             // single handle functions
567             // rotation
568             case GDK_bracketleft:
569             case GDK_braceleft:
570                 pm.rotateHandle(n, which, 1, one_pixel);
571                 break;
572             case GDK_bracketright:
573             case GDK_braceright:
574                 pm.rotateHandle(n, which, -1, one_pixel);
575                 break;
576             // adjust length
577             case GDK_period:
578             case GDK_greater:
579                 pm.scaleHandle(n, which, 1, one_pixel);
580                 break;
581             case GDK_comma:
582             case GDK_less:
583                 pm.scaleHandle(n, which, -1, one_pixel);
584                 break;
585             default:
586                 handled = false;
587                 break;
588             }
590             if (handled) return true;
591         } while(0);
592     }
595     switch (event->type) {
596     case GDK_KEY_PRESS:
597         switch (key) {
598         case GDK_Insert:
599         case GDK_KP_Insert:
600             // Insert - insert nodes in the middle of selected segments
601             insertNodes();
602             return true;
603         case GDK_i:
604         case GDK_I:
605             if (held_only_shift(event->key)) {
606                 // Shift+I - insert nodes (alternate keybinding for Mac keyboards
607                 //           that don't have the Insert key)
608                 insertNodes();
609                 return true;
610             }
611             break;
612         case GDK_d:
613         case GDK_D:
614             if (held_only_shift(event->key)) {
615                 duplicateNodes();
616                 return true;
617             }
618         case GDK_j:
619         case GDK_J:
620             if (held_only_shift(event->key)) {
621                 // Shift+J - join nodes
622                 joinNodes();
623                 return true;
624             }
625             if (held_only_alt(event->key)) {
626                 // Alt+J - join segments
627                 joinSegments();
628                 return true;
629             }
630             break;
631         case GDK_b:
632         case GDK_B:
633             if (held_only_shift(event->key)) {
634                 // Shift+B - break nodes
635                 breakNodes();
636                 return true;
637             }
638             break;
639         case GDK_Delete:
640         case GDK_KP_Delete:
641         case GDK_BackSpace:
642             if (held_shift(event->key)) break;
643             if (held_alt(event->key)) {
644                 // Alt+Delete - delete segments
645                 deleteSegments();
646             } else {
647                 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
648                 bool del_preserves_shape = prefs->getBool("/tools/nodes/delete_preserves_shape", true);
649                 // pass keep_shape = true when:
650                 // a) del preserves shape, and control is not pressed
651                 // b) ctrl+del preserves shape (del_preserves_shape is false), and control is pressed
652                 // Hence xor
653                 deleteNodes(del_preserves_shape ^ held_control(event->key));
654             }
655             return true;
656         case GDK_c:
657         case GDK_C:
658             if (held_only_shift(event->key)) {
659                 // Shift+C - make nodes cusp
660                 setNodeType(NODE_CUSP);
661                 return true;
662             }
663             break;
664         case GDK_s:
665         case GDK_S:
666             if (held_only_shift(event->key)) {
667                 // Shift+S - make nodes smooth
668                 setNodeType(NODE_SMOOTH);
669                 return true;
670             }
671             break;
672         case GDK_a:
673         case GDK_A:
674             if (held_only_shift(event->key)) {
675                 // Shift+A - make nodes auto-smooth
676                 setNodeType(NODE_AUTO);
677                 return true;
678             }
679             break;
680         case GDK_y:
681         case GDK_Y:
682             if (held_only_shift(event->key)) {
683                 // Shift+Y - make nodes symmetric
684                 setNodeType(NODE_SYMMETRIC);
685                 return true;
686             }
687             break;
688         case GDK_r:
689         case GDK_R:
690             if (held_only_shift(event->key)) {
691                 // Shift+R - reverse subpaths
692                 reverseSubpaths();
693                 return true;
694             }
695             break;
696         case GDK_l:
697         case GDK_L:
698             if (held_only_shift(event->key)) {
699                 // Shift+L - make segments linear
700                 setSegmentType(SEGMENT_STRAIGHT);
701                 return true;
702             }
703         case GDK_u:
704         case GDK_U:
705             if (held_only_shift(event->key)) {
706                 // Shift+L - make segments curves
707                 setSegmentType(SEGMENT_CUBIC_BEZIER);
708                 return true;
709             }
710         default:
711             break;
712         }
713         break;
714     case GDK_MOTION_NOTIFY:
715         combine_motion_events(_desktop->canvas, event->motion, 0);
716         for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
717             if (i->second->event(event)) return true;
718         }
719         break;
720     default: break;
721     }
723     return false;
726 /** Commit changes to XML and add undo stack entry based on the action that was done. Invoked
727  * by sub-manipulators, for example TransformHandleSet and ControlPointSelection. */
728 void MultiPathManipulator::_commit(CommitEvent cps)
730     gchar const *reason = NULL;
731     gchar const *key = NULL;
732     switch(cps) {
733     case COMMIT_MOUSE_MOVE:
734         reason = _("Move nodes");
735         break;
736     case COMMIT_KEYBOARD_MOVE_X:
737         reason = _("Move nodes horizontally");
738         key = "node:move:x";
739         break;
740     case COMMIT_KEYBOARD_MOVE_Y:
741         reason = _("Move nodes vertically");
742         key = "node:move:y";
743         break;
744     case COMMIT_MOUSE_ROTATE:
745         reason = _("Rotate nodes");
746         break;
747     case COMMIT_KEYBOARD_ROTATE:
748         reason = _("Rotate nodes");
749         key = "node:rotate";
750         break;
751     case COMMIT_MOUSE_SCALE_UNIFORM:
752         reason = _("Scale nodes uniformly");
753         break;
754     case COMMIT_MOUSE_SCALE:
755         reason = _("Scale nodes");
756         break;
757     case COMMIT_KEYBOARD_SCALE_UNIFORM:
758         reason = _("Scale nodes uniformly");
759         key = "node:scale:uniform";
760         break;
761     case COMMIT_KEYBOARD_SCALE_X:
762         reason = _("Scale nodes horizontally");
763         key = "node:scale:x";
764         break;
765     case COMMIT_KEYBOARD_SCALE_Y:
766         reason = _("Scale nodes vertically");
767         key = "node:scale:y";
768         break;
769     case COMMIT_MOUSE_SKEW_X:
770         reason = _("Skew nodes horizontally");
771         key = "node:skew:x";
772         break;
773     case COMMIT_MOUSE_SKEW_Y:
774         reason = _("Skew nodes vertically");
775         key = "node:skew:y";
776         break;
777     case COMMIT_FLIP_X:
778         reason = _("Flip nodes horizontally");
779         break;
780     case COMMIT_FLIP_Y:
781         reason = _("Flip nodes vertically");
782         break;
783     default: return;
784     }
785     
786     _selection.signal_update.emit();
787     invokeForAll(&PathManipulator::writeXML);
788     if (key) {
789         DocumentUndo::maybeDone(sp_desktop_document(_desktop), key, SP_VERB_CONTEXT_NODE, reason);
790     } else {
791         DocumentUndo::done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
792     }
793     signal_coords_changed.emit();
796 /** Commits changes to XML and adds undo stack entry. */
797 void MultiPathManipulator::_done(gchar const *reason) {
798     invokeForAll(&PathManipulator::update);
799     invokeForAll(&PathManipulator::writeXML);
800     DocumentUndo::done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
801     signal_coords_changed.emit();
804 /** Commits changes to XML, adds undo stack entry and removes empty manipulators. */
805 void MultiPathManipulator::_doneWithCleanup(gchar const *reason) {
806     _changed.block();
807     _done(reason);
808     cleanup();
809     _changed.unblock();
812 /** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */
813 guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role)
815     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
816     switch(role) {
817     case SHAPE_ROLE_CLIPPING_PATH:
818         return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff);
819     case SHAPE_ROLE_MASK:
820         return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff);
821     case SHAPE_ROLE_LPE_PARAM:
822         return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff);
823     case SHAPE_ROLE_NORMAL:
824     default:
825         return prefs->getColor("/tools/nodes/outline_color", 0xff0000ff);
826     }
829 } // namespace UI
830 } // namespace Inkscape
832 /*
833   Local Variables:
834   mode:c++
835   c-file-style:"stroustrup"
836   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
837   indent-tabs-mode:nil
838   fill-column:99
839   End:
840 */
841 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :