Code

Fix snapping in the node tool when more than one point is dragged.
[inkscape.git] / src / ui / tool / control-point-selection.cpp
1 /** @file
2  * Node selection - 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 <2geom/transforms.h>
12 #include "desktop.h"
13 #include "preferences.h"
14 #include "ui/tool/control-point-selection.h"
15 #include "ui/tool/event-utils.h"
16 #include "ui/tool/selectable-control-point.h"
17 #include "ui/tool/transform-handle-set.h"
19 namespace Inkscape {
20 namespace UI {
22 /**
23  * @class ControlPointSelection
24  * @brief Group of selected control points.
25  *
26  * Some operations can be performed on all selected points regardless of their type, therefore
27  * this class is also a Manipulator. It handles the transformations of points using
28  * the keyboard.
29  *
30  * The exposed interface is similar to that of an STL set. Internally, a hash map is used.
31  * @todo Correct iterators (that don't expose the connection list)
32  */
34 /** @var ControlPointSelection::signal_update
35  * Fires when the display needs to be updated to reflect changes.
36  */
37 /** @var ControlPointSelection::signal_point_changed
38  * Fires when a control point is added to or removed from the selection.
39  * The first param contains a pointer to the control point that changed sel. state. 
40  * The second says whether the point is currently selected.
41  */
42 /** @var ControlPointSelection::signal_commit
43  * Fires when a change that needs to be committed to XML happens.
44  */
46 ControlPointSelection::ControlPointSelection(SPDesktop *d, SPCanvasGroup *th_group)
47     : Manipulator(d)
48     , _handles(new TransformHandleSet(d, th_group))
49     , _dragging(false)
50     , _handles_visible(true)
51     , _one_node_handles(false)
52 {
53     signal_update.connect( sigc::bind(
54         sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
55         true));
56     signal_point_changed.connect(
57         sigc::hide( sigc::hide(
58             sigc::bind(
59                 sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
60                 false))));
61     _handles->signal_transform.connect(
62         sigc::mem_fun(*this, &ControlPointSelection::transform));
63     _handles->signal_commit.connect(
64         sigc::mem_fun(*this, &ControlPointSelection::_commitTransform));
65 }
67 ControlPointSelection::~ControlPointSelection()
68 {
69     clear();
70     delete _handles;
71 }
73 /** Add a control point to the selection. */
74 std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(const value_type &x)
75 {
76     iterator found = _points.find(x);
77     if (found != _points.end()) {
78         return std::pair<iterator, bool>(found, false);
79     }
81     boost::shared_ptr<connlist_type> clist(new connlist_type());
83     // hide event param and always return false
84     clist->push_back(
85         x->signal_grabbed.connect(
86             sigc::bind_return(
87                 sigc::hide(
88                     sigc::mem_fun(*this, &ControlPointSelection::_pointGrabbed)),
89                 false)));
90     clist->push_back(
91         x->signal_dragged.connect(
92                 sigc::mem_fun(*this, &ControlPointSelection::_pointDragged)));
93     clist->push_back(
94         x->signal_ungrabbed.connect(
95             sigc::hide(
96                 sigc::mem_fun(*this, &ControlPointSelection::_pointUngrabbed))));
97     clist->push_back(
98         x->signal_clicked.connect(
99             sigc::bind<0>(
100                 sigc::mem_fun(*this, &ControlPointSelection::_pointClicked),
101                 x)));
103     found = _points.insert(std::make_pair(x, clist)).first;
105     x->updateState();
106     _rot_radius.reset();
107     signal_point_changed.emit(x, true);
109     return std::pair<iterator, bool>(found, true);
112 /** Remove a point from the selection. */
113 void ControlPointSelection::erase(iterator pos)
115     SelectableControlPoint *erased = pos->first;
116     boost::shared_ptr<connlist_type> clist = pos->second;
117     for (connlist_type::iterator i = clist->begin(); i != clist->end(); ++i) {
118         i->disconnect();
119     }
120     _points.erase(pos);
121     erased->updateState();
122     _rot_radius.reset();
123     signal_point_changed.emit(erased, false);
125 ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k)
127     iterator pos = _points.find(k);
128     if (pos == _points.end()) return 0;
129     erase(pos);
130     return 1;
132 void ControlPointSelection::erase(iterator first, iterator last)
134     while (first != last) erase(first++);
137 /** Remove all points from the selection, making it empty. */
138 void ControlPointSelection::clear()
140     for (iterator i = begin(); i != end(); )
141         erase(i++);
144 /** Select all points that this selection can contain. */
145 void ControlPointSelection::selectAll()
147     for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
148         insert(*i);
149     }
151 /** Select all points inside the given rectangle (in desktop coordinates). */
152 void ControlPointSelection::selectArea(Geom::Rect const &r)
154     for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
155         if (r.contains(**i))
156             insert(*i);
157     }
159 /** Unselect all selected points and select all unselected points. */
160 void ControlPointSelection::invertSelection()
162     for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
163         if ((*i)->selected()) erase(*i);
164         else insert(*i);
165     }
167 void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir)
169     bool grow = (dir > 0);
170     Geom::Point p = origin->position();
171     double best_dist = grow ? HUGE_VAL : 0;
172     SelectableControlPoint *match = NULL;
173     for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
174         bool selected = (*i)->selected();
175         if (grow && !selected) {
176             double dist = Geom::distance((*i)->position(), p);
177             if (dist < best_dist) {
178                 best_dist = dist;
179                 match = *i;
180             }
181         }
182         if (!grow && selected) {
183             double dist = Geom::distance((*i)->position(), p);
184             // use >= to also deselect the origin node when it's the last one selected
185             if (dist >= best_dist) {
186                 best_dist = dist;
187                 match = *i;
188             }
189         }
190     }
191     if (match) {
192         if (grow) insert(match);
193         else erase(match);
194     }
197 /** Transform all selected control points by the given affine transformation. */
198 void ControlPointSelection::transform(Geom::Matrix const &m)
200     for (iterator i = _points.begin(); i != _points.end(); ++i) {
201         SelectableControlPoint *cur = i->first;
202         cur->transform(m);
203     }
204     // TODO preserving the rotation radius needs some rethinking...
205     if (_rot_radius) (*_rot_radius) *= m.descrim();
206     signal_update.emit();
209 /** Align control points on the specified axis. */
210 void ControlPointSelection::align(Geom::Dim2 axis)
212     if (empty()) return;
213     Geom::Dim2 d = static_cast<Geom::Dim2>((axis + 1) % 2);
215     Geom::OptInterval bound;
216     for (iterator i = _points.begin(); i != _points.end(); ++i) {
217         bound.unionWith(Geom::OptInterval(i->first->position()[d]));
218     }
220     double new_coord = bound->middle();
221     for (iterator i = _points.begin(); i != _points.end(); ++i) {
222         Geom::Point pos = i->first->position();
223         pos[d] = new_coord;
224         i->first->move(pos);
225     }
228 /** Equdistantly distribute control points by moving them in the specified dimension. */
229 void ControlPointSelection::distribute(Geom::Dim2 d)
231     if (empty()) return;
233     // this needs to be a multimap, otherwise it will fail when some points have the same coord
234     typedef std::multimap<double, SelectableControlPoint*> SortMap;
236     SortMap sm;
237     Geom::OptInterval bound;
238     // first we insert all points into a multimap keyed by the aligned coord to sort them
239     // simultaneously we compute the extent of selection
240     for (iterator i = _points.begin(); i != _points.end(); ++i) {
241         Geom::Point pos = i->first->position();
242         sm.insert(std::make_pair(pos[d], i->first));
243         bound.unionWith(Geom::OptInterval(pos[d]));
244     }
246     // now we iterate over the multimap and set aligned positions.
247     double step = size() == 1 ? 0 : bound->extent() / (size() - 1);
248     double start = bound->min();
249     unsigned num = 0;
250     for (SortMap::iterator i = sm.begin(); i != sm.end(); ++i, ++num) {
251         Geom::Point pos = i->second->position();
252         pos[d] = start + num * step;
253         i->second->move(pos);
254     }
257 /** Get the bounds of the selection.
258  * @return Smallest rectangle containing the positions of all selected points,
259  *         or nothing if the selection is empty */
260 Geom::OptRect ControlPointSelection::pointwiseBounds()
262     Geom::OptRect bound;
263     for (iterator i = _points.begin(); i != _points.end(); ++i) {
264         SelectableControlPoint *cur = i->first;
265         Geom::Point p = cur->position();
266         if (!bound) {
267             bound = Geom::Rect(p, p);
268         } else {
269             bound->expandTo(p);
270         }
271     }
272     return bound;
275 Geom::OptRect ControlPointSelection::bounds()
277     Geom::OptRect bound;
278     for (iterator i = _points.begin(); i != _points.end(); ++i) {
279         SelectableControlPoint *cur = i->first;
280         Geom::OptRect r = cur->bounds();
281         bound.unionWith(r);
282     }
283     return bound;
286 void ControlPointSelection::showTransformHandles(bool v, bool one_node)
288     _one_node_handles = one_node;
289     _handles_visible = v;
290     _updateTransformHandles(false);
293 void ControlPointSelection::hideTransformHandles()
295     _handles->setVisible(false);
297 void ControlPointSelection::restoreTransformHandles()
299     _updateTransformHandles(true);
302 void ControlPointSelection::toggleTransformHandlesMode()
304     if (_handles->mode() == TransformHandleSet::MODE_SCALE) {
305         _handles->setMode(TransformHandleSet::MODE_ROTATE_SKEW);
306         if (size() == 1) _handles->rotationCenter().setVisible(false);
307     } else {
308         _handles->setMode(TransformHandleSet::MODE_SCALE);
309     }
312 void ControlPointSelection::_pointGrabbed()
314     hideTransformHandles();
315     _dragging = true;
318 void ControlPointSelection::_pointDragged(Geom::Point const &old_pos, Geom::Point &new_pos,
319                                           GdkEventMotion */*event*/)
321     Geom::Point delta = new_pos - old_pos;
322     for (iterator i = _points.begin(); i != _points.end(); ++i) {
323         SelectableControlPoint *cur = i->first;
324         cur->move(cur->position() + delta);
325     }
326     _handles->rotationCenter().move(_handles->rotationCenter().position() + delta);
327     signal_update.emit();
330 void ControlPointSelection::_pointUngrabbed()
332     _dragging = false;
333     _grabbed_point = NULL;
334     restoreTransformHandles();
335     signal_commit.emit(COMMIT_MOUSE_MOVE);
338 bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventButton *event)
340     // clicking a selected node should toggle the transform handles between rotate and scale mode,
341     // if they are visible
342     if (held_shift(*event)) return false;
343     if (_handles_visible && p->selected()) {
344         toggleTransformHandlesMode();
345         return true;
346     }
347     return false;
350 void ControlPointSelection::_updateTransformHandles(bool preserve_center)
352     if (_dragging) return;
354     if (_handles_visible && size() > 1) {
355         Geom::OptRect b = pointwiseBounds();
356         _handles->setBounds(*b, preserve_center);
357         _handles->setVisible(true);
358     } else if (_one_node_handles && size() == 1) { // only one control point in selection
359         SelectableControlPoint *p = begin()->first;
360         _handles->setBounds(p->bounds());
361         _handles->rotationCenter().move(p->position());
362         _handles->rotationCenter().setVisible(false);
363         _handles->setVisible(true);
364     } else {
365         _handles->setVisible(false);
366     }
369 /** Moves the selected points along the supplied unit vector according to
370  * the modifier state of the supplied event. */
371 bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir)
373     if (held_control(event)) return false;
374     unsigned num = 1 + consume_same_key_events(shortcut_key(event), 0);
376     Geom::Point delta = dir * num; 
377     if (held_shift(event)) delta *= 10;
378     if (held_alt(event)) {
379         delta /= _desktop->current_zoom();
380     } else {
381         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
382         double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000);
383         delta *= nudge;
384     }
386     transform(Geom::Translate(delta));
387     if (fabs(dir[Geom::X]) > 0) {
388         signal_commit.emit(COMMIT_KEYBOARD_MOVE_X);
389     } else {
390         signal_commit.emit(COMMIT_KEYBOARD_MOVE_Y);
391     }
392     return true;
395 /** Rotates the selected points in the given direction according to the modifier state
396  * from the supplied event.
397  * @param event Key event to take modifier state from
398  * @param dir   Direction of rotation (math convention: 1 = counterclockwise, -1 = clockwise)
399  */
400 bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
402     if (empty()) return false;
404     Geom::Point rc = _handles->rotationCenter();
405     if (!_rot_radius) {
406         Geom::Rect b = *(size() == 1 ? bounds() : pointwiseBounds());
407         double maxlen = 0;
408         for (unsigned i = 0; i < 4; ++i) {
409             double len = (b.corner(i) - rc).length();
410             if (len > maxlen) maxlen = len;
411         }
412         _rot_radius = maxlen;
413     }
415     double angle;
416     if (held_alt(event)) {
417         // Rotate by "one pixel". We interpret this as rotating by an angle that causes
418         // the topmost point of a circle circumscribed about the selection's bounding box
419         // to move on an arc 1 screen pixel long.
420         angle = atan2(1.0 / _desktop->current_zoom(), *_rot_radius) * dir;
421     } else {
422         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
423         int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
424         angle = M_PI * dir / snaps;
425     }
427     // translate to origin, rotate, translate back to original position
428     Geom::Matrix m = Geom::Translate(-rc)
429         * Geom::Rotate(angle) * Geom::Translate(rc);
430     transform(m);
431     signal_commit.emit(COMMIT_KEYBOARD_ROTATE);
432     return true;
436 bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir)
438     if (empty()) return false;
440     // TODO should the saved rotation center or the current center be used?
441     Geom::Rect bound = (size() == 1 ? *bounds() : *pointwiseBounds());
442     double maxext = bound.maxExtent();
443     if (Geom::are_near(maxext, 0)) return false;
444     Geom::Point center = _handles->rotationCenter().position();
446     double length_change;
447     if (held_alt(event)) {
448         // Scale by "one pixel". It means shrink/grow 1px for the larger dimension
449         // of the bounding box.
450         length_change = 1.0 / _desktop->current_zoom() * dir;
451     } else {
452         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
453         length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000);
454         length_change *= dir;
455     }
456     double scale = (maxext + length_change) / maxext;
457     
458     Geom::Matrix m = Geom::Translate(-center) * Geom::Scale(scale) * Geom::Translate(center);
459     transform(m);
460     signal_commit.emit(COMMIT_KEYBOARD_SCALE_UNIFORM);
461     return true;
464 bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d)
466     if (empty()) return false;
468     Geom::Scale scale_transform(1, 1);
469     if (d == Geom::X) {
470         scale_transform = Geom::Scale(-1, 1);
471     } else {
472         scale_transform = Geom::Scale(1, -1);
473     }
475     SelectableControlPoint *scp =
476         dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
477     Geom::Point center = scp ? scp->position() : _handles->rotationCenter().position();
479     Geom::Matrix m = Geom::Translate(-center) * scale_transform * Geom::Translate(center);
480     transform(m);
481     signal_commit.emit(d == Geom::X ? COMMIT_FLIP_X : COMMIT_FLIP_Y);
482     return true;
485 void ControlPointSelection::_commitTransform(CommitEvent ce)
487     _updateTransformHandles(true);
488     signal_commit.emit(ce);
491 bool ControlPointSelection::event(GdkEvent *event)
493     // implement generic event handling that should apply for all control point selections here;
494     // for example, keyboard moves and transformations. This way this functionality doesn't need
495     // to be duplicated in many places
496     // Later split out so that it can be reused in object selection
498     switch (event->type) {
499     case GDK_KEY_PRESS:
500         // do not handle key events if the selection is empty
501         if (empty()) break;
503         switch(shortcut_key(event->key)) {
504         // moves
505         case GDK_Up:
506         case GDK_KP_Up:
507         case GDK_KP_8:
508             return _keyboardMove(event->key, Geom::Point(0, 1));
509         case GDK_Down:
510         case GDK_KP_Down:
511         case GDK_KP_2:
512             return _keyboardMove(event->key, Geom::Point(0, -1));
513         case GDK_Right:
514         case GDK_KP_Right:
515         case GDK_KP_6:
516             return _keyboardMove(event->key, Geom::Point(1, 0));
517         case GDK_Left:
518         case GDK_KP_Left:
519         case GDK_KP_4:
520             return _keyboardMove(event->key, Geom::Point(-1, 0));
522         // rotates
523         case GDK_bracketleft:
524             return _keyboardRotate(event->key, 1);
525         case GDK_bracketright:
526             return _keyboardRotate(event->key, -1);
528         // scaling
529         case GDK_less:
530         case GDK_comma:
531             return _keyboardScale(event->key, -1);
532         case GDK_greater:
533         case GDK_period:
534             return _keyboardScale(event->key, 1);
536         // TODO: skewing
538         // flipping
539         // NOTE: H is horizontal flip, while Shift+H switches transform handle mode!
540         case GDK_h:
541         case GDK_H:
542             if (held_shift(event->key)) {
543                 toggleTransformHandlesMode();
544                 return true;
545             }
546             // any modifiers except shift should cause no action
547             if (held_any_modifiers(event->key)) break;
548             return _keyboardFlip(Geom::X);
549         case GDK_v:
550         case GDK_V:
551             if (held_any_modifiers(event->key)) break;
552             return _keyboardFlip(Geom::Y);
553         default: break;
554         }
555         break;
556     default: break;
557     }
558     return false;
561 } // namespace UI
562 } // namespace Inkscape
564 /*
565   Local Variables:
566   mode:c++
567   c-file-style:"stroustrup"
568   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
569   indent-tabs-mode:nil
570   fill-column:99
571   End:
572 */
573 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :