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 "ui/tool/control-point.h"
16 #include "ui/tool/event-utils.h"
17 #include "preferences.h"
18 #include "desktop.h"
19 #include "desktop-handles.h"
20 #include "event-context.h"
21 #include "message-context.h"
23 namespace Inkscape {
24 namespace UI {
26 // class and member documentation goes here...
28 /**
29 * @class ControlPoint
30 * @brief Draggable point, the workhorse of on-canvas editing.
31 *
32 * Control points (formerly known as knots) are graphical representations of some significant
33 * point in the drawing. The drawing can be changed by dragging the point and the things that are
34 * attached to it with the mouse. Example things that could be edited with draggable points
35 * are gradient stops, the place where text is attached to a path, text kerns, nodes and handles
36 * in a path, and many more. Control points use signals heavily - <b>read the libsigc++
37 * tutorial on the wiki</b> before using this class.</b>
38 *
39 * @par Control point signals
40 * @par
41 * The control point has several signals which allow you to react to things that happen to it.
42 * The most important singals are the grabbed, dragged, ungrabbed and moved signals.
43 * When a drag happens, the order of emission is as follows:
44 * - <tt>signal_grabbed</tt>
45 * - <tt>signal_dragged</tt>
46 * - <tt>signal_dragged</tt>
47 * - <tt>signal_dragged</tt>
48 * - ...
49 * - <tt>signal_dragged</tt>
50 * - <tt>signal_ungrabbed</tt>
51 *
52 * The control point can also respond to clicks and double clicks. On a double click,
53 * <tt>signal_clicked</tt> is emitted, followed by <tt>signal_doubleclicked</tt>.
54 *
55 * A few signal usage hints if you can't be bothered to read the tutorial:
56 * - If you want some other object or a global function to react to signals of a control point
57 * from some other object, and you want to access the control point that emitted the signal
58 * in the handler, use <tt>sigc::bind</tt> like this:
59 * @code
60 * void handle_clicked_signal(ControlPoint *point, int button);
61 * point->signal_clicked.connect(
62 * sigc::bind<0>( sigc::ptr_fun(handle_clicked_signal),
63 * point ));
64 * @endcode
65 * - You can ignore unneeded parameters using sigc::hide.
66 * - If you want to get rid of the handlers added by constructors in superclasses,
67 * use the <tt>clear()</tt> method: @code signal_clicked.clear(); @endcode
68 * - To connect at the front of the slot list instead of at the end, use:
69 * @code
70 * signal_clicked.slots().push_front(
71 * sigc::mem_fun(*this, &FunkyPoint::_clickedHandler));
72 * @endcode
73 * - Note that calling <tt>slots()</tt> does not copy anything. You can disconnect
74 * and reorder slots by manipulating the elements of the slot list. The returned object is
75 * of type @verbatim (signal type)::slot_list @endverbatim.
76 *
77 * @par Which method to override?
78 * @par
79 * You might wonder which hook to use when you want to do things when the point is relocated.
80 * Here are some tips:
81 * - If the point is used to edit an object, override the move() method.
82 * - If the point can usually be dragged wherever you like but can optionally be constrained
83 * to axes or the like, add a handler for <tt>signal_dragged</tt> that modifies its new
84 * position argument.
85 * - If the point has additional canvas items tied to it (like handle lines), override
86 * the setPosition() method.
87 */
89 /**
90 * @var ControlPoint::signal_dragged
91 * Emitted while dragging, but before moving the knot to new position.
92 * Old position will always be the same as position() - there are two parameters
93 * only for convenience.
94 * - First parameter: old position, always equal to position()
95 * - Second parameter: new position (after drag). This is passed as a non-const reference,
96 * so you can change it from the handler - that's how constrained dragging is implemented.
97 * - Third parameter: motion event
98 */
100 /**
101 * @var ControlPoint::signal_clicked
102 * Emitted when the control point is clicked, at mouse button release. The parameter contains
103 * the event that caused the signal to be emitted. Your signal handler should return true
104 * if the click had some effect. If it did nothing, return false. Improperly handling this signal
105 * can cause the context menu not to appear when a control point is right-clicked.
106 */
108 /**
109 * @var ControlPoint::signal_doubleclicked
110 * Emitted when the control point is doubleclicked, at mouse button release. The parameter
111 * contains the event that caused the signal to be emitted. Your signal handler should return true
112 * if the double click had some effect. If it did nothing, return false.
113 */
115 /**
116 * @var ControlPoint::signal_grabbed
117 * Emitted when the control point is grabbed and a drag starts. The parameter contains
118 * the causing event. Return true to prevent further processing. Because all control points
119 * handle drag tolerance, <tt>signal_dragged</tt> will be emitted immediately after this signal
120 * to move the point to its new position.
121 */
123 /**
124 * @var ControlPoint::signal_ungrabbed
125 * Emitted when the control point finishes a drag. The parameter contains the event which
126 * caused the signal, but it can be NULL if the grab was broken.
127 */
129 /**
130 * @enum ControlPoint::State
131 * Enumeration representing the possible states of the control point, used to determine
132 * its appearance.
133 * @var ControlPoint::STATE_NORMAL
134 * Normal state
135 * @var ControlPoint::STATE_MOUSEOVER
136 * Mouse is hovering over the control point
137 * @var ControlPoint::STATE_CLICKED
138 * First mouse button pressed over the control point
139 */
141 // Default colors for control points
142 static ControlPoint::ColorSet default_color_set = {
143 {0xffffff00, 0x01000000}, // normal fill, stroke
144 {0xff0000ff, 0x01000000}, // mouseover fill, stroke
145 {0x0000ffff, 0x01000000} // clicked fill, stroke
146 };
148 /** Holds the currently mouseovered control point. */
149 ControlPoint *ControlPoint::mouseovered_point = 0;
151 /** Emitted when the mouseovered point changes. The parameter is the new mouseovered point.
152 * When a point ceases to be mouseovered, the parameter will be NULL. */
153 sigc::signal<void, ControlPoint*> ControlPoint::signal_mouseover_change;
155 /** Stores the window point over which the cursor was during the last mouse button press */
156 Geom::Point ControlPoint::_drag_event_origin(Geom::infinity(), Geom::infinity());
158 /** Stores the desktop point from which the last drag was initiated */
159 Geom::Point ControlPoint::_drag_origin(Geom::infinity(), Geom::infinity());
161 /** Events which should be captured when a handle is being dragged. */
162 int const ControlPoint::_grab_event_mask = (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
163 GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_KEY_PRESS_MASK |
164 GDK_KEY_RELEASE_MASK);
166 bool ControlPoint::_drag_initiated = false;
167 bool ControlPoint::_event_grab = false;
169 /** A color set which you can use to create an invisible control that can still receive events.
170 * @relates ControlPoint */
171 ControlPoint::ColorSet invisible_cset = {
172 {0x00000000, 0x00000000},
173 {0x00000000, 0x00000000},
174 {0x00000000, 0x00000000}
175 };
177 /**
178 * Create a regular control point.
179 * Derive to have constructors with a reasonable number of parameters.
180 *
181 * @param d Desktop for this control
182 * @param initial_pos Initial position of the control point in desktop coordinates
183 * @param anchor Where is the control point rendered relative to its desktop coordinates
184 * @param shape Shape of the control point: square, diamond, circle...
185 * @param size Pixel size of the visual representation
186 * @param cset Colors of the point
187 * @param group The canvas group the point's canvas item should be created in
188 */
189 ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos,
190 Gtk::AnchorType anchor, SPCtrlShapeType shape,
191 unsigned int size, ColorSet *cset, SPCanvasGroup *group)
192 : _desktop (d)
193 , _canvas_item (NULL)
194 , _cset (cset ? cset : &default_color_set)
195 , _state (STATE_NORMAL)
196 , _position (initial_pos)
197 {
198 _canvas_item = sp_canvas_item_new(
199 group ? group : sp_desktop_controls (_desktop), SP_TYPE_CTRL,
200 "anchor", (GtkAnchorType) anchor, "size", (gdouble) size, "shape", shape,
201 "filled", TRUE, "fill_color", _cset->normal.fill,
202 "stroked", TRUE, "stroke_color", _cset->normal.stroke,
203 "mode", SP_CTRL_MODE_XOR, NULL);
204 _commonInit();
205 }
207 /**
208 * Create a control point with a pixbuf-based visual representation.
209 *
210 * @param d Desktop for this control
211 * @param initial_pos Initial position of the control point in desktop coordinates
212 * @param anchor Where is the control point rendered relative to its desktop coordinates
213 * @param pixbuf Pixbuf to be used as the visual representation
214 * @param cset Colors of the point
215 * @param group The canvas group the point's canvas item should be created in
216 */
217 ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos,
218 Gtk::AnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pixbuf,
219 ColorSet *cset, SPCanvasGroup *group)
220 : _desktop (d)
221 , _canvas_item (NULL)
222 , _cset(cset ? cset : &default_color_set)
223 , _position (initial_pos)
224 {
225 _canvas_item = sp_canvas_item_new(
226 group ? group : sp_desktop_controls(_desktop), SP_TYPE_CTRL,
227 "anchor", (GtkAnchorType) anchor, "size", (gdouble) pixbuf->get_width(),
228 "shape", SP_CTRL_SHAPE_BITMAP, "pixbuf", pixbuf->gobj(),
229 "filled", TRUE, "fill_color", _cset->normal.fill,
230 "stroked", TRUE, "stroke_color", _cset->normal.stroke,
231 "mode", SP_CTRL_MODE_XOR, NULL);
232 _commonInit();
233 }
235 ControlPoint::~ControlPoint()
236 {
237 // avoid storing invalid points in mouseovered_point
238 if (this == mouseovered_point) {
239 _clearMouseover();
240 }
242 g_signal_handler_disconnect(G_OBJECT(_canvas_item), _event_handler_connection);
243 //sp_canvas_item_hide(_canvas_item);
244 gtk_object_destroy(_canvas_item);
245 }
247 void ControlPoint::_commonInit()
248 {
249 _event_handler_connection = g_signal_connect(G_OBJECT(_canvas_item), "event",
250 G_CALLBACK(_event_handler), this);
251 SP_CTRL(_canvas_item)->moveto(_position);
252 }
254 /** Relocate the control point without side effects.
255 * Overload this method only if there is an additional graphical representation
256 * that must be updated (like the lines that connect handles to nodes). If you override it,
257 * you must also call the superclass implementation of the method.
258 * @todo Investigate whether this method should be protected */
259 void ControlPoint::setPosition(Geom::Point const &pos)
260 {
261 _position = pos;
262 SP_CTRL(_canvas_item)->moveto(pos);
263 }
265 /** Move the control point to new position with side effects.
266 * This is called after each drag. Override this method if only some positions make sense
267 * for a control point (like a point that must always be on a path and can't modify it),
268 * or when moving a control point changes the positions of other points. */
269 void ControlPoint::move(Geom::Point const &pos)
270 {
271 setPosition(pos);
272 }
274 /** Apply an arbitrary affine transformation to a control point. This is used
275 * by ControlPointSelection, and is important for things like nodes with handles.
276 * The default implementation simply moves the point according to the transform. */
277 void ControlPoint::transform(Geom::Matrix const &m) {
278 move(position() * m);
279 }
281 bool ControlPoint::visible() const
282 {
283 return sp_canvas_item_is_visible(_canvas_item);
284 }
286 /** Set the visibility of the control point. An invisible point is not drawn on the canvas
287 * and cannot receive any events. If you want to have an invisible point that can respond
288 * to events, use <tt>invisible_cset</tt> as its color set. */
289 void ControlPoint::setVisible(bool v)
290 {
291 if (v) sp_canvas_item_show(_canvas_item);
292 else sp_canvas_item_hide(_canvas_item);
293 }
295 Glib::ustring ControlPoint::format_tip(char const *format, ...)
296 {
297 va_list args;
298 va_start(args, format);
299 char *dyntip = g_strdup_vprintf(format, args);
300 va_end(args);
301 Glib::ustring ret = dyntip;
302 g_free(dyntip);
303 return ret;
304 }
306 unsigned int ControlPoint::_size() const
307 {
308 double ret;
309 g_object_get(_canvas_item, "size", &ret, NULL);
310 return static_cast<unsigned int>(ret);
311 }
313 SPCtrlShapeType ControlPoint::_shape() const
314 {
315 SPCtrlShapeType ret;
316 g_object_get(_canvas_item, "shape", &ret, NULL);
317 return ret;
318 }
320 GtkAnchorType ControlPoint::_anchor() const
321 {
322 GtkAnchorType ret;
323 g_object_get(_canvas_item, "anchor", &ret, NULL);
324 return ret;
325 }
327 Glib::RefPtr<Gdk::Pixbuf> ControlPoint::_pixbuf()
328 {
329 GdkPixbuf *ret;
330 g_object_get(_canvas_item, "pixbuf", &ret, NULL);
331 return Glib::wrap(ret);
332 }
334 // Same for setters.
336 void ControlPoint::_setSize(unsigned int size)
337 {
338 g_object_set(_canvas_item, "size", (gdouble) size, NULL);
339 }
341 void ControlPoint::_setShape(SPCtrlShapeType shape)
342 {
343 g_object_set(_canvas_item, "shape", shape, NULL);
344 }
346 void ControlPoint::_setAnchor(GtkAnchorType anchor)
347 {
348 g_object_set(_canvas_item, "anchor", anchor, NULL);
349 }
351 void ControlPoint::_setPixbuf(Glib::RefPtr<Gdk::Pixbuf> p)
352 {
353 g_object_set(_canvas_item, "pixbuf", Glib::unwrap(p), NULL);
354 }
356 // re-routes events into the virtual function
357 int ControlPoint::_event_handler(SPCanvasItem *item, GdkEvent *event, ControlPoint *point)
358 {
359 return point->_eventHandler(event) ? TRUE : FALSE;
360 }
362 // main event callback, which emits all other callbacks.
363 bool ControlPoint::_eventHandler(GdkEvent *event)
364 {
365 // NOTE the static variables below are shared for all points!
367 // offset from the pointer hotspot to the center of the grabbed knot in desktop coords
368 static Geom::Point pointer_offset;
369 // number of last doubleclicked button, to be
370 static unsigned next_release_doubleclick = 0;
372 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
373 int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
375 switch(event->type)
376 {
377 case GDK_2BUTTON_PRESS:
378 // store the button number for next release
379 next_release_doubleclick = event->button.button;
380 return true;
382 case GDK_BUTTON_PRESS:
383 next_release_doubleclick = 0;
384 if (event->button.button == 1) {
385 // mouse click. internally, start dragging, but do not emit signals
386 // or change position until drag tolerance is exceeded.
387 _drag_event_origin[Geom::X] = event->button.x;
388 _drag_event_origin[Geom::Y] = event->button.y;
389 pointer_offset = _position - _desktop->w2d(_drag_event_origin);
390 _drag_initiated = false;
391 // route all events to this handler
392 sp_canvas_item_grab(_canvas_item, _grab_event_mask, NULL, event->button.time);
393 _event_grab = true;
394 _setState(STATE_CLICKED);
395 }
396 return true;
398 case GDK_MOTION_NOTIFY:
399 if (held_button<1>(event->motion) && !_desktop->event_context->space_panning) {
400 bool transferred = false;
401 if (!_drag_initiated) {
402 bool t = fabs(event->motion.x - _drag_event_origin[Geom::X]) <= drag_tolerance &&
403 fabs(event->motion.y - _drag_event_origin[Geom::Y]) <= drag_tolerance;
404 if (t) return true;
406 // if we are here, it means the tolerance was just exceeded.
407 next_release_doubleclick = 0;
408 _drag_origin = _position;
409 transferred = signal_grabbed.emit(&event->motion);
410 // _drag_initiated might change during the above signal emission
411 if (!_drag_initiated) {
412 // this guarantees smooth redraws while dragging
413 sp_canvas_force_full_redraw_after_interruptions(_desktop->canvas, 5);
414 _drag_initiated = true;
415 }
416 }
417 if (transferred) return true;
418 // the point was moved beyond the drag tolerance
419 Geom::Point new_pos = _desktop->w2d(event_point(event->motion)) + pointer_offset;
421 // the new position is passed by reference and can be changed in the handlers.
422 signal_dragged.emit(_position, new_pos, &event->motion);
423 move(new_pos);
424 _updateDragTip(&event->motion); // update dragging tip after moving to new position
426 _desktop->scroll_to_point(new_pos);
427 _desktop->set_coordinate_status(_position);
428 return true;
429 }
430 break;
432 case GDK_BUTTON_RELEASE:
433 if (_event_grab) {
434 sp_canvas_item_ungrab(_canvas_item, event->button.time);
435 _setMouseover(this, event->button.state);
436 _event_grab = false;
438 if (_drag_initiated) {
439 sp_canvas_end_forced_full_redraws(_desktop->canvas);
440 }
442 if (event->button.button == next_release_doubleclick) {
443 _drag_initiated = false;
444 return signal_doubleclicked.emit(&event->button);
445 }
446 if (event->button.button == 1) {
447 if (_drag_initiated) {
448 // it is the end of a drag
449 signal_ungrabbed.emit(&event->button);
450 _drag_initiated = false;
451 return true;
452 } else {
453 // it is the end of a click
454 return signal_clicked.emit(&event->button);
455 }
456 }
457 _drag_initiated = false;
458 }
459 break;
461 case GDK_ENTER_NOTIFY:
462 _setMouseover(this, event->crossing.state);
463 return true;
464 case GDK_LEAVE_NOTIFY:
465 _clearMouseover();
466 return true;
468 case GDK_GRAB_BROKEN:
469 if (!event->grab_broken.keyboard && _event_grab) {
470 {
471 signal_ungrabbed.emit(0);
472 if (_drag_initiated)
473 sp_canvas_end_forced_full_redraws(_desktop->canvas);
474 }
475 _setState(STATE_NORMAL);
476 _event_grab = false;
477 _drag_initiated = false;
478 return true;
479 }
480 break;
482 // update tips on modifier state change
483 case GDK_KEY_PRESS:
484 case GDK_KEY_RELEASE:
485 if (mouseovered_point != this) return false;
486 if (_drag_initiated) {
487 return true; // this prevents the tool from overwriting the drag tip
488 } else {
489 unsigned state = state_after_event(event);
490 if (state != event->key.state) {
491 // we need to return true if there was a tip available, otherwise the tool's
492 // handler will process this event and set the tool's message, overwriting
493 // the point's message
494 return _updateTip(state);
495 }
496 }
497 break;
499 default: break;
500 }
502 return false;
503 }
505 void ControlPoint::_setMouseover(ControlPoint *p, unsigned state)
506 {
507 bool visible = p->visible();
508 if (visible) { // invisible points shouldn't get mouseovered
509 p->_setState(STATE_MOUSEOVER);
510 }
511 p->_updateTip(state);
513 if (visible && mouseovered_point != p) {
514 mouseovered_point = p;
515 signal_mouseover_change.emit(mouseovered_point);
516 }
517 }
519 bool ControlPoint::_updateTip(unsigned state)
520 {
521 Glib::ustring tip = _getTip(state);
522 if (!tip.empty()) {
523 _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
524 tip.data());
525 return true;
526 } else {
527 _desktop->event_context->defaultMessageContext()->clear();
528 return false;
529 }
530 }
532 bool ControlPoint::_updateDragTip(GdkEventMotion *event)
533 {
534 if (!_hasDragTips()) return false;
535 Glib::ustring tip = _getDragTip(event);
536 if (!tip.empty()) {
537 _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
538 tip.data());
539 return true;
540 } else {
541 _desktop->event_context->defaultMessageContext()->clear();
542 return false;
543 }
544 }
546 void ControlPoint::_clearMouseover()
547 {
548 if (mouseovered_point) {
549 mouseovered_point->_desktop->event_context->defaultMessageContext()->clear();
550 mouseovered_point->_setState(STATE_NORMAL);
551 mouseovered_point = 0;
552 signal_mouseover_change.emit(mouseovered_point);
553 }
554 }
556 /** Transfer the grab to another point. This method allows one to create a draggable point
557 * that should be dragged instead of the one that received the grabbed signal.
558 * This is used to implement dragging out handles in the new node tool, for example.
559 *
560 * This method will NOT emit the ungrab signal of @c prev_point, because this would complicate
561 * using it with selectable control points. If you use this method while dragging, you must emit
562 * the ungrab signal yourself.
563 *
564 * Note that this will break horribly if you try to transfer grab between points in different
565 * desktops, which doesn't make much sense anyway. */
566 void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event)
567 {
568 if (!_event_grab) return;
570 signal_grabbed.emit(event);
571 sp_canvas_item_ungrab(prev_point->_canvas_item, event->time);
572 sp_canvas_item_grab(_canvas_item, _grab_event_mask, NULL, event->time);
574 if (!_drag_initiated) {
575 sp_canvas_force_full_redraw_after_interruptions(_desktop->canvas, 5);
576 _drag_initiated = true;
577 }
579 prev_point->_setState(STATE_NORMAL);
580 _setMouseover(this, event->state);
581 }
583 /**
584 * @brief Change the state of the knot
585 * Alters the appearance of the knot to match one of the states: normal, mouseover
586 * or clicked.
587 */
588 void ControlPoint::_setState(State state)
589 {
590 ColorEntry current = {0, 0};
591 switch(state) {
592 case STATE_NORMAL:
593 current = _cset->normal; break;
594 case STATE_MOUSEOVER:
595 current = _cset->mouseover; break;
596 case STATE_CLICKED:
597 current = _cset->clicked; break;
598 };
599 _setColors(current);
600 _state = state;
601 }
602 void ControlPoint::_setColors(ColorEntry colors)
603 {
604 g_object_set(_canvas_item, "fill_color", colors.fill, "stroke_color", colors.stroke, NULL);
605 }
607 } // namespace UI
608 } // namespace Inkscape
610 /*
611 Local Variables:
612 mode:c++
613 c-file-style:"stroustrup"
614 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
615 indent-tabs-mode:nil
616 fill-column:99
617 End:
618 */
619 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :