Code

3b0852e6e6842e4f9f7befcf429ba0a04dc95c85
[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  *
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;
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)
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())));
117 MultiPathManipulator::~MultiPathManipulator()
119     _mmap.clear();
122 /** Remove empty manipulators. */
123 void MultiPathManipulator::cleanup()
125     for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
126         if (i->second->empty()) _mmap.erase(i++);
127         else ++i;
128     }
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)
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     }
181 void MultiPathManipulator::selectSubpaths()
183     if (_selection.empty()) {
184         _selection.selectAll();
185     } else {
186         invokeForAll(&PathManipulator::selectSubpaths);
187     }
190 void MultiPathManipulator::shiftSelection(int dir)
192     invokeForAll(&PathManipulator::shiftSelection, dir);
195 void MultiPathManipulator::invertSelectionInSubpaths()
197     invokeForAll(&PathManipulator::invertSelectionInSubpaths);
200 void MultiPathManipulator::setNodeType(NodeType type)
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"));
210 void MultiPathManipulator::setSegmentType(SegmentType type)
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     }
221 void MultiPathManipulator::insertNodes()
223     invokeForAll(&PathManipulator::insertNodes);
224     _done(_("Add nodes"));
227 void MultiPathManipulator::joinNodes()
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"));
289 void MultiPathManipulator::breakNodes()
291     if (_selection.empty()) return;
292     invokeForAll(&PathManipulator::breakNodes);
293     _done(_("Break nodes"));
296 void MultiPathManipulator::deleteNodes(bool keep_shape)
298     if (_selection.empty()) return;
299     invokeForAll(&PathManipulator::deleteNodes, keep_shape);
300     _doneWithCleanup(_("Delete nodes"));
303 /** Join selected endpoints to create segments. */
304 void MultiPathManipulator::joinSegments()
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");
329 void MultiPathManipulator::deleteSegments()
331     if (_selection.empty()) return;
332     invokeForAll(&PathManipulator::deleteSegments);
333     _doneWithCleanup("Delete segments");
336 void MultiPathManipulator::alignNodes(Geom::Dim2 d)
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     }
346 void MultiPathManipulator::distributeNodes(Geom::Dim2 d)
348     _selection.distribute(d);
349     if (d == Geom::X) {
350         _done("Distrubute nodes horizontally");
351     } else {
352         _done("Distribute nodes vertically");
353     }
356 void MultiPathManipulator::reverseSubpaths()
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     }
367 void MultiPathManipulator::move(Geom::Point const &delta)
369     _selection.transform(Geom::Translate(delta));
370     _done("Move nodes");
373 void MultiPathManipulator::showOutline(bool show)
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;
382 void MultiPathManipulator::showHandles(bool show)
384     invokeForAll(&PathManipulator::showHandles, show);
385     _show_handles = show;
388 void MultiPathManipulator::showPathDirection(bool show)
390     invokeForAll(&PathManipulator::showPathDirection, show);
391     _show_path_direction = show;
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)
400     invokeForAll(&PathManipulator::setLiveOutline, set);
401     _live_outline = set;
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)
410     invokeForAll(&PathManipulator::setLiveObjects, set);
411     _live_objects = set;
414 void MultiPathManipulator::updateOutlineColors()
416     //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
417     //    i->second->setOutlineColor(_getOutlineColor(i->first.role));
418     //}
421 bool MultiPathManipulator::event(GdkEvent *event)
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;
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)
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     }
584     
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();
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();
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();
611 /** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */
612 guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role)
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     }
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 :