Code

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