5a84592b69113dbf0a47ea055fa206de877996dc
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 , _sculpt_enabled(false)
53 , _sculpting(false)
54 {
55 signal_update.connect( sigc::bind(
56 sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
57 true));
58 signal_point_changed.connect(
59 sigc::hide( sigc::hide(
60 sigc::bind(
61 sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
62 false))));
63 _handles->signal_transform.connect(
64 sigc::mem_fun(*this, &ControlPointSelection::transform));
65 _handles->signal_commit.connect(
66 sigc::mem_fun(*this, &ControlPointSelection::_commitTransform));
67 }
69 ControlPointSelection::~ControlPointSelection()
70 {
71 clear();
72 delete _handles;
73 }
75 /** Add a control point to the selection. */
76 std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(const value_type &x)
77 {
78 iterator found = _points.find(x);
79 if (found != _points.end()) {
80 return std::pair<iterator, bool>(found, false);
81 }
83 boost::shared_ptr<connlist_type> clist(new connlist_type());
85 // hide event param and always return false
86 clist->push_back(
87 x->signal_grabbed.connect(
88 sigc::bind_return(
89 sigc::bind<0>(
90 sigc::mem_fun(*this, &ControlPointSelection::_selectionGrabbed),
91 x),
92 false)));
93 clist->push_back(
94 x->signal_dragged.connect(
95 sigc::mem_fun(*this, &ControlPointSelection::_selectionDragged)));
96 // hide event parameter
97 clist->push_back(
98 x->signal_ungrabbed.connect(
99 sigc::hide(
100 sigc::mem_fun(*this, &ControlPointSelection::_selectionUngrabbed))));
102 found = _points.insert(std::make_pair(x, clist)).first;
104 x->updateState();
105 _rot_radius.reset();
106 signal_point_changed.emit(x, true);
108 return std::pair<iterator, bool>(found, true);
109 }
111 /** Remove a point from the selection. */
112 void ControlPointSelection::erase(iterator pos)
113 {
114 SelectableControlPoint *erased = pos->first;
115 boost::shared_ptr<connlist_type> clist = pos->second;
116 for (connlist_type::iterator i = clist->begin(); i != clist->end(); ++i) {
117 i->disconnect();
118 }
119 _points.erase(pos);
120 erased->updateState();
121 _rot_radius.reset();
122 signal_point_changed.emit(erased, false);
123 }
124 ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k)
125 {
126 iterator pos = _points.find(k);
127 if (pos == _points.end()) return 0;
128 erase(pos);
129 return 1;
130 }
131 void ControlPointSelection::erase(iterator first, iterator last)
132 {
133 while (first != last) erase(first++);
134 }
136 /** Remove all points from the selection, making it empty. */
137 void ControlPointSelection::clear()
138 {
139 for (iterator i = begin(); i != end(); )
140 erase(i++);
141 }
143 /** Select all points that this selection can contain. */
144 void ControlPointSelection::selectAll()
145 {
146 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
147 insert(*i);
148 }
149 }
150 /** Select all points inside the given rectangle (in desktop coordinates). */
151 void ControlPointSelection::selectArea(Geom::Rect const &r)
152 {
153 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
154 if (r.contains(**i))
155 insert(*i);
156 }
157 }
158 /** Unselect all selected points and select all unselected points. */
159 void ControlPointSelection::invertSelection()
160 {
161 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
162 if ((*i)->selected()) erase(*i);
163 else insert(*i);
164 }
165 }
166 void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir)
167 {
168 bool grow = (dir > 0);
169 Geom::Point p = origin->position();
170 double best_dist = grow ? HUGE_VAL : 0;
171 SelectableControlPoint *match = NULL;
172 for (set_type::iterator i = _all_points.begin(); i != _all_points.end(); ++i) {
173 bool selected = (*i)->selected();
174 if (grow && !selected) {
175 double dist = Geom::distance((*i)->position(), p);
176 if (dist < best_dist) {
177 best_dist = dist;
178 match = *i;
179 }
180 }
181 if (!grow && selected) {
182 double dist = Geom::distance((*i)->position(), p);
183 // use >= to also deselect the origin node when it's the last one selected
184 if (dist >= best_dist) {
185 best_dist = dist;
186 match = *i;
187 }
188 }
189 }
190 if (match) {
191 if (grow) insert(match);
192 else erase(match);
193 }
194 }
196 /** Transform all selected control points by the given affine transformation. */
197 void ControlPointSelection::transform(Geom::Matrix const &m)
198 {
199 for (iterator i = _points.begin(); i != _points.end(); ++i) {
200 SelectableControlPoint *cur = i->first;
201 cur->transform(m);
202 }
203 // TODO preserving the rotation radius needs some rethinking...
204 if (_rot_radius) (*_rot_radius) *= m.descrim();
205 signal_update.emit();
206 }
208 /** Align control points on the specified axis. */
209 void ControlPointSelection::align(Geom::Dim2 axis)
210 {
211 if (empty()) return;
212 Geom::Dim2 d = static_cast<Geom::Dim2>((axis + 1) % 2);
214 Geom::OptInterval bound;
215 for (iterator i = _points.begin(); i != _points.end(); ++i) {
216 bound.unionWith(Geom::OptInterval(i->first->position()[d]));
217 }
219 double new_coord = bound->middle();
220 for (iterator i = _points.begin(); i != _points.end(); ++i) {
221 Geom::Point pos = i->first->position();
222 pos[d] = new_coord;
223 i->first->move(pos);
224 }
225 }
227 /** Equdistantly distribute control points by moving them in the specified dimension. */
228 void ControlPointSelection::distribute(Geom::Dim2 d)
229 {
230 if (empty()) return;
232 // this needs to be a multimap, otherwise it will fail when some points have the same coord
233 typedef std::multimap<double, SelectableControlPoint*> SortMap;
235 SortMap sm;
236 Geom::OptInterval bound;
237 // first we insert all points into a multimap keyed by the aligned coord to sort them
238 // simultaneously we compute the extent of selection
239 for (iterator i = _points.begin(); i != _points.end(); ++i) {
240 Geom::Point pos = i->first->position();
241 sm.insert(std::make_pair(pos[d], i->first));
242 bound.unionWith(Geom::OptInterval(pos[d]));
243 }
245 // now we iterate over the multimap and set aligned positions.
246 double step = size() == 1 ? 0 : bound->extent() / (size() - 1);
247 double start = bound->min();
248 unsigned num = 0;
249 for (SortMap::iterator i = sm.begin(); i != sm.end(); ++i, ++num) {
250 Geom::Point pos = i->second->position();
251 pos[d] = start + num * step;
252 i->second->move(pos);
253 }
254 }
256 /** Get the bounds of the selection.
257 * @return Smallest rectangle containing the positions of all selected points,
258 * or nothing if the selection is empty */
259 Geom::OptRect ControlPointSelection::pointwiseBounds()
260 {
261 Geom::OptRect bound;
262 for (iterator i = _points.begin(); i != _points.end(); ++i) {
263 SelectableControlPoint *cur = i->first;
264 Geom::Point p = cur->position();
265 if (!bound) {
266 bound = Geom::Rect(p, p);
267 } else {
268 bound->expandTo(p);
269 }
270 }
271 return bound;
272 }
274 Geom::OptRect ControlPointSelection::bounds()
275 {
276 Geom::OptRect bound;
277 for (iterator i = _points.begin(); i != _points.end(); ++i) {
278 SelectableControlPoint *cur = i->first;
279 Geom::OptRect r = cur->bounds();
280 bound.unionWith(r);
281 }
282 return bound;
283 }
285 void ControlPointSelection::showTransformHandles(bool v, bool one_node)
286 {
287 _one_node_handles = one_node;
288 _handles_visible = v;
289 _updateTransformHandles(false);
290 }
292 void ControlPointSelection::hideTransformHandles()
293 {
294 _handles->setVisible(false);
295 }
296 void ControlPointSelection::restoreTransformHandles()
297 {
298 _updateTransformHandles(true);
299 }
301 void ControlPointSelection::_selectionGrabbed(SelectableControlPoint *p, GdkEventMotion *event)
302 {
303 hideTransformHandles();
304 _dragging = true;
305 if (held_alt(*event) && _sculpt_enabled) {
306 _sculpting = true;
307 _grabbed_point = p;
308 } else {
309 _sculpting = false;
310 }
311 }
313 void ControlPointSelection::_selectionDragged(Geom::Point const &old_pos, Geom::Point &new_pos,
314 GdkEventMotion *event)
315 {
316 Geom::Point delta = new_pos - old_pos;
317 /*if (_sculpting) {
318 // for now we only support the default sculpting profile (bell)
319 // others will be added when preferences will be able to store enumerated values
320 double pressure, alpha;
321 if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &pressure)) {
322 pressure = CLAMP(pressure, 0.2, 0.8);
323 } else {
324 pressure = 0.5;
325 }
327 alpha = 1 - 2 * fabs(pressure - 0.5);
328 if (pressure > 0.5) alpha = 1/alpha;
330 for (iterator i = _points.begin(); i != _points.end(); ++i) {
331 SelectableControlPoint *cur = i->first;
332 double dist = Geom::distance(cur->position(), _grabbed_point->position());
334 cur->move(cur->position() + delta);
335 }
336 } else*/ {
337 for (iterator i = _points.begin(); i != _points.end(); ++i) {
338 SelectableControlPoint *cur = i->first;
339 cur->move(cur->position() + delta);
340 }
341 _handles->rotationCenter().move(_handles->rotationCenter().position() + delta);
342 }
343 signal_update.emit();
344 }
346 void ControlPointSelection::_selectionUngrabbed()
347 {
348 _dragging = false;
349 _grabbed_point = NULL;
350 restoreTransformHandles();
351 signal_commit.emit(COMMIT_MOUSE_MOVE);
352 }
354 void ControlPointSelection::_updateTransformHandles(bool preserve_center)
355 {
356 if (_dragging) return;
358 if (_handles_visible && size() > 1) {
359 Geom::OptRect b = pointwiseBounds();
360 _handles->setBounds(*b, preserve_center);
361 _handles->setVisible(true);
362 } else if (_one_node_handles && size() == 1) { // only one control point in selection
363 SelectableControlPoint *p = begin()->first;
364 _handles->setBounds(p->bounds());
365 _handles->rotationCenter().move(p->position());
366 _handles->rotationCenter().setVisible(false);
367 _handles->setVisible(true);
368 } else {
369 _handles->setVisible(false);
370 }
371 }
373 /** Moves the selected points along the supplied unit vector according to
374 * the modifier state of the supplied event. */
375 bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir)
376 {
377 if (held_control(event)) return false;
378 unsigned num = 1 + consume_same_key_events(shortcut_key(event), 0);
380 Geom::Point delta = dir * num;
381 if (held_shift(event)) delta *= 10;
382 if (held_alt(event)) {
383 delta /= _desktop->current_zoom();
384 } else {
385 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
386 double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000);
387 delta *= nudge;
388 }
390 transform(Geom::Translate(delta));
391 if (fabs(dir[Geom::X]) > 0) {
392 signal_commit.emit(COMMIT_KEYBOARD_MOVE_X);
393 } else {
394 signal_commit.emit(COMMIT_KEYBOARD_MOVE_Y);
395 }
396 return true;
397 }
399 /** Rotates the selected points in the given direction according to the modifier state
400 * from the supplied event.
401 * @param event Key event to take modifier state from
402 * @param dir Direction of rotation (math convention: 1 = counterclockwise, -1 = clockwise)
403 */
404 bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
405 {
406 if (empty()) return false;
408 Geom::Point rc = _handles->rotationCenter();
409 if (!_rot_radius) {
410 Geom::Rect b = *(size() == 1 ? bounds() : pointwiseBounds());
411 double maxlen = 0;
412 for (unsigned i = 0; i < 4; ++i) {
413 double len = (b.corner(i) - rc).length();
414 if (len > maxlen) maxlen = len;
415 }
416 _rot_radius = maxlen;
417 }
419 double angle;
420 if (held_alt(event)) {
421 // Rotate by "one pixel". We interpret this as rotating by an angle that causes
422 // the topmost point of a circle circumscribed about the selection's bounding box
423 // to move on an arc 1 screen pixel long.
424 angle = atan2(1.0 / _desktop->current_zoom(), *_rot_radius) * dir;
425 } else {
426 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
427 int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
428 angle = M_PI * dir / snaps;
429 }
431 // translate to origin, rotate, translate back to original position
432 Geom::Matrix m = Geom::Translate(-rc)
433 * Geom::Rotate(angle) * Geom::Translate(rc);
434 transform(m);
435 signal_commit.emit(COMMIT_KEYBOARD_ROTATE);
436 return true;
437 }
440 bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir)
441 {
442 if (empty()) return false;
444 // TODO should the saved rotation center or the current center be used?
445 Geom::Rect bound = (size() == 1 ? *bounds() : *pointwiseBounds());
446 double maxext = bound.maxExtent();
447 if (Geom::are_near(maxext, 0)) return false;
448 Geom::Point center = _handles->rotationCenter().position();
450 double length_change;
451 if (held_alt(event)) {
452 // Scale by "one pixel". It means shrink/grow 1px for the larger dimension
453 // of the bounding box.
454 length_change = 1.0 / _desktop->current_zoom() * dir;
455 } else {
456 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
457 length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000);
458 length_change *= dir;
459 }
460 double scale = (maxext + length_change) / maxext;
462 Geom::Matrix m = Geom::Translate(-center) * Geom::Scale(scale) * Geom::Translate(center);
463 transform(m);
464 signal_commit.emit(COMMIT_KEYBOARD_SCALE_UNIFORM);
465 return true;
466 }
468 bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d)
469 {
470 if (empty()) return false;
472 Geom::Scale scale_transform(1, 1);
473 if (d == Geom::X) {
474 scale_transform = Geom::Scale(-1, 1);
475 } else {
476 scale_transform = Geom::Scale(1, -1);
477 }
479 SelectableControlPoint *scp =
480 dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
481 Geom::Point center = scp ? scp->position() : _handles->rotationCenter().position();
483 Geom::Matrix m = Geom::Translate(-center) * scale_transform * Geom::Translate(center);
484 transform(m);
485 signal_commit.emit(d == Geom::X ? COMMIT_FLIP_X : COMMIT_FLIP_Y);
486 return true;
487 }
489 void ControlPointSelection::_commitTransform(CommitEvent ce)
490 {
491 _updateTransformHandles(true);
492 signal_commit.emit(ce);
493 }
495 bool ControlPointSelection::event(GdkEvent *event)
496 {
497 // implement generic event handling that should apply for all control point selections here;
498 // for example, keyboard moves and transformations. This way this functionality doesn't need
499 // to be duplicated in many places
500 // Later split out so that it can be reused in object selection
502 switch (event->type) {
503 case GDK_KEY_PRESS:
504 // do not handle key events if the selection is empty
505 if (empty()) break;
507 switch(shortcut_key(event->key)) {
508 // moves
509 case GDK_Up:
510 case GDK_KP_Up:
511 case GDK_KP_8:
512 return _keyboardMove(event->key, Geom::Point(0, 1));
513 case GDK_Down:
514 case GDK_KP_Down:
515 case GDK_KP_2:
516 return _keyboardMove(event->key, Geom::Point(0, -1));
517 case GDK_Right:
518 case GDK_KP_Right:
519 case GDK_KP_6:
520 return _keyboardMove(event->key, Geom::Point(1, 0));
521 case GDK_Left:
522 case GDK_KP_Left:
523 case GDK_KP_4:
524 return _keyboardMove(event->key, Geom::Point(-1, 0));
526 // rotates
527 case GDK_bracketleft:
528 return _keyboardRotate(event->key, 1);
529 case GDK_bracketright:
530 return _keyboardRotate(event->key, -1);
532 // scaling
533 case GDK_less:
534 case GDK_comma:
535 return _keyboardScale(event->key, -1);
536 case GDK_greater:
537 case GDK_period:
538 return _keyboardScale(event->key, 1);
540 // TODO: skewing
542 // flipping
543 // NOTE: H is horizontal flip, while Shift+H switches transform handle mode!
544 case GDK_h:
545 case GDK_H:
546 if (held_shift(event->key)) {
547 // TODO make a method for mode switching
548 if (_handles->mode() == TransformHandleSet::MODE_SCALE) {
549 _handles->setMode(TransformHandleSet::MODE_ROTATE_SKEW);
550 if (size() == 1) _handles->rotationCenter().setVisible(false);
551 } else {
552 _handles->setMode(TransformHandleSet::MODE_SCALE);
553 }
554 return true;
555 }
556 // any modifiers except shift should cause no action
557 if (held_any_modifiers(event->key)) break;
558 return _keyboardFlip(Geom::X);
559 case GDK_v:
560 case GDK_V:
561 if (held_any_modifiers(event->key)) break;
562 return _keyboardFlip(Geom::Y);
563 default: break;
564 }
565 break;
566 default: break;
567 }
568 return false;
569 }
571 } // namespace UI
572 } // namespace Inkscape
574 /*
575 Local Variables:
576 mode:c++
577 c-file-style:"stroustrup"
578 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
579 indent-tabs-mode:nil
580 fill-column:99
581 End:
582 */
583 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :