Code

Fix path reverse action (Shift+R) in the node tool.
[inkscape.git] / src / ui / tool / multi-path-manipulator.cpp
1 /** @file
2  * 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 <tr1/unordered_set>
12 #include <ext/hash_set>
13 #include <boost/shared_ptr.hpp>
14 #include <glib.h>
15 #include <glibmm/i18n.h>
16 #include "desktop.h"
17 #include "desktop-handles.h"
18 #include "document.h"
19 #include "live_effects/lpeobject.h"
20 #include "message-stack.h"
21 #include "preferences.h"
22 #include "sp-path.h"
23 #include "ui/tool/control-point-selection.h"
24 #include "ui/tool/event-utils.h"
25 #include "ui/tool/node.h"
26 #include "ui/tool/multi-path-manipulator.h"
27 #include "ui/tool/path-manipulator.h"
29 namespace std { using namespace __gnu_cxx; }
31 namespace __gnu_cxx {
32 template<>
33 struct hash<Inkscape::UI::NodeList::iterator> {
34     size_t operator()(Inkscape::UI::NodeList::iterator const &n) const {
35         return reinterpret_cast<size_t>(n.ptr());
36     }
37 };
38 }
40 namespace Inkscape {
41 namespace UI {
43 namespace {
44 typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair;
45 typedef std::vector<IterPair> IterPairList;
46 typedef std::hash_set<NodeList::iterator> IterSet;
47 typedef std::multimap<double, IterPair> DistanceMap;
48 typedef std::pair<double, IterPair> DistanceMapItem;
50 /** Find pairs of selected endnodes suitable for joining. */
51 void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs)
52 {
53     IterSet join_iters;
54     DistanceMap dists;
56     // find all endnodes in selection
57     for (ControlPointSelection::iterator i = sel.begin(); i != sel.end(); ++i) {
58         Node *node = dynamic_cast<Node*>(i->first);
59         if (!node) continue;
60         NodeList::iterator iter = NodeList::get_iterator(node);
61         if (!iter.next() || !iter.prev()) join_iters.insert(iter);
62     }
64     if (join_iters.size() < 2) return;
66     // Below we find the closest pairs. The algorithm is O(N^3).
67     // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs
68     // with their distances in a multimap (not worth it IMO).
69     while (join_iters.size() >= 2) {
70         double closest = DBL_MAX;
71         IterPair closest_pair;
72         for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) {
73             for (IterSet::iterator j = join_iters.begin(); j != i; ++j) {
74                 double dist = Geom::distance(**i, **j);
75                 if (dist < closest) {
76                     closest = dist;
77                     closest_pair = std::make_pair(*i, *j);
78                 }
79             }
80         }
81         pairs.push_back(closest_pair);
82         join_iters.erase(closest_pair.first);
83         join_iters.erase(closest_pair.second);
84     }
85 }
87 /** After this function, first should be at the end of path and second at the beginnning.
88  * @returns True if the nodes are in the same subpath */
89 bool prepare_join(IterPair &join_iters)
90 {
91     if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) {
92         if (join_iters.first.next()) // if first is begin, swap the iterators
93             std::swap(join_iters.first, join_iters.second);
94         return true;
95     }
97     NodeList &sp_first = NodeList::get(join_iters.first);
98     NodeList &sp_second = NodeList::get(join_iters.second);
99     if (join_iters.first.next()) { // first is begin
100         if (join_iters.second.next()) { // second is begin
101             sp_first.reverse();
102         } else { // second is end
103             std::swap(join_iters.first, join_iters.second);
104         }
105     } else { // first is end
106         if (join_iters.second.next()) { // second is begin
107             // do nothing
108         } else { // second is end
109             sp_second.reverse();
110         }
111     }
112     return false;
114 } // anonymous namespace
117 MultiPathManipulator::MultiPathManipulator(PathSharedData &data, sigc::connection &chg)
118     : PointManipulator(data.node_data.desktop, *data.node_data.selection)
119     , _path_data(data)
120     , _changed(chg)
122     _selection.signal_commit.connect(
123         sigc::mem_fun(*this, &MultiPathManipulator::_commit));
124     _selection.signal_point_changed.connect(
125         sigc::hide( sigc::hide(
126             signal_coords_changed.make_slot())));
129 MultiPathManipulator::~MultiPathManipulator()
131     _mmap.clear();
134 /** Remove empty manipulators. */
135 void MultiPathManipulator::cleanup()
137     for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
138         if (i->second->empty()) _mmap.erase(i++);
139         else ++i;
140     }
143 /** @brief Change the set of items to edit.
144  *
145  * This method attempts to preserve as much of the state as possible. */
146 void MultiPathManipulator::setItems(std::set<ShapeRecord> const &s)
148     std::set<ShapeRecord> shapes(s);
150     // iterate over currently edited items, modifying / removing them as necessary
151     for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) {
152         std::set<ShapeRecord>::iterator si = shapes.find(i->first);
153         if (si == shapes.end()) {
154             // This item is no longer supposed to be edited - remove its manipulator
155             _mmap.erase(i++);
156         } else {
157             ShapeRecord const &sr = i->first;
158             ShapeRecord const &sr_new = *si;
159             // if the shape record differs, replace the key only and modify other values
160             if (sr.edit_transform != sr_new.edit_transform ||
161                 sr.role != sr_new.role)
162             {
163                 boost::shared_ptr<PathManipulator> hold(i->second);
164                 if (sr.edit_transform != sr_new.edit_transform)
165                     hold->setControlsTransform(sr_new.edit_transform);
166                 if (sr.role != sr_new.role) {
167                     //hold->setOutlineColor(_getOutlineColor(sr_new.role));
168                 }
169                 _mmap.erase(sr);
170                 _mmap.insert(std::make_pair(sr_new, hold));
171             }
172             shapes.erase(si); // remove the processed record
173             ++i;
174         }
175     }
177     // add newly selected items
178     for (std::set<ShapeRecord>::iterator i = shapes.begin(); i != shapes.end(); ++i) {
179         ShapeRecord const &r = *i;
180         if (!SP_IS_PATH(r.item) && !IS_LIVEPATHEFFECT(r.item)) continue;
181         boost::shared_ptr<PathManipulator> newpm(new PathManipulator(*this, (SPPath*) r.item,
182             r.edit_transform, _getOutlineColor(r.role), r.lpe_key));
183         newpm->showHandles(_show_handles);
184         // always show outlines for clips and masks
185         newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL);
186         newpm->showPathDirection(_show_path_direction);
187         _mmap.insert(std::make_pair(r, newpm));
188     }
191 void MultiPathManipulator::selectSubpaths()
193     if (_selection.empty()) {
194         _selection.selectAll();
195     } else {
196         invokeForAll(&PathManipulator::selectSubpaths);
197     }
200 void MultiPathManipulator::shiftSelection(int dir)
202     invokeForAll(&PathManipulator::shiftSelection, dir);
205 void MultiPathManipulator::invertSelectionInSubpaths()
207     invokeForAll(&PathManipulator::invertSelectionInSubpaths);
210 void MultiPathManipulator::setNodeType(NodeType type)
212     if (_selection.empty()) return;
213     for (ControlPointSelection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
214         Node *node = dynamic_cast<Node*>(i->first);
215         if (node) node->setType(type);
216     }
217     _done(_("Change node type"));
220 void MultiPathManipulator::setSegmentType(SegmentType type)
222     if (_selection.empty()) return;
223     invokeForAll(&PathManipulator::setSegmentType, type);
224     if (type == SEGMENT_STRAIGHT) {
225         _done(_("Straighten segments"));
226     } else {
227         _done(_("Make segments curves"));
228     }
231 void MultiPathManipulator::insertNodes()
233     invokeForAll(&PathManipulator::insertNodes);
234     _done(_("Add nodes"));
237 void MultiPathManipulator::joinNodes()
239     invokeForAll(&PathManipulator::hideDragPoint);
240     // Node join has two parts. In the first one we join two subpaths by fusing endpoints
241     // into one. In the second we fuse nodes in each subpath.
242     IterPairList joins;
243     NodeList::iterator preserve_pos;
244     Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point);
245     if (mouseover_node) {
246         preserve_pos = NodeList::get_iterator(mouseover_node);
247     }
248     find_join_iterators(_selection, joins);
250     for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
251         bool same_path = prepare_join(*i);
252         bool mouseover = true;
253         NodeList &sp_first = NodeList::get(i->first);
254         NodeList &sp_second = NodeList::get(i->second);
255         i->first->setType(NODE_CUSP, false);
257         Geom::Point joined_pos, pos_front, pos_back;
258         pos_front = *i->second->front();
259         pos_back = *i->first->back();
260         if (i->first == preserve_pos) {
261             joined_pos = *i->first;
262         } else if (i->second == preserve_pos) {
263             joined_pos = *i->second;
264         } else {
265             joined_pos = Geom::middle_point(pos_back, pos_front);
266             mouseover = false;
267         }
269         // if the handles aren't degenerate, don't move them
270         i->first->move(joined_pos);
271         Node *joined_node = i->first.ptr();
272         if (!i->second->front()->isDegenerate()) {
273             joined_node->front()->setPosition(pos_front);
274         }
275         if (!i->first->back()->isDegenerate()) {
276             joined_node->back()->setPosition(pos_back);
277         }
278         if (mouseover) {
279             // Second node could be mouseovered, but it will be deleted, so we must change
280             // the preserve_pos iterator to the first node.
281             preserve_pos = i->first;
282         }
283         sp_second.erase(i->second);
285         if (same_path) {
286             sp_first.setClosed(true);
287         } else {
288             sp_first.splice(sp_first.end(), sp_second);
289             sp_second.kill();
290         }
291         _selection.insert(i->first.ptr());
292     }
294     if (joins.empty()) {
295         // Second part replaces contiguous selections of nodes with single nodes
296         invokeForAll(&PathManipulator::weldNodes, preserve_pos);
297     }
299     _doneWithCleanup(_("Join nodes"));
302 void MultiPathManipulator::breakNodes()
304     if (_selection.empty()) return;
305     invokeForAll(&PathManipulator::breakNodes);
306     _done(_("Break nodes"));
309 void MultiPathManipulator::deleteNodes(bool keep_shape)
311     if (_selection.empty()) return;
312     invokeForAll(&PathManipulator::deleteNodes, keep_shape);
313     _doneWithCleanup(_("Delete nodes"));
316 /** Join selected endpoints to create segments. */
317 void MultiPathManipulator::joinSegments()
319     IterPairList joins;
320     find_join_iterators(_selection, joins);
322     for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
323         bool same_path = prepare_join(*i);
324         NodeList &sp_first = NodeList::get(i->first);
325         NodeList &sp_second = NodeList::get(i->second);
326         i->first->setType(NODE_CUSP, false);
327         i->second->setType(NODE_CUSP, false);
328         if (same_path) {
329             sp_first.setClosed(true);
330         } else {
331             sp_first.splice(sp_first.end(), sp_second);
332             sp_second.kill();
333         }
334     }
336     if (joins.empty()) {
337         invokeForAll(&PathManipulator::weldSegments);
338     }
339     _doneWithCleanup("Join segments");
342 void MultiPathManipulator::deleteSegments()
344     if (_selection.empty()) return;
345     invokeForAll(&PathManipulator::deleteSegments);
346     _doneWithCleanup("Delete segments");
349 void MultiPathManipulator::alignNodes(Geom::Dim2 d)
351     _selection.align(d);
352     if (d == Geom::X) {
353         _done("Align nodes to a horizontal line");
354     } else {
355         _done("Align nodes to a vertical line");
356     }
359 void MultiPathManipulator::distributeNodes(Geom::Dim2 d)
361     _selection.distribute(d);
362     if (d == Geom::X) {
363         _done("Distrubute nodes horizontally");
364     } else {
365         _done("Distribute nodes vertically");
366     }
369 void MultiPathManipulator::reverseSubpaths()
371     if (_selection.empty()) {
372         invokeForAll(&PathManipulator::reverseSubpaths, false);
373         _done("Reverse subpaths");
374     } else {
375         invokeForAll(&PathManipulator::reverseSubpaths, true);
376         _done("Reverse selected subpaths");
377     }
380 void MultiPathManipulator::move(Geom::Point const &delta)
382     _selection.transform(Geom::Translate(delta));
383     _done("Move nodes");
386 void MultiPathManipulator::showOutline(bool show)
388     for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
389         // always show outlines for clipping paths and masks
390         i->second->showOutline(show || i->first.role != SHAPE_ROLE_NORMAL);
391     }
392     _show_outline = show;
395 void MultiPathManipulator::showHandles(bool show)
397     invokeForAll(&PathManipulator::showHandles, show);
398     _show_handles = show;
401 void MultiPathManipulator::showPathDirection(bool show)
403     invokeForAll(&PathManipulator::showPathDirection, show);
404     _show_path_direction = show;
407 void MultiPathManipulator::updateOutlineColors()
409     //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
410     //    i->second->setOutlineColor(_getOutlineColor(i->first.role));
411     //}
414 bool MultiPathManipulator::event(GdkEvent *event)
416     switch (event->type) {
417     case GDK_KEY_PRESS:
418         switch (shortcut_key(event->key)) {
419         case GDK_Insert:
420         case GDK_KP_Insert:
421             // Insert - insert nodes in the middle of selected segments
422             insertNodes();
423             return true;
424         case GDK_i:
425         case GDK_I:
426             if (held_only_shift(event->key)) {
427                 // Shift+I - insert nodes (alternate keybinding for Mac keyboards
428                 //           that don't have the Insert key)
429                 insertNodes();
430                 return true;
431             }
432             break;
433         case GDK_j:
434         case GDK_J:
435             if (held_only_shift(event->key)) {
436                 // Shift+J - join nodes
437                 joinNodes();
438                 return true;
439             }
440             if (held_only_alt(event->key)) {
441                 // Alt+J - join segments
442                 joinSegments();
443                 return true;
444             }
445             break;
446         case GDK_b:
447         case GDK_B:
448             if (held_only_shift(event->key)) {
449                 // Shift+B - break nodes
450                 breakNodes();
451                 return true;
452             }
453             break;
454         case GDK_Delete:
455         case GDK_KP_Delete:
456         case GDK_BackSpace:
457             if (held_shift(event->key)) break;
458             if (held_alt(event->key)) {
459                 // Alt+Delete - delete segments
460                 deleteSegments();
461             } else {
462                 // Control+Delete - delete nodes
463                 // Delete - delete nodes preserving shape
464                 deleteNodes(!held_control(event->key));
465             }
466             return true;
467         case GDK_c:
468         case GDK_C:
469             if (held_only_shift(event->key)) {
470                 // Shift+C - make nodes cusp
471                 setNodeType(NODE_CUSP);
472                 return true;
473             }
474             break;
475         case GDK_s:
476         case GDK_S:
477             if (held_only_shift(event->key)) {
478                 // Shift+S - make nodes smooth
479                 setNodeType(NODE_SMOOTH);
480                 return true;
481             }
482             break;
483         case GDK_a:
484         case GDK_A:
485             if (held_only_shift(event->key)) {
486                 // Shift+A - make nodes auto-smooth
487                 setNodeType(NODE_AUTO);
488                 return true;
489             }
490             break;
491         case GDK_y:
492         case GDK_Y:
493             if (held_only_shift(event->key)) {
494                 // Shift+Y - make nodes symmetric
495                 setNodeType(NODE_SYMMETRIC);
496                 return true;
497             }
498             break;
499         case GDK_r:
500         case GDK_R:
501             if (held_only_shift(event->key)) {
502                 // Shift+R - reverse subpaths
503                 reverseSubpaths();
504             }
505             break;
506         default:
507             break;
508         }
509         break;
510     default: break;
511     }
513     for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
514         if (i->second->event(event)) return true;
515     }
516     return false;
519 /** Commit changes to XML and add undo stack entry based on the action that was done. Invoked
520  * by sub-manipulators, for example TransformHandleSet and ControlPointSelection. */
521 void MultiPathManipulator::_commit(CommitEvent cps)
523     gchar const *reason = NULL;
524     gchar const *key = NULL;
525     switch(cps) {
526     case COMMIT_MOUSE_MOVE:
527         reason = _("Move nodes");
528         break;
529     case COMMIT_KEYBOARD_MOVE_X:
530         reason = _("Move nodes horizontally");
531         key = "node:move:x";
532         break;
533     case COMMIT_KEYBOARD_MOVE_Y:
534         reason = _("Move nodes vertically");
535         key = "node:move:y";
536         break;
537     case COMMIT_MOUSE_ROTATE:
538         reason = _("Rotate nodes");
539         break;
540     case COMMIT_KEYBOARD_ROTATE:
541         reason = _("Rotate nodes");
542         key = "node:rotate";
543         break;
544     case COMMIT_MOUSE_SCALE_UNIFORM:
545         reason = _("Scale nodes uniformly");
546         break;
547     case COMMIT_MOUSE_SCALE:
548         reason = _("Scale nodes");
549         break;
550     case COMMIT_KEYBOARD_SCALE_UNIFORM:
551         reason = _("Scale nodes uniformly");
552         key = "node:scale:uniform";
553         break;
554     case COMMIT_KEYBOARD_SCALE_X:
555         reason = _("Scale nodes horizontally");
556         key = "node:scale:x";
557         break;
558     case COMMIT_KEYBOARD_SCALE_Y:
559         reason = _("Scale nodes vertically");
560         key = "node:scale:y";
561         break;
562     case COMMIT_FLIP_X:
563         reason = _("Flip nodes horizontally");
564         break;
565     case COMMIT_FLIP_Y:
566         reason = _("Flip nodes vertically");
567         break;
568     default: return;
569     }
570     
571     _selection.signal_update.emit();
572     invokeForAll(&PathManipulator::writeXML);
573     if (key) {
574         sp_document_maybe_done(sp_desktop_document(_desktop), key, SP_VERB_CONTEXT_NODE, reason);
575     } else {
576         sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
577     }
578     signal_coords_changed.emit();
581 /** Commits changes to XML and adds undo stack entry. */
582 void MultiPathManipulator::_done(gchar const *reason) {
583     invokeForAll(&PathManipulator::update);
584     invokeForAll(&PathManipulator::writeXML);
585     sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
586     signal_coords_changed.emit();
589 /** Commits changes to XML, adds undo stack entry and removes empty manipulators. */
590 void MultiPathManipulator::_doneWithCleanup(gchar const *reason) {
591     _changed.block();
592     _done(reason);
593     cleanup();
594     _changed.unblock();
597 /** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */
598 guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role)
600     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
601     switch(role) {
602     case SHAPE_ROLE_CLIPPING_PATH:
603         return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff);
604     case SHAPE_ROLE_MASK:
605         return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff);
606     case SHAPE_ROLE_LPE_PARAM:
607         return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff);
608     case SHAPE_ROLE_NORMAL:
609     default:
610         return prefs->getColor("/tools/nodes/outline_color", 0xff0000ff);
611     }
614 } // namespace UI
615 } // namespace Inkscape
617 /*
618   Local Variables:
619   mode:c++
620   c-file-style:"stroustrup"
621   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
622   indent-tabs-mode:nil
623   fill-column:99
624   End:
625 */
626 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :