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 found = _points.insert(x).first;
83 x->updateState();
84 _rot_radius.reset();
85 signal_point_changed.emit(x, true);
87 return std::pair<iterator, bool>(found, true);
88 }
90 /** Remove a point from the selection. */
91 void ControlPointSelection::erase(iterator pos)
92 {
93 SelectableControlPoint *erased = *pos;
94 _points.erase(pos);
95 erased->updateState();
96 _rot_radius.reset();
97 signal_point_changed.emit(erased, false);
98 }
99 ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k)
100 {
101 iterator pos = _points.find(k);
102 if (pos == _points.end()) return 0;
103 erase(pos);
104 return 1;
105 }
106 void ControlPointSelection::erase(iterator first, iterator last)
107 {
108 while (first != last) erase(first++);
109 }
111 /** Remove all points from the selection, making it empty. */
112 void ControlPointSelection::clear()
113 {
114 for (iterator i = begin(); i != end(); )
115 erase(i++);
116 }
118 /** Select all points that this selection can contain. */
119 void ControlPointSelection::selectAll()
120 {
121 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
122 insert(*i);
123 }
124 }
125 /** Select all points inside the given rectangle (in desktop coordinates). */
126 void ControlPointSelection::selectArea(Geom::Rect const &r)
127 {
128 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
129 if (r.contains(**i))
130 insert(*i);
131 }
132 }
133 /** Unselect all selected points and select all unselected points. */
134 void ControlPointSelection::invertSelection()
135 {
136 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
137 if ((*i)->selected()) erase(*i);
138 else insert(*i);
139 }
140 }
141 void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir)
142 {
143 bool grow = (dir > 0);
144 Geom::Point p = origin->position();
145 double best_dist = grow ? HUGE_VAL : 0;
146 SelectableControlPoint *match = NULL;
147 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
148 bool selected = (*i)->selected();
149 if (grow && !selected) {
150 double dist = Geom::distance((*i)->position(), p);
151 if (dist < best_dist) {
152 best_dist = dist;
153 match = *i;
154 }
155 }
156 if (!grow && selected) {
157 double dist = Geom::distance((*i)->position(), p);
158 // use >= to also deselect the origin node when it's the last one selected
159 if (dist >= best_dist) {
160 best_dist = dist;
161 match = *i;
162 }
163 }
164 }
165 if (match) {
166 if (grow) insert(match);
167 else erase(match);
168 }
169 }
171 /** Transform all selected control points by the given affine transformation. */
172 void ControlPointSelection::transform(Geom::Matrix const &m)
173 {
174 for (iterator i = _points.begin(); i != _points.end(); ++i) {
175 SelectableControlPoint *cur = *i;
176 cur->transform(m);
177 }
178 // TODO preserving the rotation radius needs some rethinking...
179 if (_rot_radius) (*_rot_radius) *= m.descrim();
180 signal_update.emit();
181 }
183 /** Align control points on the specified axis. */
184 void ControlPointSelection::align(Geom::Dim2 axis)
185 {
186 if (empty()) return;
187 Geom::Dim2 d = static_cast<Geom::Dim2>((axis + 1) % 2);
189 Geom::OptInterval bound;
190 for (iterator i = _points.begin(); i != _points.end(); ++i) {
191 bound.unionWith(Geom::OptInterval((*i)->position()[d]));
192 }
194 double new_coord = bound->middle();
195 for (iterator i = _points.begin(); i != _points.end(); ++i) {
196 Geom::Point pos = (*i)->position();
197 pos[d] = new_coord;
198 (*i)->move(pos);
199 }
200 }
202 /** Equdistantly distribute control points by moving them in the specified dimension. */
203 void ControlPointSelection::distribute(Geom::Dim2 d)
204 {
205 if (empty()) return;
207 // this needs to be a multimap, otherwise it will fail when some points have the same coord
208 typedef std::multimap<double, SelectableControlPoint*> SortMap;
210 SortMap sm;
211 Geom::OptInterval bound;
212 // first we insert all points into a multimap keyed by the aligned coord to sort them
213 // simultaneously we compute the extent of selection
214 for (iterator i = _points.begin(); i != _points.end(); ++i) {
215 Geom::Point pos = (*i)->position();
216 sm.insert(std::make_pair(pos[d], (*i)));
217 bound.unionWith(Geom::OptInterval(pos[d]));
218 }
220 // now we iterate over the multimap and set aligned positions.
221 double step = size() == 1 ? 0 : bound->extent() / (size() - 1);
222 double start = bound->min();
223 unsigned num = 0;
224 for (SortMap::iterator i = sm.begin(); i != sm.end(); ++i, ++num) {
225 Geom::Point pos = i->second->position();
226 pos[d] = start + num * step;
227 i->second->move(pos);
228 }
229 }
231 /** Get the bounds of the selection.
232 * @return Smallest rectangle containing the positions of all selected points,
233 * or nothing if the selection is empty */
234 Geom::OptRect ControlPointSelection::pointwiseBounds()
235 {
236 Geom::OptRect bound;
237 for (iterator i = _points.begin(); i != _points.end(); ++i) {
238 SelectableControlPoint *cur = (*i);
239 Geom::Point p = cur->position();
240 if (!bound) {
241 bound = Geom::Rect(p, p);
242 } else {
243 bound->expandTo(p);
244 }
245 }
246 return bound;
247 }
249 Geom::OptRect ControlPointSelection::bounds()
250 {
251 Geom::OptRect bound;
252 for (iterator i = _points.begin(); i != _points.end(); ++i) {
253 SelectableControlPoint *cur = (*i);
254 Geom::OptRect r = cur->bounds();
255 bound.unionWith(r);
256 }
257 return bound;
258 }
260 void ControlPointSelection::showTransformHandles(bool v, bool one_node)
261 {
262 _one_node_handles = one_node;
263 _handles_visible = v;
264 _updateTransformHandles(false);
265 }
267 void ControlPointSelection::hideTransformHandles()
268 {
269 _handles->setVisible(false);
270 }
271 void ControlPointSelection::restoreTransformHandles()
272 {
273 _updateTransformHandles(true);
274 }
276 void ControlPointSelection::toggleTransformHandlesMode()
277 {
278 if (_handles->mode() == TransformHandleSet::MODE_SCALE) {
279 _handles->setMode(TransformHandleSet::MODE_ROTATE_SKEW);
280 if (size() == 1) _handles->rotationCenter().setVisible(false);
281 } else {
282 _handles->setMode(TransformHandleSet::MODE_SCALE);
283 }
284 }
286 void ControlPointSelection::_pointGrabbed()
287 {
288 hideTransformHandles();
289 _dragging = true;
290 }
292 void ControlPointSelection::_pointDragged(Geom::Point const &old_pos, Geom::Point &new_pos,
293 GdkEventMotion */*event*/)
294 {
295 Geom::Point delta = new_pos - old_pos;
296 for (iterator i = _points.begin(); i != _points.end(); ++i) {
297 SelectableControlPoint *cur = (*i);
298 cur->move(cur->position() + delta);
299 }
300 _handles->rotationCenter().move(_handles->rotationCenter().position() + delta);
301 signal_update.emit();
302 }
304 void ControlPointSelection::_pointUngrabbed()
305 {
306 _dragging = false;
307 _grabbed_point = NULL;
308 restoreTransformHandles();
309 signal_commit.emit(COMMIT_MOUSE_MOVE);
310 }
312 bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventButton *event)
313 {
314 // clicking a selected node should toggle the transform handles between rotate and scale mode,
315 // if they are visible
316 if (held_no_modifiers(*event) && _handles_visible && p->selected()) {
317 toggleTransformHandlesMode();
318 return true;
319 }
320 return false;
321 }
323 void ControlPointSelection::_updateTransformHandles(bool preserve_center)
324 {
325 if (_dragging) return;
327 if (_handles_visible && size() > 1) {
328 Geom::OptRect b = pointwiseBounds();
329 _handles->setBounds(*b, preserve_center);
330 _handles->setVisible(true);
331 } else if (_one_node_handles && size() == 1) { // only one control point in selection
332 SelectableControlPoint *p = *begin();
333 _handles->setBounds(p->bounds());
334 _handles->rotationCenter().move(p->position());
335 _handles->rotationCenter().setVisible(false);
336 _handles->setVisible(true);
337 } else {
338 _handles->setVisible(false);
339 }
340 }
342 /** Moves the selected points along the supplied unit vector according to
343 * the modifier state of the supplied event. */
344 bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir)
345 {
346 if (held_control(event)) return false;
347 unsigned num = 1 + combine_key_events(shortcut_key(event), 0);
349 Geom::Point delta = dir * num;
350 if (held_shift(event)) delta *= 10;
351 if (held_alt(event)) {
352 delta /= _desktop->current_zoom();
353 } else {
354 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
355 double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000);
356 delta *= nudge;
357 }
359 transform(Geom::Translate(delta));
360 if (fabs(dir[Geom::X]) > 0) {
361 signal_commit.emit(COMMIT_KEYBOARD_MOVE_X);
362 } else {
363 signal_commit.emit(COMMIT_KEYBOARD_MOVE_Y);
364 }
365 return true;
366 }
368 /** Rotates the selected points in the given direction according to the modifier state
369 * from the supplied event.
370 * @param event Key event to take modifier state from
371 * @param dir Direction of rotation (math convention: 1 = counterclockwise, -1 = clockwise)
372 */
373 bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
374 {
375 if (empty()) return false;
377 Geom::Point rc = _handles->rotationCenter();
378 if (!_rot_radius) {
379 Geom::Rect b = *(size() == 1 ? bounds() : pointwiseBounds());
380 double maxlen = 0;
381 for (unsigned i = 0; i < 4; ++i) {
382 double len = (b.corner(i) - rc).length();
383 if (len > maxlen) maxlen = len;
384 }
385 _rot_radius = maxlen;
386 }
388 double angle;
389 if (held_alt(event)) {
390 // Rotate by "one pixel". We interpret this as rotating by an angle that causes
391 // the topmost point of a circle circumscribed about the selection's bounding box
392 // to move on an arc 1 screen pixel long.
393 angle = atan2(1.0 / _desktop->current_zoom(), *_rot_radius) * dir;
394 } else {
395 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
396 int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
397 angle = M_PI * dir / snaps;
398 }
400 // translate to origin, rotate, translate back to original position
401 Geom::Matrix m = Geom::Translate(-rc)
402 * Geom::Rotate(angle) * Geom::Translate(rc);
403 transform(m);
404 signal_commit.emit(COMMIT_KEYBOARD_ROTATE);
405 return true;
406 }
409 bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir)
410 {
411 if (empty()) return false;
413 // TODO should the saved rotation center or the current center be used?
414 Geom::Rect bound = (size() == 1 ? *bounds() : *pointwiseBounds());
415 double maxext = bound.maxExtent();
416 if (Geom::are_near(maxext, 0)) return false;
417 Geom::Point center = _handles->rotationCenter().position();
419 double length_change;
420 if (held_alt(event)) {
421 // Scale by "one pixel". It means shrink/grow 1px for the larger dimension
422 // of the bounding box.
423 length_change = 1.0 / _desktop->current_zoom() * dir;
424 } else {
425 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
426 length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000);
427 length_change *= dir;
428 }
429 double scale = (maxext + length_change) / maxext;
431 Geom::Matrix m = Geom::Translate(-center) * Geom::Scale(scale) * Geom::Translate(center);
432 transform(m);
433 signal_commit.emit(COMMIT_KEYBOARD_SCALE_UNIFORM);
434 return true;
435 }
437 bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d)
438 {
439 if (empty()) return false;
441 Geom::Scale scale_transform(1, 1);
442 if (d == Geom::X) {
443 scale_transform = Geom::Scale(-1, 1);
444 } else {
445 scale_transform = Geom::Scale(1, -1);
446 }
448 SelectableControlPoint *scp =
449 dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
450 Geom::Point center = scp ? scp->position() : _handles->rotationCenter().position();
452 Geom::Matrix m = Geom::Translate(-center) * scale_transform * Geom::Translate(center);
453 transform(m);
454 signal_commit.emit(d == Geom::X ? COMMIT_FLIP_X : COMMIT_FLIP_Y);
455 return true;
456 }
458 void ControlPointSelection::_commitTransform(CommitEvent ce)
459 {
460 _updateTransformHandles(true);
461 signal_commit.emit(ce);
462 }
464 bool ControlPointSelection::event(GdkEvent *event)
465 {
466 // implement generic event handling that should apply for all control point selections here;
467 // for example, keyboard moves and transformations. This way this functionality doesn't need
468 // to be duplicated in many places
469 // Later split out so that it can be reused in object selection
471 switch (event->type) {
472 case GDK_KEY_PRESS:
473 // do not handle key events if the selection is empty
474 if (empty()) break;
476 switch(shortcut_key(event->key)) {
477 // moves
478 case GDK_Up:
479 case GDK_KP_Up:
480 case GDK_KP_8:
481 return _keyboardMove(event->key, Geom::Point(0, 1));
482 case GDK_Down:
483 case GDK_KP_Down:
484 case GDK_KP_2:
485 return _keyboardMove(event->key, Geom::Point(0, -1));
486 case GDK_Right:
487 case GDK_KP_Right:
488 case GDK_KP_6:
489 return _keyboardMove(event->key, Geom::Point(1, 0));
490 case GDK_Left:
491 case GDK_KP_Left:
492 case GDK_KP_4:
493 return _keyboardMove(event->key, Geom::Point(-1, 0));
495 // rotates
496 case GDK_bracketleft:
497 return _keyboardRotate(event->key, 1);
498 case GDK_bracketright:
499 return _keyboardRotate(event->key, -1);
501 // scaling
502 case GDK_less:
503 case GDK_comma:
504 return _keyboardScale(event->key, -1);
505 case GDK_greater:
506 case GDK_period:
507 return _keyboardScale(event->key, 1);
509 // TODO: skewing
511 // flipping
512 // NOTE: H is horizontal flip, while Shift+H switches transform handle mode!
513 case GDK_h:
514 case GDK_H:
515 if (held_shift(event->key)) {
516 toggleTransformHandlesMode();
517 return true;
518 }
519 // any modifiers except shift should cause no action
520 if (held_any_modifiers(event->key)) break;
521 return _keyboardFlip(Geom::X);
522 case GDK_v:
523 case GDK_V:
524 if (held_any_modifiers(event->key)) break;
525 return _keyboardFlip(Geom::Y);
526 default: break;
527 }
528 break;
529 default: break;
530 }
531 return false;
532 }
534 } // namespace UI
535 } // namespace Inkscape
537 /*
538 Local Variables:
539 mode:c++
540 c-file-style:"stroustrup"
541 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
542 indent-tabs-mode:nil
543 fill-column:99
544 End:
545 */
546 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :