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