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;
122 }
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)
130 {
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())));
136 }
138 MultiPathManipulator::~MultiPathManipulator()
139 {
140 _mmap.clear();
141 }
143 /** Remove empty manipulators. */
144 void MultiPathManipulator::cleanup()
145 {
146 for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
147 if (i->second->empty()) _mmap.erase(i++);
148 else ++i;
149 }
150 }
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)
156 {
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 }
200 }
202 void MultiPathManipulator::selectSubpaths()
203 {
204 if (_selection.empty()) {
205 _selection.selectAll();
206 } else {
207 invokeForAll(&PathManipulator::selectSubpaths);
208 }
209 }
211 void MultiPathManipulator::shiftSelection(int dir)
212 {
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());
285 }
287 void MultiPathManipulator::invertSelectionInSubpaths()
288 {
289 invokeForAll(&PathManipulator::invertSelectionInSubpaths);
290 }
292 void MultiPathManipulator::setNodeType(NodeType type)
293 {
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"));
318 }
320 void MultiPathManipulator::setSegmentType(SegmentType type)
321 {
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 }
329 }
331 void MultiPathManipulator::insertNodes()
332 {
333 invokeForAll(&PathManipulator::insertNodes);
334 _done(_("Add nodes"));
335 }
337 void MultiPathManipulator::duplicateNodes()
338 {
339 invokeForAll(&PathManipulator::duplicateNodes);
340 _done(_("Duplicate nodes"));
341 }
343 void MultiPathManipulator::joinNodes()
344 {
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"));
403 }
405 void MultiPathManipulator::breakNodes()
406 {
407 if (_selection.empty()) return;
408 invokeForAll(&PathManipulator::breakNodes);
409 _done(_("Break nodes"));
410 }
412 void MultiPathManipulator::deleteNodes(bool keep_shape)
413 {
414 if (_selection.empty()) return;
415 invokeForAll(&PathManipulator::deleteNodes, keep_shape);
416 _doneWithCleanup(_("Delete nodes"));
417 }
419 /** Join selected endpoints to create segments. */
420 void MultiPathManipulator::joinSegments()
421 {
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");
443 }
445 void MultiPathManipulator::deleteSegments()
446 {
447 if (_selection.empty()) return;
448 invokeForAll(&PathManipulator::deleteSegments);
449 _doneWithCleanup("Delete segments");
450 }
452 void MultiPathManipulator::alignNodes(Geom::Dim2 d)
453 {
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 }
460 }
462 void MultiPathManipulator::distributeNodes(Geom::Dim2 d)
463 {
464 _selection.distribute(d);
465 if (d == Geom::X) {
466 _done("Distrubute nodes horizontally");
467 } else {
468 _done("Distribute nodes vertically");
469 }
470 }
472 void MultiPathManipulator::reverseSubpaths()
473 {
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 }
481 }
483 void MultiPathManipulator::move(Geom::Point const &delta)
484 {
485 _selection.transform(Geom::Translate(delta));
486 _done("Move nodes");
487 }
489 void MultiPathManipulator::showOutline(bool show)
490 {
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;
496 }
498 void MultiPathManipulator::showHandles(bool show)
499 {
500 invokeForAll(&PathManipulator::showHandles, show);
501 _show_handles = show;
502 }
504 void MultiPathManipulator::showPathDirection(bool show)
505 {
506 invokeForAll(&PathManipulator::showPathDirection, show);
507 _show_path_direction = show;
508 }
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)
515 {
516 invokeForAll(&PathManipulator::setLiveOutline, set);
517 _live_outline = set;
518 }
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)
525 {
526 invokeForAll(&PathManipulator::setLiveObjects, set);
527 _live_objects = set;
528 }
530 void MultiPathManipulator::updateOutlineColors()
531 {
532 //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
533 // i->second->setOutlineColor(_getOutlineColor(i->first.role));
534 //}
535 }
537 bool MultiPathManipulator::event(GdkEvent *event)
538 {
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;
724 }
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)
729 {
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 }
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();
794 }
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();
802 }
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();
810 }
812 /** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */
813 guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role)
814 {
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 }
827 }
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 :