Code

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