/** * @file * Desktop-bound visual control object - implementation. */ /* Authors: * Krzysztof KosiƄski * * Copyright (C) 2009 Authors * Released under GNU GPL, read the file 'COPYING' for more information */ #include #include #include #include <2geom/point.h> #include "desktop.h" #include "desktop-handles.h" #include "display/sp-canvas.h" #include "display/snap-indicator.h" #include "event-context.h" #include "message-context.h" #include "preferences.h" #include "ui/tool/control-point.h" #include "ui/tool/event-utils.h" #include "ui/tool/transform-handle-set.h" namespace Inkscape { namespace UI { // class and member documentation goes here... /** * @class ControlPoint * Draggable point, the workhorse of on-canvas editing. * * Control points (formerly known as knots) are graphical representations of some significant * point in the drawing. The drawing can be changed by dragging the point and the things that are * attached to it with the mouse. Example things that could be edited with draggable points * are gradient stops, the place where text is attached to a path, text kerns, nodes and handles * in a path, and many more. * * @par Control point event handlers * @par * The control point has several virtual methods which allow you to react to things that * happen to it. The most important ones are the grabbed, dragged, ungrabbed and moved functions. * When a drag happens, the order of calls is as follows: * - grabbed() * - dragged() * - dragged() * - dragged() * - ... * - dragged() * - ungrabbed() * * The control point can also respond to clicks and double clicks. On a double click, * clicked() is called, followed by doubleclicked(). When deriving from SelectableControlPoint, * you need to manually call the superclass version at the appropriate point in your handler. * * @par Which method to override? * @par * You might wonder which hook to use when you want to do things when the point is relocated. * Here are some tips: * - If the point is used to edit an object, override the move() method. * - If the point can usually be dragged wherever you like but can optionally be constrained * to axes or the like, add a handler for signal_dragged that modifies its new * position argument. * - If the point has additional canvas items tied to it (like handle lines), override * the setPosition() method. */ /** * @enum ControlPoint::State * Enumeration representing the possible states of the control point, used to determine * its appearance. * @var ControlPoint::STATE_NORMAL * Normal state * @var ControlPoint::STATE_MOUSEOVER * Mouse is hovering over the control point * @var ControlPoint::STATE_CLICKED * First mouse button pressed over the control point */ // Default colors for control points static ControlPoint::ColorSet default_color_set = { {0xffffff00, 0x01000000}, // normal fill, stroke {0xff0000ff, 0x01000000}, // mouseover fill, stroke {0x0000ffff, 0x01000000} // clicked fill, stroke }; /** Holds the currently mouseovered control point. */ ControlPoint *ControlPoint::mouseovered_point = 0; /** Emitted when the mouseovered point changes. The parameter is the new mouseovered point. * When a point ceases to be mouseovered, the parameter will be NULL. */ sigc::signal ControlPoint::signal_mouseover_change; /** Stores the window point over which the cursor was during the last mouse button press */ Geom::Point ControlPoint::_drag_event_origin(Geom::infinity(), Geom::infinity()); /** Stores the desktop point from which the last drag was initiated */ Geom::Point ControlPoint::_drag_origin(Geom::infinity(), Geom::infinity()); /** Events which should be captured when a handle is being dragged. */ int const ControlPoint::_grab_event_mask = (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK); bool ControlPoint::_drag_initiated = false; bool ControlPoint::_event_grab = false; /** A color set which you can use to create an invisible control that can still receive events. * @relates ControlPoint */ ControlPoint::ColorSet invisible_cset = { {0x00000000, 0x00000000}, {0x00000000, 0x00000000}, {0x00000000, 0x00000000} }; /** * Create a regular control point. * Derive to have constructors with a reasonable number of parameters. * * @param d Desktop for this control * @param initial_pos Initial position of the control point in desktop coordinates * @param anchor Where is the control point rendered relative to its desktop coordinates * @param shape Shape of the control point: square, diamond, circle... * @param size Pixel size of the visual representation * @param cset Colors of the point * @param group The canvas group the point's canvas item should be created in */ ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, Gtk::AnchorType anchor, SPCtrlShapeType shape, unsigned int size, ColorSet *cset, SPCanvasGroup *group) : _desktop (d) , _canvas_item (NULL) , _cset (cset ? cset : &default_color_set) , _state (STATE_NORMAL) , _position (initial_pos) { _canvas_item = sp_canvas_item_new( group ? group : sp_desktop_controls (_desktop), SP_TYPE_CTRL, "anchor", (GtkAnchorType) anchor, "size", (gdouble) size, "shape", shape, "filled", TRUE, "fill_color", _cset->normal.fill, "stroked", TRUE, "stroke_color", _cset->normal.stroke, "mode", SP_CTRL_MODE_XOR, NULL); _commonInit(); } /** * Create a control point with a pixbuf-based visual representation. * * @param d Desktop for this control * @param initial_pos Initial position of the control point in desktop coordinates * @param anchor Where is the control point rendered relative to its desktop coordinates * @param pixbuf Pixbuf to be used as the visual representation * @param cset Colors of the point * @param group The canvas group the point's canvas item should be created in */ ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, Gtk::AnchorType anchor, Glib::RefPtr pixbuf, ColorSet *cset, SPCanvasGroup *group) : _desktop (d) , _canvas_item (NULL) , _cset(cset ? cset : &default_color_set) , _position (initial_pos) { _canvas_item = sp_canvas_item_new( group ? group : sp_desktop_controls(_desktop), SP_TYPE_CTRL, "anchor", (GtkAnchorType) anchor, "size", (gdouble) pixbuf->get_width(), "shape", SP_CTRL_SHAPE_BITMAP, "pixbuf", pixbuf->gobj(), "filled", TRUE, "fill_color", _cset->normal.fill, "stroked", TRUE, "stroke_color", _cset->normal.stroke, "mode", SP_CTRL_MODE_XOR, NULL); _commonInit(); } ControlPoint::~ControlPoint() { // avoid storing invalid points in mouseovered_point if (this == mouseovered_point) { _clearMouseover(); } g_signal_handler_disconnect(G_OBJECT(_canvas_item), _event_handler_connection); //sp_canvas_item_hide(_canvas_item); gtk_object_destroy(_canvas_item); } void ControlPoint::_commonInit() { SP_CTRL(_canvas_item)->moveto(_position); _event_handler_connection = g_signal_connect(G_OBJECT(_canvas_item), "event", G_CALLBACK(_event_handler), this); } /** Relocate the control point without side effects. * Overload this method only if there is an additional graphical representation * that must be updated (like the lines that connect handles to nodes). If you override it, * you must also call the superclass implementation of the method. * @todo Investigate whether this method should be protected */ void ControlPoint::setPosition(Geom::Point const &pos) { _position = pos; SP_CTRL(_canvas_item)->moveto(pos); } /** Move the control point to new position with side effects. * This is called after each drag. Override this method if only some positions make sense * for a control point (like a point that must always be on a path and can't modify it), * or when moving a control point changes the positions of other points. */ void ControlPoint::move(Geom::Point const &pos) { setPosition(pos); } /** Apply an arbitrary affine transformation to a control point. This is used * by ControlPointSelection, and is important for things like nodes with handles. * The default implementation simply moves the point according to the transform. */ void ControlPoint::transform(Geom::Affine const &m) { move(position() * m); } bool ControlPoint::visible() const { return sp_canvas_item_is_visible(_canvas_item); } /** Set the visibility of the control point. An invisible point is not drawn on the canvas * and cannot receive any events. If you want to have an invisible point that can respond * to events, use invisible_cset as its color set. */ void ControlPoint::setVisible(bool v) { if (v) sp_canvas_item_show(_canvas_item); else sp_canvas_item_hide(_canvas_item); } Glib::ustring ControlPoint::format_tip(char const *format, ...) { va_list args; va_start(args, format); char *dyntip = g_strdup_vprintf(format, args); va_end(args); Glib::ustring ret = dyntip; g_free(dyntip); return ret; } unsigned int ControlPoint::_size() const { double ret; g_object_get(_canvas_item, "size", &ret, NULL); return static_cast(ret); } SPCtrlShapeType ControlPoint::_shape() const { SPCtrlShapeType ret; g_object_get(_canvas_item, "shape", &ret, NULL); return ret; } GtkAnchorType ControlPoint::_anchor() const { GtkAnchorType ret; g_object_get(_canvas_item, "anchor", &ret, NULL); return ret; } Glib::RefPtr ControlPoint::_pixbuf() { GdkPixbuf *ret; g_object_get(_canvas_item, "pixbuf", &ret, NULL); return Glib::wrap(ret); } // Same for setters. void ControlPoint::_setSize(unsigned int size) { g_object_set(_canvas_item, "size", (gdouble) size, NULL); } void ControlPoint::_setShape(SPCtrlShapeType shape) { g_object_set(_canvas_item, "shape", shape, NULL); } void ControlPoint::_setAnchor(GtkAnchorType anchor) { g_object_set(_canvas_item, "anchor", anchor, NULL); } void ControlPoint::_setPixbuf(Glib::RefPtr p) { g_object_set(_canvas_item, "pixbuf", Glib::unwrap(p), NULL); } // re-routes events into the virtual function int ControlPoint::_event_handler(SPCanvasItem */*item*/, GdkEvent *event, ControlPoint *point) { return point->_eventHandler(event) ? TRUE : FALSE; } // main event callback, which emits all other callbacks. bool ControlPoint::_eventHandler(GdkEvent *event) { // NOTE the static variables below are shared for all points! // TODO handle clicks and drags from other buttons too // offset from the pointer hotspot to the center of the grabbed knot in desktop coords static Geom::Point pointer_offset; // number of last doubleclicked button static unsigned next_release_doubleclick = 0; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); switch(event->type) { case GDK_BUTTON_PRESS: next_release_doubleclick = 0; if (event->button.button == 1) { // 1st mouse button click. internally, start dragging, but do not emit signals // or change position until drag tolerance is exceeded. _drag_event_origin[Geom::X] = event->button.x; _drag_event_origin[Geom::Y] = event->button.y; pointer_offset = _position - _desktop->w2d(_drag_event_origin); _drag_initiated = false; // route all events to this handler sp_canvas_item_grab(_canvas_item, _grab_event_mask, NULL, event->button.time); _event_grab = true; _setState(STATE_CLICKED); return true; } return _event_grab; case GDK_2BUTTON_PRESS: // store the button number for next release next_release_doubleclick = event->button.button; return true; case GDK_MOTION_NOTIFY: combine_motion_events(_desktop->canvas, event->motion, 0); if (_event_grab && !_desktop->event_context->space_panning) { _desktop->snapindicator->remove_snaptarget(); bool transferred = false; if (!_drag_initiated) { bool t = fabs(event->motion.x - _drag_event_origin[Geom::X]) <= drag_tolerance && fabs(event->motion.y - _drag_event_origin[Geom::Y]) <= drag_tolerance; if (t) return true; // if we are here, it means the tolerance was just exceeded. _drag_origin = _position; transferred = grabbed(&event->motion); // _drag_initiated might change during the above virtual call if (!_drag_initiated) { // this guarantees smooth redraws while dragging sp_canvas_force_full_redraw_after_interruptions(_desktop->canvas, 5); _drag_initiated = true; } } if (!transferred) { // dragging in progress Geom::Point new_pos = _desktop->w2d(event_point(event->motion)) + pointer_offset; // the new position is passed by reference and can be changed in the handlers. dragged(new_pos, &event->motion); move(new_pos); _updateDragTip(&event->motion); // update dragging tip after moving to new position _desktop->scroll_to_point(new_pos); _desktop->set_coordinate_status(_position); sp_event_context_snap_delay_handler(_desktop->event_context, NULL, (gpointer) this, &event->motion, DelayedSnapEvent::CONTROL_POINT_HANDLER); } return true; } break; case GDK_BUTTON_RELEASE: if (_event_grab && event->button.button == 1) { // If we have any pending snap event, then invoke it now! // (This is needed because we might not have snapped on the latest GDK_MOTION_NOTIFY event // if the mouse speed was too high. This is inherent to the snap-delay mechanism. // We must snap at some point in time though, and this is our last chance) // PS: For other contexts this is handled already in sp_event_context_item_handler or // sp_event_context_root_handler if (_desktop->event_context->_delayed_snap_event) { sp_event_context_snap_watchdog_callback(_desktop->event_context->_delayed_snap_event); } sp_canvas_item_ungrab(_canvas_item, event->button.time); _setMouseover(this, event->button.state); _event_grab = false; if (_drag_initiated) { sp_canvas_end_forced_full_redraws(_desktop->canvas); } if (_drag_initiated) { // it is the end of a drag _drag_initiated = false; ungrabbed(&event->button); return true; } else { // it is the end of a click if (next_release_doubleclick) { return doubleclicked(&event->button); } else { return clicked(&event->button); } } } break; case GDK_ENTER_NOTIFY: _setMouseover(this, event->crossing.state); return true; case GDK_LEAVE_NOTIFY: _clearMouseover(); return true; case GDK_GRAB_BROKEN: if (_event_grab && !event->grab_broken.keyboard) { { ungrabbed(NULL); if (_drag_initiated) sp_canvas_end_forced_full_redraws(_desktop->canvas); } _setState(STATE_NORMAL); _event_grab = false; _drag_initiated = false; return true; } break; // update tips on modifier state change // TODO add ESC keybinding as drag cancel case GDK_KEY_PRESS: switch (get_group0_keyval(&event->key)) { case GDK_Tab: {// Downcast from ControlPoint to TransformHandle, if possible // This is an ugly hack; we should have the transform handle intercept the keystrokes itself TransformHandle *th = dynamic_cast(this); if (th) { th->getNextClosestPoint(false); return true; } break; } case GDK_ISO_Left_Tab: {// Downcast from ControlPoint to TransformHandle, if possible // This is an ugly hack; we should have the transform handle intercept the keystrokes itself TransformHandle *th = dynamic_cast(this); if (th) { th->getNextClosestPoint(true); return true; } break; } default: break; } // Do not break here, to allow for updating tooltips and such case GDK_KEY_RELEASE: if (mouseovered_point != this) return false; if (_drag_initiated) { return true; // this prevents the tool from overwriting the drag tip } else { unsigned state = state_after_event(event); if (state != event->key.state) { // we need to return true if there was a tip available, otherwise the tool's // handler will process this event and set the tool's message, overwriting // the point's message return _updateTip(state); } } break; default: break; } // do not propagate events during grab - it might cause problems return _event_grab; } void ControlPoint::_setMouseover(ControlPoint *p, unsigned state) { bool visible = p->visible(); if (visible) { // invisible points shouldn't get mouseovered p->_setState(STATE_MOUSEOVER); } p->_updateTip(state); if (visible && mouseovered_point != p) { mouseovered_point = p; signal_mouseover_change.emit(mouseovered_point); } } bool ControlPoint::_updateTip(unsigned state) { Glib::ustring tip = _getTip(state); if (!tip.empty()) { _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, tip.data()); return true; } else { _desktop->event_context->defaultMessageContext()->clear(); return false; } } bool ControlPoint::_updateDragTip(GdkEventMotion *event) { if (!_hasDragTips()) return false; Glib::ustring tip = _getDragTip(event); if (!tip.empty()) { _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, tip.data()); return true; } else { _desktop->event_context->defaultMessageContext()->clear(); return false; } } void ControlPoint::_clearMouseover() { if (mouseovered_point) { mouseovered_point->_desktop->event_context->defaultMessageContext()->clear(); mouseovered_point->_setState(STATE_NORMAL); mouseovered_point = 0; signal_mouseover_change.emit(mouseovered_point); } } /** Transfer the grab to another point. This method allows one to create a draggable point * that should be dragged instead of the one that received the grabbed signal. * This is used to implement dragging out handles in the new node tool, for example. * * This method will NOT emit the ungrab signal of @c prev_point, because this would complicate * using it with selectable control points. If you use this method while dragging, you must emit * the ungrab signal yourself. * * Note that this will break horribly if you try to transfer grab between points in different * desktops, which doesn't make much sense anyway. */ void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event) { if (!_event_grab) return; grabbed(event); sp_canvas_item_ungrab(prev_point->_canvas_item, event->time); sp_canvas_item_grab(_canvas_item, _grab_event_mask, NULL, event->time); if (!_drag_initiated) { sp_canvas_force_full_redraw_after_interruptions(_desktop->canvas, 5); _drag_initiated = true; } prev_point->_setState(STATE_NORMAL); _setMouseover(this, event->state); } /** * Change the state of the knot. * Alters the appearance of the knot to match one of the states: normal, mouseover * or clicked. */ void ControlPoint::_setState(State state) { ColorEntry current = {0, 0}; switch(state) { case STATE_NORMAL: current = _cset->normal; break; case STATE_MOUSEOVER: current = _cset->mouseover; break; case STATE_CLICKED: current = _cset->clicked; break; }; _setColors(current); _state = state; } void ControlPoint::_setColors(ColorEntry colors) { g_object_set(_canvas_item, "fill_color", colors.fill, "stroke_color", colors.stroke, NULL); } // dummy implementations for handlers // they are here to avoid unused param warnings bool ControlPoint::grabbed(GdkEventMotion *) { return false; } void ControlPoint::dragged(Geom::Point &, GdkEventMotion *) {} void ControlPoint::ungrabbed(GdkEventButton *) {} bool ControlPoint::clicked(GdkEventButton *) { return false; } bool ControlPoint::doubleclicked(GdkEventButton *) { return false; } } // namespace UI } // namespace Inkscape /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :