1 /** @file
2 * Desktop-bound visual control object - 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 <iostream>
12 #include <gdkmm.h>
13 #include <gtkmm.h>
14 #include <2geom/point.h>
15 #include "desktop.h"
16 #include "desktop-handles.h"
17 #include "display/sp-canvas.h"
18 #include "display/snap-indicator.h"
19 #include "event-context.h"
20 #include "message-context.h"
21 #include "preferences.h"
22 #include "ui/tool/control-point.h"
23 #include "ui/tool/event-utils.h"
25 namespace Inkscape {
26 namespace UI {
28 // class and member documentation goes here...
30 /**
31 * @class ControlPoint
32 * @brief Draggable point, the workhorse of on-canvas editing.
33 *
34 * Control points (formerly known as knots) are graphical representations of some significant
35 * point in the drawing. The drawing can be changed by dragging the point and the things that are
36 * attached to it with the mouse. Example things that could be edited with draggable points
37 * are gradient stops, the place where text is attached to a path, text kerns, nodes and handles
38 * in a path, and many more.
39 *
40 * @par Control point event handlers
41 * @par
42 * The control point has several virtual methods which allow you to react to things that
43 * happen to it. The most important ones are the grabbed, dragged, ungrabbed and moved functions.
44 * When a drag happens, the order of calls is as follows:
45 * - <tt>grabbed()</tt>
46 * - <tt>dragged()</tt>
47 * - <tt>dragged()</tt>
48 * - <tt>dragged()</tt>
49 * - ...
50 * - <tt>dragged()</tt>
51 * - <tt>ungrabbed()</tt>
52 *
53 * The control point can also respond to clicks and double clicks. On a double click,
54 * clicked() is called, followed by doubleclicked(). When deriving from SelectableControlPoint,
55 * you need to manually call the superclass version at the appropriate point in your handler.
56 *
57 * @par Which method to override?
58 * @par
59 * You might wonder which hook to use when you want to do things when the point is relocated.
60 * Here are some tips:
61 * - If the point is used to edit an object, override the move() method.
62 * - If the point can usually be dragged wherever you like but can optionally be constrained
63 * to axes or the like, add a handler for <tt>signal_dragged</tt> that modifies its new
64 * position argument.
65 * - If the point has additional canvas items tied to it (like handle lines), override
66 * the setPosition() method.
67 */
69 /**
70 * @enum ControlPoint::State
71 * Enumeration representing the possible states of the control point, used to determine
72 * its appearance.
73 * @var ControlPoint::STATE_NORMAL
74 * Normal state
75 * @var ControlPoint::STATE_MOUSEOVER
76 * Mouse is hovering over the control point
77 * @var ControlPoint::STATE_CLICKED
78 * First mouse button pressed over the control point
79 */
81 // Default colors for control points
82 static ControlPoint::ColorSet default_color_set = {
83 {0xffffff00, 0x01000000}, // normal fill, stroke
84 {0xff0000ff, 0x01000000}, // mouseover fill, stroke
85 {0x0000ffff, 0x01000000} // clicked fill, stroke
86 };
88 /** Holds the currently mouseovered control point. */
89 ControlPoint *ControlPoint::mouseovered_point = 0;
91 /** Emitted when the mouseovered point changes. The parameter is the new mouseovered point.
92 * When a point ceases to be mouseovered, the parameter will be NULL. */
93 sigc::signal<void, ControlPoint*> ControlPoint::signal_mouseover_change;
95 /** Stores the window point over which the cursor was during the last mouse button press */
96 Geom::Point ControlPoint::_drag_event_origin(Geom::infinity(), Geom::infinity());
98 /** Stores the desktop point from which the last drag was initiated */
99 Geom::Point ControlPoint::_drag_origin(Geom::infinity(), Geom::infinity());
101 /** Events which should be captured when a handle is being dragged. */
102 int const ControlPoint::_grab_event_mask = (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
103 GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_KEY_PRESS_MASK |
104 GDK_KEY_RELEASE_MASK);
106 bool ControlPoint::_drag_initiated = false;
107 bool ControlPoint::_event_grab = false;
109 /** A color set which you can use to create an invisible control that can still receive events.
110 * @relates ControlPoint */
111 ControlPoint::ColorSet invisible_cset = {
112 {0x00000000, 0x00000000},
113 {0x00000000, 0x00000000},
114 {0x00000000, 0x00000000}
115 };
117 /**
118 * Create a regular control point.
119 * Derive to have constructors with a reasonable number of parameters.
120 *
121 * @param d Desktop for this control
122 * @param initial_pos Initial position of the control point in desktop coordinates
123 * @param anchor Where is the control point rendered relative to its desktop coordinates
124 * @param shape Shape of the control point: square, diamond, circle...
125 * @param size Pixel size of the visual representation
126 * @param cset Colors of the point
127 * @param group The canvas group the point's canvas item should be created in
128 */
129 ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos,
130 Gtk::AnchorType anchor, SPCtrlShapeType shape,
131 unsigned int size, ColorSet *cset, SPCanvasGroup *group)
132 : _desktop (d)
133 , _canvas_item (NULL)
134 , _cset (cset ? cset : &default_color_set)
135 , _state (STATE_NORMAL)
136 , _position (initial_pos)
137 {
138 _canvas_item = sp_canvas_item_new(
139 group ? group : sp_desktop_controls (_desktop), SP_TYPE_CTRL,
140 "anchor", (GtkAnchorType) anchor, "size", (gdouble) size, "shape", shape,
141 "filled", TRUE, "fill_color", _cset->normal.fill,
142 "stroked", TRUE, "stroke_color", _cset->normal.stroke,
143 "mode", SP_CTRL_MODE_XOR, NULL);
144 _commonInit();
145 }
147 /**
148 * Create a control point with a pixbuf-based visual representation.
149 *
150 * @param d Desktop for this control
151 * @param initial_pos Initial position of the control point in desktop coordinates
152 * @param anchor Where is the control point rendered relative to its desktop coordinates
153 * @param pixbuf Pixbuf to be used as the visual representation
154 * @param cset Colors of the point
155 * @param group The canvas group the point's canvas item should be created in
156 */
157 ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos,
158 Gtk::AnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pixbuf,
159 ColorSet *cset, SPCanvasGroup *group)
160 : _desktop (d)
161 , _canvas_item (NULL)
162 , _cset(cset ? cset : &default_color_set)
163 , _position (initial_pos)
164 {
165 _canvas_item = sp_canvas_item_new(
166 group ? group : sp_desktop_controls(_desktop), SP_TYPE_CTRL,
167 "anchor", (GtkAnchorType) anchor, "size", (gdouble) pixbuf->get_width(),
168 "shape", SP_CTRL_SHAPE_BITMAP, "pixbuf", pixbuf->gobj(),
169 "filled", TRUE, "fill_color", _cset->normal.fill,
170 "stroked", TRUE, "stroke_color", _cset->normal.stroke,
171 "mode", SP_CTRL_MODE_XOR, NULL);
172 _commonInit();
173 }
175 ControlPoint::~ControlPoint()
176 {
177 // avoid storing invalid points in mouseovered_point
178 if (this == mouseovered_point) {
179 _clearMouseover();
180 }
182 g_signal_handler_disconnect(G_OBJECT(_canvas_item), _event_handler_connection);
183 //sp_canvas_item_hide(_canvas_item);
184 gtk_object_destroy(_canvas_item);
185 }
187 void ControlPoint::_commonInit()
188 {
189 SP_CTRL(_canvas_item)->moveto(_position);
190 _event_handler_connection = g_signal_connect(G_OBJECT(_canvas_item), "event",
191 G_CALLBACK(_event_handler), this);
192 }
194 /** Relocate the control point without side effects.
195 * Overload this method only if there is an additional graphical representation
196 * that must be updated (like the lines that connect handles to nodes). If you override it,
197 * you must also call the superclass implementation of the method.
198 * @todo Investigate whether this method should be protected */
199 void ControlPoint::setPosition(Geom::Point const &pos)
200 {
201 _position = pos;
202 SP_CTRL(_canvas_item)->moveto(pos);
203 }
205 /** Move the control point to new position with side effects.
206 * This is called after each drag. Override this method if only some positions make sense
207 * for a control point (like a point that must always be on a path and can't modify it),
208 * or when moving a control point changes the positions of other points. */
209 void ControlPoint::move(Geom::Point const &pos)
210 {
211 setPosition(pos);
212 }
214 /** Apply an arbitrary affine transformation to a control point. This is used
215 * by ControlPointSelection, and is important for things like nodes with handles.
216 * The default implementation simply moves the point according to the transform. */
217 void ControlPoint::transform(Geom::Matrix const &m) {
218 move(position() * m);
219 }
221 bool ControlPoint::visible() const
222 {
223 return sp_canvas_item_is_visible(_canvas_item);
224 }
226 /** Set the visibility of the control point. An invisible point is not drawn on the canvas
227 * and cannot receive any events. If you want to have an invisible point that can respond
228 * to events, use <tt>invisible_cset</tt> as its color set. */
229 void ControlPoint::setVisible(bool v)
230 {
231 if (v) sp_canvas_item_show(_canvas_item);
232 else sp_canvas_item_hide(_canvas_item);
233 }
235 Glib::ustring ControlPoint::format_tip(char const *format, ...)
236 {
237 va_list args;
238 va_start(args, format);
239 char *dyntip = g_strdup_vprintf(format, args);
240 va_end(args);
241 Glib::ustring ret = dyntip;
242 g_free(dyntip);
243 return ret;
244 }
246 unsigned int ControlPoint::_size() const
247 {
248 double ret;
249 g_object_get(_canvas_item, "size", &ret, NULL);
250 return static_cast<unsigned int>(ret);
251 }
253 SPCtrlShapeType ControlPoint::_shape() const
254 {
255 SPCtrlShapeType ret;
256 g_object_get(_canvas_item, "shape", &ret, NULL);
257 return ret;
258 }
260 GtkAnchorType ControlPoint::_anchor() const
261 {
262 GtkAnchorType ret;
263 g_object_get(_canvas_item, "anchor", &ret, NULL);
264 return ret;
265 }
267 Glib::RefPtr<Gdk::Pixbuf> ControlPoint::_pixbuf()
268 {
269 GdkPixbuf *ret;
270 g_object_get(_canvas_item, "pixbuf", &ret, NULL);
271 return Glib::wrap(ret);
272 }
274 // Same for setters.
276 void ControlPoint::_setSize(unsigned int size)
277 {
278 g_object_set(_canvas_item, "size", (gdouble) size, NULL);
279 }
281 void ControlPoint::_setShape(SPCtrlShapeType shape)
282 {
283 g_object_set(_canvas_item, "shape", shape, NULL);
284 }
286 void ControlPoint::_setAnchor(GtkAnchorType anchor)
287 {
288 g_object_set(_canvas_item, "anchor", anchor, NULL);
289 }
291 void ControlPoint::_setPixbuf(Glib::RefPtr<Gdk::Pixbuf> p)
292 {
293 g_object_set(_canvas_item, "pixbuf", Glib::unwrap(p), NULL);
294 }
296 // re-routes events into the virtual function
297 int ControlPoint::_event_handler(SPCanvasItem */*item*/, GdkEvent *event, ControlPoint *point)
298 {
299 return point->_eventHandler(event) ? TRUE : FALSE;
300 }
302 // main event callback, which emits all other callbacks.
303 bool ControlPoint::_eventHandler(GdkEvent *event)
304 {
305 // NOTE the static variables below are shared for all points!
306 // TODO handle clicks and drags from other buttons too
308 // offset from the pointer hotspot to the center of the grabbed knot in desktop coords
309 static Geom::Point pointer_offset;
310 // number of last doubleclicked button
311 static unsigned next_release_doubleclick = 0;
313 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
314 int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
316 switch(event->type)
317 {
318 case GDK_BUTTON_PRESS:
319 next_release_doubleclick = 0;
320 if (event->button.button == 1) {
321 // 1st mouse button click. internally, start dragging, but do not emit signals
322 // or change position until drag tolerance is exceeded.
323 _drag_event_origin[Geom::X] = event->button.x;
324 _drag_event_origin[Geom::Y] = event->button.y;
325 pointer_offset = _position - _desktop->w2d(_drag_event_origin);
326 _drag_initiated = false;
327 // route all events to this handler
328 sp_canvas_item_grab(_canvas_item, _grab_event_mask, NULL, event->button.time);
329 _event_grab = true;
330 _setState(STATE_CLICKED);
331 return true;
332 }
333 return _event_grab;
335 case GDK_2BUTTON_PRESS:
336 // store the button number for next release
337 next_release_doubleclick = event->button.button;
338 return true;
340 case GDK_MOTION_NOTIFY:
341 combine_motion_events(_desktop->canvas, event->motion, 0);
342 if (_event_grab && !_desktop->event_context->space_panning) {
343 _desktop->snapindicator->remove_snaptarget();
344 bool transferred = false;
345 if (!_drag_initiated) {
346 bool t = fabs(event->motion.x - _drag_event_origin[Geom::X]) <= drag_tolerance &&
347 fabs(event->motion.y - _drag_event_origin[Geom::Y]) <= drag_tolerance;
348 if (t) return true;
350 // if we are here, it means the tolerance was just exceeded.
351 _drag_origin = _position;
352 transferred = grabbed(&event->motion);
353 // _drag_initiated might change during the above virtual call
354 if (!_drag_initiated) {
355 // this guarantees smooth redraws while dragging
356 sp_canvas_force_full_redraw_after_interruptions(_desktop->canvas, 5);
357 _drag_initiated = true;
358 }
359 }
360 if (!transferred) {
361 // dragging in progress
362 Geom::Point new_pos = _desktop->w2d(event_point(event->motion)) + pointer_offset;
364 // the new position is passed by reference and can be changed in the handlers.
365 dragged(new_pos, &event->motion);
366 move(new_pos);
367 _updateDragTip(&event->motion); // update dragging tip after moving to new position
369 _desktop->scroll_to_point(new_pos);
370 _desktop->set_coordinate_status(_position);
371 sp_event_context_snap_delay_handler(_desktop->event_context, NULL,
372 (gpointer) this, &event->motion,
373 DelayedSnapEvent::CONTROL_POINT_HANDLER);
374 }
375 return true;
376 }
377 break;
379 case GDK_BUTTON_RELEASE:
380 if (_event_grab && event->button.button == 1) {
381 // If we have any pending snap event, then invoke it now!
382 // (This is needed because we might not have snapped on the latest GDK_MOTION_NOTIFY event
383 // if the mouse speed was too high. This is inherent to the snap-delay mechanism.
384 // We must snap at some point in time though, and this is our last chance)
385 // PS: For other contexts this is handled already in sp_event_context_item_handler or
386 // sp_event_context_root_handler
387 if (_desktop->event_context->_delayed_snap_event) {
388 sp_event_context_snap_watchdog_callback(_desktop->event_context->_delayed_snap_event);
389 }
391 sp_canvas_item_ungrab(_canvas_item, event->button.time);
392 _setMouseover(this, event->button.state);
393 _event_grab = false;
395 if (_drag_initiated) {
396 sp_canvas_end_forced_full_redraws(_desktop->canvas);
397 }
399 if (_drag_initiated) {
400 // it is the end of a drag
401 _drag_initiated = false;
402 ungrabbed(&event->button);
403 return true;
404 } else {
405 // it is the end of a click
406 if (next_release_doubleclick) {
407 return doubleclicked(&event->button);
408 } else {
409 return clicked(&event->button);
410 }
411 }
412 }
413 break;
415 case GDK_ENTER_NOTIFY:
416 _setMouseover(this, event->crossing.state);
417 return true;
418 case GDK_LEAVE_NOTIFY:
419 _clearMouseover();
420 return true;
422 case GDK_GRAB_BROKEN:
423 if (_event_grab && !event->grab_broken.keyboard) {
424 {
425 ungrabbed(NULL);
426 if (_drag_initiated)
427 sp_canvas_end_forced_full_redraws(_desktop->canvas);
428 }
429 _setState(STATE_NORMAL);
430 _event_grab = false;
431 _drag_initiated = false;
432 return true;
433 }
434 break;
436 // update tips on modifier state change
437 // TODO add ESC keybinding as drag cancel
438 case GDK_KEY_PRESS:
439 case GDK_KEY_RELEASE:
440 if (mouseovered_point != this) return false;
441 if (_drag_initiated) {
442 return true; // this prevents the tool from overwriting the drag tip
443 } else {
444 unsigned state = state_after_event(event);
445 if (state != event->key.state) {
446 // we need to return true if there was a tip available, otherwise the tool's
447 // handler will process this event and set the tool's message, overwriting
448 // the point's message
449 return _updateTip(state);
450 }
451 }
452 break;
454 default: break;
455 }
457 // do not propagate events during grab - it might cause problems
458 return _event_grab;
459 }
461 void ControlPoint::_setMouseover(ControlPoint *p, unsigned state)
462 {
463 bool visible = p->visible();
464 if (visible) { // invisible points shouldn't get mouseovered
465 p->_setState(STATE_MOUSEOVER);
466 }
467 p->_updateTip(state);
469 if (visible && mouseovered_point != p) {
470 mouseovered_point = p;
471 signal_mouseover_change.emit(mouseovered_point);
472 }
473 }
475 bool ControlPoint::_updateTip(unsigned state)
476 {
477 Glib::ustring tip = _getTip(state);
478 if (!tip.empty()) {
479 _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
480 tip.data());
481 return true;
482 } else {
483 _desktop->event_context->defaultMessageContext()->clear();
484 return false;
485 }
486 }
488 bool ControlPoint::_updateDragTip(GdkEventMotion *event)
489 {
490 if (!_hasDragTips()) return false;
491 Glib::ustring tip = _getDragTip(event);
492 if (!tip.empty()) {
493 _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
494 tip.data());
495 return true;
496 } else {
497 _desktop->event_context->defaultMessageContext()->clear();
498 return false;
499 }
500 }
502 void ControlPoint::_clearMouseover()
503 {
504 if (mouseovered_point) {
505 mouseovered_point->_desktop->event_context->defaultMessageContext()->clear();
506 mouseovered_point->_setState(STATE_NORMAL);
507 mouseovered_point = 0;
508 signal_mouseover_change.emit(mouseovered_point);
509 }
510 }
512 /** Transfer the grab to another point. This method allows one to create a draggable point
513 * that should be dragged instead of the one that received the grabbed signal.
514 * This is used to implement dragging out handles in the new node tool, for example.
515 *
516 * This method will NOT emit the ungrab signal of @c prev_point, because this would complicate
517 * using it with selectable control points. If you use this method while dragging, you must emit
518 * the ungrab signal yourself.
519 *
520 * Note that this will break horribly if you try to transfer grab between points in different
521 * desktops, which doesn't make much sense anyway. */
522 void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event)
523 {
524 if (!_event_grab) return;
526 grabbed(event);
527 sp_canvas_item_ungrab(prev_point->_canvas_item, event->time);
528 sp_canvas_item_grab(_canvas_item, _grab_event_mask, NULL, event->time);
530 if (!_drag_initiated) {
531 sp_canvas_force_full_redraw_after_interruptions(_desktop->canvas, 5);
532 _drag_initiated = true;
533 }
535 prev_point->_setState(STATE_NORMAL);
536 _setMouseover(this, event->state);
537 }
539 /**
540 * @brief Change the state of the knot
541 * Alters the appearance of the knot to match one of the states: normal, mouseover
542 * or clicked.
543 */
544 void ControlPoint::_setState(State state)
545 {
546 ColorEntry current = {0, 0};
547 switch(state) {
548 case STATE_NORMAL:
549 current = _cset->normal; break;
550 case STATE_MOUSEOVER:
551 current = _cset->mouseover; break;
552 case STATE_CLICKED:
553 current = _cset->clicked; break;
554 };
555 _setColors(current);
556 _state = state;
557 }
558 void ControlPoint::_setColors(ColorEntry colors)
559 {
560 g_object_set(_canvas_item, "fill_color", colors.fill, "stroke_color", colors.stroke, NULL);
561 }
563 // dummy implementations for handlers
564 // they are here to avoid unused param warnings
565 bool ControlPoint::grabbed(GdkEventMotion *) { return false; }
566 void ControlPoint::dragged(Geom::Point &, GdkEventMotion *) {}
567 void ControlPoint::ungrabbed(GdkEventButton *) {}
568 bool ControlPoint::clicked(GdkEventButton *) { return false; }
569 bool ControlPoint::doubleclicked(GdkEventButton *) { return false; }
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:fileencoding=utf-8:textwidth=99 :