3b0852e6e6842e4f9f7befcf429ba0a04dc95c85
1 /** @file
2 * Multi path manipulator - implementation
3 */
4 /* Authors:
5 * Krzysztof KosiĆski <tweenk.pl@gmail.com>
6 *
7 * Copyright (C) 2009 Authors
8 * Released under GNU GPL, read the file 'COPYING' for more information
9 */
11 #include <boost/unordered_set.hpp>
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"
28 namespace Inkscape {
29 namespace UI {
31 namespace {
32 typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair;
33 typedef std::vector<IterPair> IterPairList;
34 typedef boost::unordered_set<NodeList::iterator> IterSet;
35 typedef std::multimap<double, IterPair> DistanceMap;
36 typedef std::pair<double, IterPair> DistanceMapItem;
38 /** Find pairs of selected endnodes suitable for joining. */
39 void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs)
40 {
41 IterSet join_iters;
42 DistanceMap dists;
44 // find all endnodes in selection
45 for (ControlPointSelection::iterator i = sel.begin(); i != sel.end(); ++i) {
46 Node *node = dynamic_cast<Node*>(*i);
47 if (!node) continue;
48 NodeList::iterator iter = NodeList::get_iterator(node);
49 if (!iter.next() || !iter.prev()) join_iters.insert(iter);
50 }
52 if (join_iters.size() < 2) return;
54 // Below we find the closest pairs. The algorithm is O(N^3).
55 // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs
56 // with their distances in a multimap (not worth it IMO).
57 while (join_iters.size() >= 2) {
58 double closest = DBL_MAX;
59 IterPair closest_pair;
60 for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) {
61 for (IterSet::iterator j = join_iters.begin(); j != i; ++j) {
62 double dist = Geom::distance(**i, **j);
63 if (dist < closest) {
64 closest = dist;
65 closest_pair = std::make_pair(*i, *j);
66 }
67 }
68 }
69 pairs.push_back(closest_pair);
70 join_iters.erase(closest_pair.first);
71 join_iters.erase(closest_pair.second);
72 }
73 }
75 /** After this function, first should be at the end of path and second at the beginnning.
76 * @returns True if the nodes are in the same subpath */
77 bool prepare_join(IterPair &join_iters)
78 {
79 if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) {
80 if (join_iters.first.next()) // if first is begin, swap the iterators
81 std::swap(join_iters.first, join_iters.second);
82 return true;
83 }
85 NodeList &sp_first = NodeList::get(join_iters.first);
86 NodeList &sp_second = NodeList::get(join_iters.second);
87 if (join_iters.first.next()) { // first is begin
88 if (join_iters.second.next()) { // second is begin
89 sp_first.reverse();
90 } else { // second is end
91 std::swap(join_iters.first, join_iters.second);
92 }
93 } else { // first is end
94 if (join_iters.second.next()) { // second is begin
95 // do nothing
96 } else { // second is end
97 sp_second.reverse();
98 }
99 }
100 return false;
101 }
102 } // anonymous namespace
105 MultiPathManipulator::MultiPathManipulator(PathSharedData &data, sigc::connection &chg)
106 : PointManipulator(data.node_data.desktop, *data.node_data.selection)
107 , _path_data(data)
108 , _changed(chg)
109 {
110 _selection.signal_commit.connect(
111 sigc::mem_fun(*this, &MultiPathManipulator::_commit));
112 _selection.signal_point_changed.connect(
113 sigc::hide( sigc::hide(
114 signal_coords_changed.make_slot())));
115 }
117 MultiPathManipulator::~MultiPathManipulator()
118 {
119 _mmap.clear();
120 }
122 /** Remove empty manipulators. */
123 void MultiPathManipulator::cleanup()
124 {
125 for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
126 if (i->second->empty()) _mmap.erase(i++);
127 else ++i;
128 }
129 }
131 /** @brief Change the set of items to edit.
132 *
133 * This method attempts to preserve as much of the state as possible. */
134 void MultiPathManipulator::setItems(std::set<ShapeRecord> const &s)
135 {
136 std::set<ShapeRecord> shapes(s);
138 // iterate over currently edited items, modifying / removing them as necessary
139 for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) {
140 std::set<ShapeRecord>::iterator si = shapes.find(i->first);
141 if (si == shapes.end()) {
142 // This item is no longer supposed to be edited - remove its manipulator
143 _mmap.erase(i++);
144 } else {
145 ShapeRecord const &sr = i->first;
146 ShapeRecord const &sr_new = *si;
147 // if the shape record differs, replace the key only and modify other values
148 if (sr.edit_transform != sr_new.edit_transform ||
149 sr.role != sr_new.role)
150 {
151 boost::shared_ptr<PathManipulator> hold(i->second);
152 if (sr.edit_transform != sr_new.edit_transform)
153 hold->setControlsTransform(sr_new.edit_transform);
154 if (sr.role != sr_new.role) {
155 //hold->setOutlineColor(_getOutlineColor(sr_new.role));
156 }
157 _mmap.erase(sr);
158 _mmap.insert(std::make_pair(sr_new, hold));
159 }
160 shapes.erase(si); // remove the processed record
161 ++i;
162 }
163 }
165 // add newly selected items
166 for (std::set<ShapeRecord>::iterator i = shapes.begin(); i != shapes.end(); ++i) {
167 ShapeRecord const &r = *i;
168 if (!SP_IS_PATH(r.item) && !IS_LIVEPATHEFFECT(r.item)) continue;
169 boost::shared_ptr<PathManipulator> newpm(new PathManipulator(*this, (SPPath*) r.item,
170 r.edit_transform, _getOutlineColor(r.role), r.lpe_key));
171 newpm->showHandles(_show_handles);
172 // always show outlines for clips and masks
173 newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL);
174 newpm->showPathDirection(_show_path_direction);
175 newpm->setLiveOutline(_live_outline);
176 newpm->setLiveObjects(_live_objects);
177 _mmap.insert(std::make_pair(r, newpm));
178 }
179 }
181 void MultiPathManipulator::selectSubpaths()
182 {
183 if (_selection.empty()) {
184 _selection.selectAll();
185 } else {
186 invokeForAll(&PathManipulator::selectSubpaths);
187 }
188 }
190 void MultiPathManipulator::shiftSelection(int dir)
191 {
192 invokeForAll(&PathManipulator::shiftSelection, dir);
193 }
195 void MultiPathManipulator::invertSelectionInSubpaths()
196 {
197 invokeForAll(&PathManipulator::invertSelectionInSubpaths);
198 }
200 void MultiPathManipulator::setNodeType(NodeType type)
201 {
202 if (_selection.empty()) return;
203 for (ControlPointSelection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
204 Node *node = dynamic_cast<Node*>(*i);
205 if (node) node->setType(type);
206 }
207 _done(_("Change node type"));
208 }
210 void MultiPathManipulator::setSegmentType(SegmentType type)
211 {
212 if (_selection.empty()) return;
213 invokeForAll(&PathManipulator::setSegmentType, type);
214 if (type == SEGMENT_STRAIGHT) {
215 _done(_("Straighten segments"));
216 } else {
217 _done(_("Make segments curves"));
218 }
219 }
221 void MultiPathManipulator::insertNodes()
222 {
223 invokeForAll(&PathManipulator::insertNodes);
224 _done(_("Add nodes"));
225 }
227 void MultiPathManipulator::joinNodes()
228 {
229 invokeForAll(&PathManipulator::hideDragPoint);
230 // Node join has two parts. In the first one we join two subpaths by fusing endpoints
231 // into one. In the second we fuse nodes in each subpath.
232 IterPairList joins;
233 NodeList::iterator preserve_pos;
234 Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point);
235 if (mouseover_node) {
236 preserve_pos = NodeList::get_iterator(mouseover_node);
237 }
238 find_join_iterators(_selection, joins);
240 for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
241 bool same_path = prepare_join(*i);
242 NodeList &sp_first = NodeList::get(i->first);
243 NodeList &sp_second = NodeList::get(i->second);
244 i->first->setType(NODE_CUSP, false);
246 Geom::Point joined_pos, pos_handle_front, pos_handle_back;
247 pos_handle_front = *i->second->front();
248 pos_handle_back = *i->first->back();
250 // When we encounter the mouseover node, we unset the iterator - it will be invalidated
251 if (i->first == preserve_pos) {
252 joined_pos = *i->first;
253 preserve_pos = NodeList::iterator();
254 } else if (i->second == preserve_pos) {
255 joined_pos = *i->second;
256 preserve_pos = NodeList::iterator();
257 } else {
258 joined_pos = Geom::middle_point(*i->first, *i->second);
259 }
261 // if the handles aren't degenerate, don't move them
262 i->first->move(joined_pos);
263 Node *joined_node = i->first.ptr();
264 if (!i->second->front()->isDegenerate()) {
265 joined_node->front()->setPosition(pos_handle_front);
266 }
267 if (!i->first->back()->isDegenerate()) {
268 joined_node->back()->setPosition(pos_handle_back);
269 }
270 sp_second.erase(i->second);
272 if (same_path) {
273 sp_first.setClosed(true);
274 } else {
275 sp_first.splice(sp_first.end(), sp_second);
276 sp_second.kill();
277 }
278 _selection.insert(i->first.ptr());
279 }
281 if (joins.empty()) {
282 // Second part replaces contiguous selections of nodes with single nodes
283 invokeForAll(&PathManipulator::weldNodes, preserve_pos);
284 }
286 _doneWithCleanup(_("Join nodes"));
287 }
289 void MultiPathManipulator::breakNodes()
290 {
291 if (_selection.empty()) return;
292 invokeForAll(&PathManipulator::breakNodes);
293 _done(_("Break nodes"));
294 }
296 void MultiPathManipulator::deleteNodes(bool keep_shape)
297 {
298 if (_selection.empty()) return;
299 invokeForAll(&PathManipulator::deleteNodes, keep_shape);
300 _doneWithCleanup(_("Delete nodes"));
301 }
303 /** Join selected endpoints to create segments. */
304 void MultiPathManipulator::joinSegments()
305 {
306 IterPairList joins;
307 find_join_iterators(_selection, joins);
309 for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
310 bool same_path = prepare_join(*i);
311 NodeList &sp_first = NodeList::get(i->first);
312 NodeList &sp_second = NodeList::get(i->second);
313 i->first->setType(NODE_CUSP, false);
314 i->second->setType(NODE_CUSP, false);
315 if (same_path) {
316 sp_first.setClosed(true);
317 } else {
318 sp_first.splice(sp_first.end(), sp_second);
319 sp_second.kill();
320 }
321 }
323 if (joins.empty()) {
324 invokeForAll(&PathManipulator::weldSegments);
325 }
326 _doneWithCleanup("Join segments");
327 }
329 void MultiPathManipulator::deleteSegments()
330 {
331 if (_selection.empty()) return;
332 invokeForAll(&PathManipulator::deleteSegments);
333 _doneWithCleanup("Delete segments");
334 }
336 void MultiPathManipulator::alignNodes(Geom::Dim2 d)
337 {
338 _selection.align(d);
339 if (d == Geom::X) {
340 _done("Align nodes to a horizontal line");
341 } else {
342 _done("Align nodes to a vertical line");
343 }
344 }
346 void MultiPathManipulator::distributeNodes(Geom::Dim2 d)
347 {
348 _selection.distribute(d);
349 if (d == Geom::X) {
350 _done("Distrubute nodes horizontally");
351 } else {
352 _done("Distribute nodes vertically");
353 }
354 }
356 void MultiPathManipulator::reverseSubpaths()
357 {
358 if (_selection.empty()) {
359 invokeForAll(&PathManipulator::reverseSubpaths, false);
360 _done("Reverse subpaths");
361 } else {
362 invokeForAll(&PathManipulator::reverseSubpaths, true);
363 _done("Reverse selected subpaths");
364 }
365 }
367 void MultiPathManipulator::move(Geom::Point const &delta)
368 {
369 _selection.transform(Geom::Translate(delta));
370 _done("Move nodes");
371 }
373 void MultiPathManipulator::showOutline(bool show)
374 {
375 for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
376 // always show outlines for clipping paths and masks
377 i->second->showOutline(show || i->first.role != SHAPE_ROLE_NORMAL);
378 }
379 _show_outline = show;
380 }
382 void MultiPathManipulator::showHandles(bool show)
383 {
384 invokeForAll(&PathManipulator::showHandles, show);
385 _show_handles = show;
386 }
388 void MultiPathManipulator::showPathDirection(bool show)
389 {
390 invokeForAll(&PathManipulator::showPathDirection, show);
391 _show_path_direction = show;
392 }
394 /** @brief Set live outline update status
395 * When set to true, outline will be updated continuously when dragging
396 * or transforming nodes. Otherwise it will only update when changes are committed
397 * to XML. */
398 void MultiPathManipulator::setLiveOutline(bool set)
399 {
400 invokeForAll(&PathManipulator::setLiveOutline, set);
401 _live_outline = set;
402 }
404 /** @brief Set live object update status
405 * When set to true, objects will be updated continuously when dragging
406 * or transforming nodes. Otherwise they will only update when changes are committed
407 * to XML. */
408 void MultiPathManipulator::setLiveObjects(bool set)
409 {
410 invokeForAll(&PathManipulator::setLiveObjects, set);
411 _live_objects = set;
412 }
414 void MultiPathManipulator::updateOutlineColors()
415 {
416 //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
417 // i->second->setOutlineColor(_getOutlineColor(i->first.role));
418 //}
419 }
421 bool MultiPathManipulator::event(GdkEvent *event)
422 {
423 switch (event->type) {
424 case GDK_KEY_PRESS:
425 switch (shortcut_key(event->key)) {
426 case GDK_Insert:
427 case GDK_KP_Insert:
428 // Insert - insert nodes in the middle of selected segments
429 insertNodes();
430 return true;
431 case GDK_i:
432 case GDK_I:
433 if (held_only_shift(event->key)) {
434 // Shift+I - insert nodes (alternate keybinding for Mac keyboards
435 // that don't have the Insert key)
436 insertNodes();
437 return true;
438 }
439 break;
440 case GDK_j:
441 case GDK_J:
442 if (held_only_shift(event->key)) {
443 // Shift+J - join nodes
444 joinNodes();
445 return true;
446 }
447 if (held_only_alt(event->key)) {
448 // Alt+J - join segments
449 joinSegments();
450 return true;
451 }
452 break;
453 case GDK_b:
454 case GDK_B:
455 if (held_only_shift(event->key)) {
456 // Shift+B - break nodes
457 breakNodes();
458 return true;
459 }
460 break;
461 case GDK_Delete:
462 case GDK_KP_Delete:
463 case GDK_BackSpace:
464 if (held_shift(event->key)) break;
465 if (held_alt(event->key)) {
466 // Alt+Delete - delete segments
467 deleteSegments();
468 } else {
469 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
470 bool del_preserves_shape = prefs->getBool("/tools/nodes/delete_preserves_shape", true);
471 // pass keep_shape = true when:
472 // a) del preserves shape, and control is not pressed
473 // b) ctrl+del preserves shape (del_preserves_shape is false), and control is pressed
474 // Hence xor
475 deleteNodes(del_preserves_shape ^ held_control(event->key));
476 }
477 return true;
478 case GDK_c:
479 case GDK_C:
480 if (held_only_shift(event->key)) {
481 // Shift+C - make nodes cusp
482 setNodeType(NODE_CUSP);
483 return true;
484 }
485 break;
486 case GDK_s:
487 case GDK_S:
488 if (held_only_shift(event->key)) {
489 // Shift+S - make nodes smooth
490 setNodeType(NODE_SMOOTH);
491 return true;
492 }
493 break;
494 case GDK_a:
495 case GDK_A:
496 if (held_only_shift(event->key)) {
497 // Shift+A - make nodes auto-smooth
498 setNodeType(NODE_AUTO);
499 return true;
500 }
501 break;
502 case GDK_y:
503 case GDK_Y:
504 if (held_only_shift(event->key)) {
505 // Shift+Y - make nodes symmetric
506 setNodeType(NODE_SYMMETRIC);
507 return true;
508 }
509 break;
510 case GDK_r:
511 case GDK_R:
512 if (held_only_shift(event->key)) {
513 // Shift+R - reverse subpaths
514 reverseSubpaths();
515 }
516 break;
517 default:
518 break;
519 }
520 break;
521 case GDK_MOTION_NOTIFY:
522 combine_motion_events(_desktop->canvas, event->motion, 0);
523 for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
524 if (i->second->event(event)) return true;
525 }
526 break;
527 default: break;
528 }
530 return false;
531 }
533 /** Commit changes to XML and add undo stack entry based on the action that was done. Invoked
534 * by sub-manipulators, for example TransformHandleSet and ControlPointSelection. */
535 void MultiPathManipulator::_commit(CommitEvent cps)
536 {
537 gchar const *reason = NULL;
538 gchar const *key = NULL;
539 switch(cps) {
540 case COMMIT_MOUSE_MOVE:
541 reason = _("Move nodes");
542 break;
543 case COMMIT_KEYBOARD_MOVE_X:
544 reason = _("Move nodes horizontally");
545 key = "node:move:x";
546 break;
547 case COMMIT_KEYBOARD_MOVE_Y:
548 reason = _("Move nodes vertically");
549 key = "node:move:y";
550 break;
551 case COMMIT_MOUSE_ROTATE:
552 reason = _("Rotate nodes");
553 break;
554 case COMMIT_KEYBOARD_ROTATE:
555 reason = _("Rotate nodes");
556 key = "node:rotate";
557 break;
558 case COMMIT_MOUSE_SCALE_UNIFORM:
559 reason = _("Scale nodes uniformly");
560 break;
561 case COMMIT_MOUSE_SCALE:
562 reason = _("Scale nodes");
563 break;
564 case COMMIT_KEYBOARD_SCALE_UNIFORM:
565 reason = _("Scale nodes uniformly");
566 key = "node:scale:uniform";
567 break;
568 case COMMIT_KEYBOARD_SCALE_X:
569 reason = _("Scale nodes horizontally");
570 key = "node:scale:x";
571 break;
572 case COMMIT_KEYBOARD_SCALE_Y:
573 reason = _("Scale nodes vertically");
574 key = "node:scale:y";
575 break;
576 case COMMIT_FLIP_X:
577 reason = _("Flip nodes horizontally");
578 break;
579 case COMMIT_FLIP_Y:
580 reason = _("Flip nodes vertically");
581 break;
582 default: return;
583 }
585 _selection.signal_update.emit();
586 invokeForAll(&PathManipulator::writeXML);
587 if (key) {
588 sp_document_maybe_done(sp_desktop_document(_desktop), key, SP_VERB_CONTEXT_NODE, reason);
589 } else {
590 sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
591 }
592 signal_coords_changed.emit();
593 }
595 /** Commits changes to XML and adds undo stack entry. */
596 void MultiPathManipulator::_done(gchar const *reason) {
597 invokeForAll(&PathManipulator::update);
598 invokeForAll(&PathManipulator::writeXML);
599 sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
600 signal_coords_changed.emit();
601 }
603 /** Commits changes to XML, adds undo stack entry and removes empty manipulators. */
604 void MultiPathManipulator::_doneWithCleanup(gchar const *reason) {
605 _changed.block();
606 _done(reason);
607 cleanup();
608 _changed.unblock();
609 }
611 /** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */
612 guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role)
613 {
614 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
615 switch(role) {
616 case SHAPE_ROLE_CLIPPING_PATH:
617 return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff);
618 case SHAPE_ROLE_MASK:
619 return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff);
620 case SHAPE_ROLE_LPE_PARAM:
621 return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff);
622 case SHAPE_ROLE_NORMAL:
623 default:
624 return prefs->getColor("/tools/nodes/outline_color", 0xff0000ff);
625 }
626 }
628 } // namespace UI
629 } // namespace Inkscape
631 /*
632 Local Variables:
633 mode:c++
634 c-file-style:"stroustrup"
635 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
636 indent-tabs-mode:nil
637 fill-column:99
638 End:
639 */
640 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :