Code

Fix LPEs and break mask transform undo
[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 <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 two selected endnodes.
41  * @returns -1 if not enough endnodes selected, 1 if too many, 0 if OK */
42 void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs)
43 {
44     IterSet join_iters;
45     DistanceMap dists;
47     // find all endnodes in selection
48     for (ControlPointSelection::iterator i = sel.begin(); i != sel.end(); ++i) {
49         Node *node = dynamic_cast<Node*>(i->first);
50         if (!node) continue;
51         NodeList::iterator iter = NodeList::get_iterator(node);
52         if (!iter.next() || !iter.prev()) join_iters.insert(iter);
53     }
55     if (join_iters.size() < 2) return;
57     // Below we find the closest pairs. The algorithm is O(N^3).
58     // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs
59     // with their distances in a multimap (not worth it IMO).
60     while (join_iters.size() >= 2) {
61         double closest = DBL_MAX;
62         IterPair closest_pair;
63         for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) {
64             for (IterSet::iterator j = join_iters.begin(); j != i; ++j) {
65                 double dist = Geom::distance(**i, **j);
66                 if (dist < closest) {
67                     closest = dist;
68                     closest_pair = std::make_pair(*i, *j);
69                 }
70             }
71         }
72         pairs.push_back(closest_pair);
73         join_iters.erase(closest_pair.first);
74         join_iters.erase(closest_pair.second);
75     }
76 }
78 /** After this function, first should be at the end of path and second at the beginnning.
79  * @returns True if the nodes are in the same subpath */
80 bool prepare_join(IterPair &join_iters)
81 {
82     if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) {
83         if (join_iters.first.next()) // if first is begin, swap the iterators
84             std::swap(join_iters.first, join_iters.second);
85         return true;
86     }
88     NodeList &sp_first = NodeList::get(join_iters.first);
89     NodeList &sp_second = NodeList::get(join_iters.second);
90     if (join_iters.first.next()) { // first is begin
91         if (join_iters.second.next()) { // second is begin
92             sp_first.reverse();
93         } else { // second is end
94             std::swap(join_iters.first, join_iters.second);
95         }
96     } else { // first is end
97         if (join_iters.second.next()) { // second is begin
98             // do nothing
99         } else { // second is end
100             sp_second.reverse();
101         }
102     }
103     return false;
105 } // anonymous namespace
108 MultiPathManipulator::MultiPathManipulator(PathSharedData const &data, sigc::connection &chg)
109     : PointManipulator(data.node_data.desktop, *data.node_data.selection)
110     , _path_data(data)
111     , _changed(chg)
113     //
114     _selection.signal_commit.connect(
115         sigc::mem_fun(*this, &MultiPathManipulator::_commit));
116     _selection.signal_point_changed.connect(
117         sigc::hide( sigc::hide(
118             signal_coords_changed.make_slot())));
121 MultiPathManipulator::~MultiPathManipulator()
123     _mmap.clear();
126 /** Remove empty manipulators. */
127 void MultiPathManipulator::cleanup()
129     for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) {
130         if (i->second->empty()) _mmap.erase(i++);
131         else ++i;
132     }
135 /** @brief Change the set of items to edit.
136  *
137  * This method attempts to preserve as much of the state as possible. */
138 void MultiPathManipulator::setItems(std::set<ShapeRecord> const &s)
140     std::set<ShapeRecord> shapes(s);
142     // iterate over currently edited items, modifying / removing them as necessary
143     for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) {
144         std::set<ShapeRecord>::iterator si = shapes.find(i->first);
145         if (si == shapes.end()) {
146             // This item is no longer supposed to be edited - remove its manipulator
147             _mmap.erase(i++);
148         } else {
149             ShapeRecord const &sr = i->first;
150             ShapeRecord const &sr_new = *si;
151             // if the shape record differs, replace the key only and modify other values
152             if (sr.edit_transform != sr_new.edit_transform ||
153                 sr.role != sr_new.role)
154             {
155                 boost::shared_ptr<PathManipulator> hold(i->second);
156                 if (sr.edit_transform != sr_new.edit_transform)
157                     hold->setControlsTransform(sr_new.edit_transform);
158                 if (sr.role != sr_new.role) {
159                     //hold->setOutlineColor(_getOutlineColor(sr_new.role));
160                 }
161                 _mmap.erase(sr);
162                 _mmap.insert(std::make_pair(sr_new, hold));
163             }
164             shapes.erase(si); // remove the processed record
165             ++i;
166         }
167     }
169     // add newly selected items
170     for (std::set<ShapeRecord>::iterator i = shapes.begin(); i != shapes.end(); ++i) {
171         ShapeRecord const &r = *i;
172         if (!SP_IS_PATH(r.item) && !IS_LIVEPATHEFFECT(r.item)) continue;
173         boost::shared_ptr<PathManipulator> newpm(new PathManipulator(_path_data, (SPPath*) r.item,
174             r.edit_transform, _getOutlineColor(r.role), r.lpe_key));
175         newpm->showHandles(_show_handles);
176         // always show outlines for clips and masks
177         newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL);
178         newpm->showPathDirection(_show_path_direction);
179         _mmap.insert(std::make_pair(r, newpm));
180     }
183 void MultiPathManipulator::selectSubpaths()
185     if (_selection.empty()) {
186         invokeForAll(&PathManipulator::selectAll);
187     } else {
188         invokeForAll(&PathManipulator::selectSubpaths);
189     }
191 void MultiPathManipulator::selectAll()
193     invokeForAll(&PathManipulator::selectAll);
196 void MultiPathManipulator::selectArea(Geom::Rect const &area, bool take)
198     if (take) _selection.clear();
199     invokeForAll(&PathManipulator::selectArea, area);
202 void MultiPathManipulator::shiftSelection(int dir)
204     invokeForAll(&PathManipulator::shiftSelection, dir);
206 void MultiPathManipulator::invertSelection()
208     invokeForAll(&PathManipulator::invertSelection);
210 void MultiPathManipulator::invertSelectionInSubpaths()
212     invokeForAll(&PathManipulator::invertSelectionInSubpaths);
214 void MultiPathManipulator::deselect()
216     _selection.clear();
219 void MultiPathManipulator::setNodeType(NodeType type)
221     if (_selection.empty()) return;
222     for (ControlPointSelection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
223         Node *node = dynamic_cast<Node*>(i->first);
224         if (node) node->setType(type);
225     }
226     _done(_("Change node type"));
229 void MultiPathManipulator::setSegmentType(SegmentType type)
231     if (_selection.empty()) return;
232     invokeForAll(&PathManipulator::setSegmentType, type);
233     if (type == SEGMENT_STRAIGHT) {
234         _done(_("Straighten segments"));
235     } else {
236         _done(_("Make segments curves"));
237     }
240 void MultiPathManipulator::insertNodes()
242     invokeForAll(&PathManipulator::insertNodes);
243     _done(_("Add nodes"));
246 void MultiPathManipulator::joinNodes()
248     invokeForAll(&PathManipulator::hideDragPoint);
249     // Node join has two parts. In the first one we join two subpaths by fusing endpoints
250     // into one. In the second we fuse nodes in each subpath.
251     IterPairList joins;
252     NodeList::iterator preserve_pos;
253     Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point);
254     if (mouseover_node) {
255         preserve_pos = NodeList::get_iterator(mouseover_node);
256     }
257     find_join_iterators(_selection, joins);
259     for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
260         bool same_path = prepare_join(*i);
261         bool mouseover = true;
262         NodeList &sp_first = NodeList::get(i->first);
263         NodeList &sp_second = NodeList::get(i->second);
264         i->first->setType(NODE_CUSP, false);
266         Geom::Point joined_pos, pos_front, pos_back;
267         pos_front = *i->second->front();
268         pos_back = *i->first->back();
269         if (i->first == preserve_pos) {
270             joined_pos = *i->first;
271         } else if (i->second == preserve_pos) {
272             joined_pos = *i->second;
273         } else {
274             joined_pos = Geom::middle_point(pos_back, pos_front);
275             mouseover = false;
276         }
278         // if the handles aren't degenerate, don't move them
279         i->first->move(joined_pos);
280         Node *joined_node = i->first.ptr();
281         if (!i->second->front()->isDegenerate()) {
282             joined_node->front()->setPosition(pos_front);
283         }
284         if (!i->first->back()->isDegenerate()) {
285             joined_node->back()->setPosition(pos_back);
286         }
287         if (mouseover) {
288             // Second node could be mouseovered, but it will be deleted, so we must change
289             // the preserve_pos iterator to the first node.
290             preserve_pos = i->first;
291         }
292         sp_second.erase(i->second);
294         if (same_path) {
295             sp_first.setClosed(true);
296         } else {
297             sp_first.splice(sp_first.end(), sp_second);
298             sp_second.kill();
299         }
300         _selection.insert(i->first.ptr());
301     }
302     // Second part replaces contiguous selections of nodes with single nodes
303     invokeForAll(&PathManipulator::weldNodes, preserve_pos);
304     _doneWithCleanup(_("Join nodes"));
307 void MultiPathManipulator::breakNodes()
309     if (_selection.empty()) return;
310     invokeForAll(&PathManipulator::breakNodes);
311     _done(_("Break nodes"));
314 void MultiPathManipulator::deleteNodes(bool keep_shape)
316     if (_selection.empty()) return;
317     invokeForAll(&PathManipulator::deleteNodes, keep_shape);
318     _doneWithCleanup(_("Delete nodes"));
321 /** Join selected endpoints to create segments. */
322 void MultiPathManipulator::joinSegment()
324     IterPairList joins;
325     find_join_iterators(_selection, joins);
326     if (joins.empty()) {
327         _desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE,
328             _("There must be at least 2 endnodes in selection"));
329         return;
330     }
332     for (IterPairList::iterator i = joins.begin(); i != joins.end(); ++i) {
333         bool same_path = prepare_join(*i);
334         NodeList &sp_first = NodeList::get(i->first);
335         NodeList &sp_second = NodeList::get(i->second);
336         i->first->setType(NODE_CUSP, false);
337         i->second->setType(NODE_CUSP, false);
338         if (same_path) {
339             sp_first.setClosed(true);
340         } else {
341             sp_first.splice(sp_first.end(), sp_second);
342             sp_second.kill();
343         }
344     }
346     _doneWithCleanup("Join segment");
349 void MultiPathManipulator::deleteSegments()
351     if (_selection.empty()) return;
352     invokeForAll(&PathManipulator::deleteSegments);
353     _doneWithCleanup("Delete segments");
356 void MultiPathManipulator::alignNodes(Geom::Dim2 d)
358     _selection.align(d);
359     if (d == Geom::X) {
360         _done("Align nodes to a horizontal line");
361     } else {
362         _done("Align nodes to a vertical line");
363     }
366 void MultiPathManipulator::distributeNodes(Geom::Dim2 d)
368     _selection.distribute(d);
369     if (d == Geom::X) {
370         _done("Distrubute nodes horizontally");
371     } else {
372         _done("Distribute nodes vertically");
373     }
376 void MultiPathManipulator::reverseSubpaths()
378     invokeForAll(&PathManipulator::reverseSubpaths);
379     _done("Reverse selected subpaths");
382 void MultiPathManipulator::move(Geom::Point const &delta)
384     _selection.transform(Geom::Translate(delta));
385     _done("Move nodes");
388 void MultiPathManipulator::showOutline(bool show)
390     for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
391         // always show outlines for clipping paths and masks
392         i->second->showOutline(show || i->first.role != SHAPE_ROLE_NORMAL);
393     }
394     _show_outline = show;
397 void MultiPathManipulator::showHandles(bool show)
399     invokeForAll(&PathManipulator::showHandles, show);
400     _show_handles = show;
403 void MultiPathManipulator::showPathDirection(bool show)
405     invokeForAll(&PathManipulator::showPathDirection, show);
406     _show_path_direction = show;
409 void MultiPathManipulator::updateOutlineColors()
411     //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
412     //    i->second->setOutlineColor(_getOutlineColor(i->first.role));
413     //}
416 bool MultiPathManipulator::event(GdkEvent *event)
418     switch (event->type) {
419     case GDK_KEY_PRESS:
420         switch (shortcut_key(event->key)) {
421         case GDK_Insert:
422         case GDK_KP_Insert:
423             insertNodes();
424             return true;
425         case GDK_i:
426         case GDK_I:
427             if (held_only_shift(event->key)) {
428                 insertNodes();
429                 return true;
430             }
431             break;
432         case GDK_j:
433         case GDK_J:
434             if (held_only_shift(event->key)) {
435                 joinNodes();
436                 return true;
437             }
438             if (held_only_alt(event->key)) {
439                 joinSegment();
440                 return true;
441             }
442             break;
443         case GDK_b:
444         case GDK_B:
445             if (held_only_shift(event->key)) {
446                 breakNodes();
447                 return true;
448             }
449             break;
450         case GDK_Delete:
451         case GDK_KP_Delete:
452         case GDK_BackSpace:
453             if (held_shift(event->key)) break;
454             if (held_alt(event->key)) {
455                 deleteSegments();
456             } else {
457                 deleteNodes(!held_control(event->key));
458             }
459             return true;
460         case GDK_c:
461         case GDK_C:
462             if (held_only_shift(event->key)) {
463                 setNodeType(NODE_CUSP);
464                 return true;
465             }
466             break;
467         case GDK_s:
468         case GDK_S:
469             if (held_only_shift(event->key)) {
470                 setNodeType(NODE_SMOOTH);
471                 return true;
472             }
473             break;
474         case GDK_a:
475         case GDK_A:
476             if (held_only_shift(event->key)) {
477                 setNodeType(NODE_AUTO);
478                 return true;
479             }
480             break;
481         case GDK_y:
482         case GDK_Y:
483             if (held_only_shift(event->key)) {
484                 setNodeType(NODE_SYMMETRIC);
485                 return true;
486             }
487             break;
488         case GDK_r:
489         case GDK_R:
490             if (held_only_shift(event->key)) {
491                 reverseSubpaths();
492                 break;
493             }
494             break;
495         default:
496             break;
497         }
498         break;
499     default: break;
500     }
502     for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
503         if (i->second->event(event)) return true;
504     }
505     return false;
508 void MultiPathManipulator::_commit(CommitEvent cps)
510     gchar const *reason = NULL;
511     gchar const *key = NULL;
512     switch(cps) {
513     case COMMIT_MOUSE_MOVE:
514         reason = _("Move nodes");
515         break;
516     case COMMIT_KEYBOARD_MOVE_X:
517         reason = _("Move nodes horizontally");
518         key = "node:move:x";
519         break;
520     case COMMIT_KEYBOARD_MOVE_Y:
521         reason = _("Move nodes vertically");
522         key = "node:move:y";
523         break;
524     case COMMIT_MOUSE_ROTATE:
525         reason = _("Rotate nodes");
526         break;
527     case COMMIT_KEYBOARD_ROTATE:
528         reason = _("Rotate nodes");
529         key = "node:rotate";
530         break;
531     case COMMIT_MOUSE_SCALE_UNIFORM:
532         reason = _("Scale nodes uniformly");
533         break;
534     case COMMIT_MOUSE_SCALE:
535         reason = _("Scale nodes");
536         break;
537     case COMMIT_KEYBOARD_SCALE_UNIFORM:
538         reason = _("Scale nodes uniformly");
539         key = "node:scale:uniform";
540         break;
541     case COMMIT_KEYBOARD_SCALE_X:
542         reason = _("Scale nodes horizontally");
543         key = "node:scale:x";
544         break;
545     case COMMIT_KEYBOARD_SCALE_Y:
546         reason = _("Scale nodes vertically");
547         key = "node:scale:y";
548         break;
549     case COMMIT_FLIP_X:
550         reason = _("Flip nodes horizontally");
551         break;
552     case COMMIT_FLIP_Y:
553         reason = _("Flip nodes vertically");
554         break;
555     default: return;
556     }
557     
558     _selection.signal_update.emit();
559     invokeForAll(&PathManipulator::writeXML);
560     if (key) {
561         sp_document_maybe_done(sp_desktop_document(_desktop), key, SP_VERB_CONTEXT_NODE, reason);
562     } else {
563         sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
564     }
565     signal_coords_changed.emit();
568 /** Commits changes to XML and adds undo stack entry. */
569 void MultiPathManipulator::_done(gchar const *reason) {
570     invokeForAll(&PathManipulator::update);
571     invokeForAll(&PathManipulator::writeXML);
572     sp_document_done(sp_desktop_document(_desktop), SP_VERB_CONTEXT_NODE, reason);
573     signal_coords_changed.emit();
576 /** Commits changes to XML, adds undo stack entry and removes empty manipulators. */
577 void MultiPathManipulator::_doneWithCleanup(gchar const *reason) {
578     _changed.block();
579     _done(reason);
580     cleanup();
581     _changed.unblock();
584 guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role)
586     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
587     switch(role) {
588     case SHAPE_ROLE_CLIPPING_PATH:
589         return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff);
590     case SHAPE_ROLE_MASK:
591         return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff);
592     case SHAPE_ROLE_LPE_PARAM:
593         return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff);
594     case SHAPE_ROLE_NORMAL:
595     default:
596         return prefs->getColor("/tools/nodes/outline_color", 0xff0000ff);
597     }
600 } // namespace UI
601 } // namespace Inkscape
603 /*
604   Local Variables:
605   mode:c++
606   c-file-style:"stroustrup"
607   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
608   indent-tabs-mode:nil
609   fill-column:99
610   End:
611 */
612 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :