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)
136 {
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();
144 }
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)
163 {
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();
172 }
174 ControlPoint::~ControlPoint()
175 {
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);
184 }
186 void ControlPoint::_commonInit()
187 {
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);
191 }
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)
199 {
200 _position = pos;
201 SP_CTRL(_canvas_item)->moveto(pos);
202 }
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)
209 {
210 setPosition(pos);
211 }
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);
218 }
220 bool ControlPoint::visible() const
221 {
222 return sp_canvas_item_is_visible(_canvas_item);
223 }
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)
229 {
230 if (v) sp_canvas_item_show(_canvas_item);
231 else sp_canvas_item_hide(_canvas_item);
232 }
234 Glib::ustring ControlPoint::format_tip(char const *format, ...)
235 {
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;
243 }
245 unsigned int ControlPoint::_size() const
246 {
247 double ret;
248 g_object_get(_canvas_item, "size", &ret, NULL);
249 return static_cast<unsigned int>(ret);
250 }
252 SPCtrlShapeType ControlPoint::_shape() const
253 {
254 SPCtrlShapeType ret;
255 g_object_get(_canvas_item, "shape", &ret, NULL);
256 return ret;
257 }
259 GtkAnchorType ControlPoint::_anchor() const
260 {
261 GtkAnchorType ret;
262 g_object_get(_canvas_item, "anchor", &ret, NULL);
263 return ret;
264 }
266 Glib::RefPtr<Gdk::Pixbuf> ControlPoint::_pixbuf()
267 {
268 GdkPixbuf *ret;
269 g_object_get(_canvas_item, "pixbuf", &ret, NULL);
270 return Glib::wrap(ret);
271 }
273 // Same for setters.
275 void ControlPoint::_setSize(unsigned int size)
276 {
277 g_object_set(_canvas_item, "size", (gdouble) size, NULL);
278 }
280 void ControlPoint::_setShape(SPCtrlShapeType shape)
281 {
282 g_object_set(_canvas_item, "shape", shape, NULL);
283 }
285 void ControlPoint::_setAnchor(GtkAnchorType anchor)
286 {
287 g_object_set(_canvas_item, "anchor", anchor, NULL);
288 }
290 void ControlPoint::_setPixbuf(Glib::RefPtr<Gdk::Pixbuf> p)
291 {
292 g_object_set(_canvas_item, "pixbuf", Glib::unwrap(p), NULL);
293 }
295 // re-routes events into the virtual function
296 int ControlPoint::_event_handler(SPCanvasItem */*item*/, GdkEvent *event, ControlPoint *point)
297 {
298 return point->_eventHandler(event) ? TRUE : FALSE;
299 }
301 // main event callback, which emits all other callbacks.
302 bool ControlPoint::_eventHandler(GdkEvent *event)
303 {
304 // NOTE the static variables below are shared for all points!
306 // offset from the pointer hotspot to the center of the grabbed knot in desktop coords
307 static Geom::Point pointer_offset;
308 // number of last doubleclicked button, to be
309 static unsigned next_release_doubleclick = 0;
311 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
312 int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
314 switch(event->type)
315 {
316 case GDK_2BUTTON_PRESS:
317 // store the button number for next release
318 next_release_doubleclick = event->button.button;
319 return true;
321 case GDK_BUTTON_PRESS:
322 next_release_doubleclick = 0;
323 if (event->button.button == 1) {
324 // mouse click. internally, start dragging, but do not emit signals
325 // or change position until drag tolerance is exceeded.
326 _drag_event_origin[Geom::X] = event->button.x;
327 _drag_event_origin[Geom::Y] = event->button.y;
328 pointer_offset = _position - _desktop->w2d(_drag_event_origin);
329 _drag_initiated = false;
330 // route all events to this handler
331 sp_canvas_item_grab(_canvas_item, _grab_event_mask, NULL, event->button.time);
332 _event_grab = true;
333 _setState(STATE_CLICKED);
334 }
335 return true;
337 case GDK_MOTION_NOTIFY:
338 if (held_button<1>(event->motion) && !_desktop->event_context->space_panning) {
339 _desktop->snapindicator->remove_snaptarget();
340 bool transferred = false;
341 if (!_drag_initiated) {
342 bool t = fabs(event->motion.x - _drag_event_origin[Geom::X]) <= drag_tolerance &&
343 fabs(event->motion.y - _drag_event_origin[Geom::Y]) <= drag_tolerance;
344 if (t) return true;
346 // if we are here, it means the tolerance was just exceeded.
347 next_release_doubleclick = 0;
348 _drag_origin = _position;
349 transferred = grabbed(&event->motion);
350 // _drag_initiated might change during the above virtual call
351 if (!_drag_initiated) {
352 // this guarantees smooth redraws while dragging
353 sp_canvas_force_full_redraw_after_interruptions(_desktop->canvas, 5);
354 _drag_initiated = true;
355 }
356 }
357 if (!transferred) {
358 // dragging in progress
359 Geom::Point new_pos = _desktop->w2d(event_point(event->motion)) + pointer_offset;
361 // the new position is passed by reference and can be changed in the handlers.
362 dragged(new_pos, &event->motion);
363 move(new_pos);
364 _updateDragTip(&event->motion); // update dragging tip after moving to new position
366 _desktop->scroll_to_point(new_pos);
367 _desktop->set_coordinate_status(_position);
368 sp_event_context_snap_delay_handler(_desktop->event_context, NULL,
369 reinterpret_cast<SPKnot*>(this), &event->motion,
370 DelayedSnapEvent::CONTROL_POINT_HANDLER);
371 }
372 return true;
373 }
374 break;
376 case GDK_BUTTON_RELEASE:
377 if (!_event_grab) break;
379 // TODO I think snapping on release is wrong, or at least counter-intuitive.
380 sp_event_context_snap_watchdog_callback(_desktop->event_context->_delayed_snap_event);
381 sp_event_context_discard_delayed_snap_event(_desktop->event_context);
382 _desktop->snapindicator->remove_snaptarget();
384 sp_canvas_item_ungrab(_canvas_item, event->button.time);
385 _setMouseover(this, event->button.state);
386 _event_grab = false;
388 if (_drag_initiated) {
389 sp_canvas_end_forced_full_redraws(_desktop->canvas);
390 }
392 if (event->button.button == next_release_doubleclick) {
393 _drag_initiated = false;
394 return doubleclicked(&event->button);
395 }
396 if (event->button.button == 1) {
397 if (_drag_initiated) {
398 // it is the end of a drag
399 ungrabbed(&event->button);
400 _drag_initiated = false;
401 return true;
402 } else {
403 // it is the end of a click
404 return clicked(&event->button);
405 }
406 }
407 _drag_initiated = false;
408 break;
410 case GDK_ENTER_NOTIFY:
411 _setMouseover(this, event->crossing.state);
412 return true;
413 case GDK_LEAVE_NOTIFY:
414 _clearMouseover();
415 return true;
417 case GDK_GRAB_BROKEN:
418 if (!event->grab_broken.keyboard && _event_grab) {
419 {
420 ungrabbed(NULL);
421 if (_drag_initiated)
422 sp_canvas_end_forced_full_redraws(_desktop->canvas);
423 }
424 _setState(STATE_NORMAL);
425 _event_grab = false;
426 _drag_initiated = false;
427 return true;
428 }
429 break;
431 // update tips on modifier state change
432 case GDK_KEY_PRESS:
433 case GDK_KEY_RELEASE:
434 if (mouseovered_point != this) return false;
435 if (_drag_initiated) {
436 return true; // this prevents the tool from overwriting the drag tip
437 } else {
438 unsigned state = state_after_event(event);
439 if (state != event->key.state) {
440 // we need to return true if there was a tip available, otherwise the tool's
441 // handler will process this event and set the tool's message, overwriting
442 // the point's message
443 return _updateTip(state);
444 }
445 }
446 break;
448 default: break;
449 }
451 return false;
452 }
454 void ControlPoint::_setMouseover(ControlPoint *p, unsigned state)
455 {
456 bool visible = p->visible();
457 if (visible) { // invisible points shouldn't get mouseovered
458 p->_setState(STATE_MOUSEOVER);
459 }
460 p->_updateTip(state);
462 if (visible && mouseovered_point != p) {
463 mouseovered_point = p;
464 signal_mouseover_change.emit(mouseovered_point);
465 }
466 }
468 bool ControlPoint::_updateTip(unsigned state)
469 {
470 Glib::ustring tip = _getTip(state);
471 if (!tip.empty()) {
472 _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
473 tip.data());
474 return true;
475 } else {
476 _desktop->event_context->defaultMessageContext()->clear();
477 return false;
478 }
479 }
481 bool ControlPoint::_updateDragTip(GdkEventMotion *event)
482 {
483 if (!_hasDragTips()) return false;
484 Glib::ustring tip = _getDragTip(event);
485 if (!tip.empty()) {
486 _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
487 tip.data());
488 return true;
489 } else {
490 _desktop->event_context->defaultMessageContext()->clear();
491 return false;
492 }
493 }
495 void ControlPoint::_clearMouseover()
496 {
497 if (mouseovered_point) {
498 mouseovered_point->_desktop->event_context->defaultMessageContext()->clear();
499 mouseovered_point->_setState(STATE_NORMAL);
500 mouseovered_point = 0;
501 signal_mouseover_change.emit(mouseovered_point);
502 }
503 }
505 /** Transfer the grab to another point. This method allows one to create a draggable point
506 * that should be dragged instead of the one that received the grabbed signal.
507 * This is used to implement dragging out handles in the new node tool, for example.
508 *
509 * This method will NOT emit the ungrab signal of @c prev_point, because this would complicate
510 * using it with selectable control points. If you use this method while dragging, you must emit
511 * the ungrab signal yourself.
512 *
513 * Note that this will break horribly if you try to transfer grab between points in different
514 * desktops, which doesn't make much sense anyway. */
515 void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event)
516 {
517 if (!_event_grab) return;
519 grabbed(event);
520 sp_canvas_item_ungrab(prev_point->_canvas_item, event->time);
521 sp_canvas_item_grab(_canvas_item, _grab_event_mask, NULL, event->time);
523 if (!_drag_initiated) {
524 sp_canvas_force_full_redraw_after_interruptions(_desktop->canvas, 5);
525 _drag_initiated = true;
526 }
528 prev_point->_setState(STATE_NORMAL);
529 _setMouseover(this, event->state);
530 }
532 /**
533 * @brief Change the state of the knot
534 * Alters the appearance of the knot to match one of the states: normal, mouseover
535 * or clicked.
536 */
537 void ControlPoint::_setState(State state)
538 {
539 ColorEntry current = {0, 0};
540 switch(state) {
541 case STATE_NORMAL:
542 current = _cset->normal; break;
543 case STATE_MOUSEOVER:
544 current = _cset->mouseover; break;
545 case STATE_CLICKED:
546 current = _cset->clicked; break;
547 };
548 _setColors(current);
549 _state = state;
550 }
551 void ControlPoint::_setColors(ColorEntry colors)
552 {
553 g_object_set(_canvas_item, "fill_color", colors.fill, "stroke_color", colors.stroke, NULL);
554 }
556 // dummy implementations for handlers
557 // they are here to avoid unused param warnings
558 bool ControlPoint::grabbed(GdkEventMotion *) { return false; }
559 void ControlPoint::dragged(Geom::Point &, GdkEventMotion *) {}
560 void ControlPoint::ungrabbed(GdkEventButton *) {}
561 bool ControlPoint::clicked(GdkEventButton *) { return false; }
562 bool ControlPoint::doubleclicked(GdkEventButton *) { return false; }
564 } // namespace UI
565 } // namespace Inkscape
567 /*
568 Local Variables:
569 mode:c++
570 c-file-style:"stroustrup"
571 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
572 indent-tabs-mode:nil
573 fill-column:99
574 End:
575 */
576 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :