// SPDX-License-Identifier: GPL-2.0-or-later /* * SPKnot implementation * * Authors: * Lauris Kaplinski * bulia byak * Abhishek Sharma * * Copyright (C) 1999-2005 authors * Copyright (C) 2001-2002 Ximian, Inc. * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #ifdef HAVE_CONFIG_H #endif #include #include #include "display/sodipodi-ctrl.h" #include "desktop.h" #include "knot.h" #include "knot-ptr.h" #include "document.h" #include "document-undo.h" #include "message-stack.h" #include "message-context.h" #include "ui/tools/node-tool.h" #include using Inkscape::DocumentUndo; #define KNOT_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) const gchar *nograbenv = getenv("INKSCAPE_NO_GRAB"); static bool nograb = (nograbenv && *nograbenv && (*nograbenv != '0')); static bool grabbed = false; static bool moved = false; static gint xp = 0, yp = 0; // where drag started static gint tolerance = 0; static bool within_tolerance = false; static bool transform_escaped = false; // true iff resize or rotate was cancelled by esc. void knot_ref(SPKnot* knot) { knot->ref_count++; } void knot_unref(SPKnot* knot) { if (--knot->ref_count < 1) { delete knot; } } static int sp_knot_handler(SPCanvasItem *item, GdkEvent *event, SPKnot *knot); SPKnot::SPKnot(SPDesktop *desktop, gchar const *tip) : ref_count(1) { this->desktop = nullptr; this->item = nullptr; this->owner = nullptr; this->flags = 0; this->size = 8; this->angle = 0; this->pos = Geom::Point(0, 0); this->grabbed_rel_pos = Geom::Point(0, 0); this->anchor = SP_ANCHOR_CENTER; this->shape = SP_KNOT_SHAPE_SQUARE; this->mode = SP_KNOT_MODE_XOR; this->tip = nullptr; this->_event_handler_id = 0; this->pressure = 0; this->fill[SP_KNOT_STATE_NORMAL] = 0xffffff00; this->fill[SP_KNOT_STATE_MOUSEOVER] = 0xff0000ff; this->fill[SP_KNOT_STATE_DRAGGING] = 0xff0000ff; this->fill[SP_KNOT_STATE_SELECTED] = 0x0000ffff; this->stroke[SP_KNOT_STATE_NORMAL] = 0x01000000; this->stroke[SP_KNOT_STATE_MOUSEOVER] = 0x01000000; this->stroke[SP_KNOT_STATE_DRAGGING] = 0x01000000; this->stroke[SP_KNOT_STATE_SELECTED] = 0x01000000; this->image[SP_KNOT_STATE_NORMAL] = nullptr; this->image[SP_KNOT_STATE_MOUSEOVER] = nullptr; this->image[SP_KNOT_STATE_DRAGGING] = nullptr; this->image[SP_KNOT_STATE_SELECTED] = nullptr; this->cursor[SP_KNOT_STATE_NORMAL] = nullptr; this->cursor[SP_KNOT_STATE_MOUSEOVER] = nullptr; this->cursor[SP_KNOT_STATE_DRAGGING] = nullptr; this->cursor[SP_KNOT_STATE_SELECTED] = nullptr; this->saved_cursor = nullptr; this->pixbuf = nullptr; this->desktop = desktop; this->flags = SP_KNOT_VISIBLE; if (tip) { this->tip = g_strdup (tip); } this->item = sp_canvas_item_new(desktop->getControls(), SP_TYPE_CTRL, "anchor", SP_ANCHOR_CENTER, "size", 9, "angle", 0.0, "filled", TRUE, "fill_color", 0xffffff00, "stroked", TRUE, "stroke_color", 0x01000000, "mode", SP_KNOT_MODE_XOR, NULL); this->_event_handler_id = g_signal_connect(G_OBJECT(this->item), "event", G_CALLBACK(sp_knot_handler), this); knot_created_callback(this); } SPKnot::~SPKnot() { auto display = gdk_display_get_default(); auto seat = gdk_display_get_default_seat(display); auto device = gdk_seat_get_pointer(seat); if ((this->flags & SP_KNOT_GRABBED) && gdk_display_device_is_grabbed(display, device)) { // This happens e.g. when deleting a node in node tool while dragging it gdk_seat_ungrab(seat); } if (this->_event_handler_id > 0) { g_signal_handler_disconnect(G_OBJECT (this->item), this->_event_handler_id); this->_event_handler_id = 0; } if (this->item) { sp_canvas_item_destroy(this->item); this->item = nullptr; } for (auto & i : this->cursor) { if (i) { g_object_unref(i); i = nullptr; } } if (this->tip) { g_free(this->tip); this->tip = nullptr; } // FIXME: cannot snap to destroyed knot (lp:1309050) //sp_event_context_discard_delayed_snap_event(this->desktop->event_context); knot_deleted_callback(this); } void SPKnot::startDragging(Geom::Point const &p, gint x, gint y, guint32 etime) { // save drag origin xp = x; yp = y; within_tolerance = true; this->grabbed_rel_pos = p - this->pos; this->drag_origin = this->pos; if (!nograb) { sp_canvas_item_grab(this->item, KNOT_EVENT_MASK, this->cursor[SP_KNOT_STATE_DRAGGING], etime); } this->setFlag(SP_KNOT_GRABBED, TRUE); grabbed = TRUE; } void SPKnot::selectKnot(bool select){ setFlag(SP_KNOT_SELECTED, select); } /** * Called to handle events on knots. */ static int sp_knot_handler(SPCanvasItem */*item*/, GdkEvent *event, SPKnot *knot) { g_assert(knot != nullptr); g_assert(SP_IS_KNOT(knot)); /* Run client universal event handler, if present */ bool consumed = knot->event_signal.emit(knot, event); if (consumed) { return true; } bool key_press_event_unconsumed = FALSE; knot_ref(knot); Inkscape::Preferences *prefs = Inkscape::Preferences::get(); tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); switch (event->type) { case GDK_2BUTTON_PRESS: if (event->button.button == 1) { knot->doubleclicked_signal.emit(knot, event->button.state); grabbed = FALSE; moved = FALSE; consumed = TRUE; } break; case GDK_BUTTON_PRESS: if ((event->button.button == 1) && knot->desktop && knot->desktop->event_context && !knot->desktop->event_context->space_panning) { Geom::Point const p = knot->desktop->w2d(Geom::Point(event->button.x, event->button.y)); knot->startDragging(p, (gint) event->button.x, (gint) event->button.y, event->button.time); knot->mousedown_signal.emit(knot, event->button.state); consumed = TRUE; } break; case GDK_BUTTON_RELEASE: if (event->button.button == 1 && knot->desktop && knot->desktop->event_context && !knot->desktop->event_context->space_panning) { // If we have any pending snap event, then invoke it now if (knot->desktop->event_context->_delayed_snap_event) { sp_event_context_snap_watchdog_callback(knot->desktop->event_context->_delayed_snap_event); } sp_event_context_discard_delayed_snap_event(knot->desktop->event_context); knot->pressure = 0; if (transform_escaped) { transform_escaped = false; consumed = TRUE; } else { knot->setFlag(SP_KNOT_GRABBED, FALSE); if (!nograb) { sp_canvas_item_ungrab(knot->item); } if (moved) { knot->setFlag(SP_KNOT_DRAGGING, FALSE); knot->ungrabbed_signal.emit(knot, event->button.state); } else { knot->click_signal.emit(knot, event->button.state); } grabbed = FALSE; moved = FALSE; consumed = TRUE; } } Inkscape::UI::Tools::sp_update_helperpath(); break; case GDK_MOTION_NOTIFY: if (!(event->motion.state & GDK_BUTTON1_MASK) && knot->flags & SP_KNOT_DRAGGING) { // If we have any pending snap event, then invoke it now if (knot->desktop->event_context->_delayed_snap_event) { sp_event_context_snap_watchdog_callback(knot->desktop->event_context->_delayed_snap_event); } sp_event_context_discard_delayed_snap_event(knot->desktop->event_context); knot->pressure = 0; if (transform_escaped) { transform_escaped = false; consumed = TRUE; } else { knot->setFlag(SP_KNOT_GRABBED, FALSE); if (!nograb) { sp_canvas_item_ungrab(knot->item); } if (moved) { knot->setFlag(SP_KNOT_DRAGGING, FALSE); knot->ungrabbed_signal.emit(knot, event->motion.state); } else { knot->click_signal.emit(knot, event->motion.state); } grabbed = FALSE; moved = FALSE; consumed = TRUE; Inkscape::UI::Tools::sp_update_helperpath(); } } else if (grabbed && knot->desktop && knot->desktop->event_context && !knot->desktop->event_context->space_panning) { consumed = TRUE; if ( within_tolerance && ( abs( (gint) event->motion.x - xp ) < tolerance ) && ( abs( (gint) event->motion.y - yp ) < tolerance ) ) { break; // do not drag if we're within tolerance from origin } // Once the user has moved farther than tolerance from the original location // (indicating they intend to move the object, not click), then always process the // motion notify coordinates as given (no snapping back to origin) within_tolerance = false; if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &knot->pressure)) { knot->pressure = CLAMP (knot->pressure, 0, 1); } else { knot->pressure = 0.5; } if (!moved) { knot->setFlag(SP_KNOT_DRAGGING, TRUE); knot->grabbed_signal.emit(knot, event->button.state); } sp_event_context_snap_delay_handler(knot->desktop->event_context, nullptr, knot, (GdkEventMotion *)event, Inkscape::UI::Tools::DelayedSnapEvent::KNOT_HANDLER); sp_knot_handler_request_position(event, knot); moved = TRUE; } break; case GDK_ENTER_NOTIFY: knot->setFlag(SP_KNOT_MOUSEOVER, TRUE); knot->setFlag(SP_KNOT_GRABBED, FALSE); if (knot->tip && knot->desktop && knot->desktop->event_context) { knot->desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, knot->tip); } grabbed = FALSE; moved = FALSE; consumed = TRUE; break; case GDK_LEAVE_NOTIFY: knot->setFlag(SP_KNOT_MOUSEOVER, FALSE); knot->setFlag(SP_KNOT_GRABBED, FALSE); if (knot->tip && knot->desktop && knot->desktop->event_context) { knot->desktop->event_context->defaultMessageContext()->clear(); } grabbed = FALSE; moved = FALSE; consumed = TRUE; break; case GDK_KEY_PRESS: // keybindings for knot switch (Inkscape::UI::Tools::get_latin_keyval(&event->key)) { case GDK_KEY_Escape: knot->setFlag(SP_KNOT_GRABBED, FALSE); if (!nograb) { sp_canvas_item_ungrab(knot->item); } if (moved) { knot->setFlag(SP_KNOT_DRAGGING, FALSE); knot->ungrabbed_signal.emit(knot, event->button.state); DocumentUndo::undo(knot->desktop->getDocument()); knot->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Node or handle drag canceled.")); transform_escaped = true; consumed = TRUE; } grabbed = FALSE; moved = FALSE; sp_event_context_discard_delayed_snap_event(knot->desktop->event_context); break; default: consumed = FALSE; key_press_event_unconsumed = TRUE; break; } break; default: break; } knot_unref(knot); if (key_press_event_unconsumed) { return false; // e.g. in case "%" was pressed to toggle snapping, or Q for quick zoom (while dragging a handle) } else { return consumed || grabbed; } } void sp_knot_handler_request_position(GdkEvent *event, SPKnot *knot) { Geom::Point const motion_w(event->motion.x, event->motion.y); Geom::Point const motion_dt = knot->desktop->w2d(motion_w); Geom::Point p = motion_dt - knot->grabbed_rel_pos; knot->requestPosition(p, event->motion.state); knot->desktop->scroll_to_point (motion_dt); knot->desktop->set_coordinate_status(knot->pos); // display the coordinate of knot, not cursor - they may be different! if (event->motion.state & GDK_BUTTON1_MASK) { Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK); } } void SPKnot::show() { this->setFlag(SP_KNOT_VISIBLE, TRUE); } void SPKnot::hide() { this->setFlag(SP_KNOT_VISIBLE, FALSE); } void SPKnot::requestPosition(Geom::Point const &p, guint state) { bool done = this->request_signal.emit(this, &const_cast(p), state); /* If user did not complete, we simply move knot to new position */ if (!done) { this->setPosition(p, state); } } void SPKnot::setPosition(Geom::Point const &p, guint state) { this->pos = p; if (this->item) { SP_CTRL(this->item)->moveto(p); } this->moved_signal.emit(this, p, state); } void SPKnot::moveto(Geom::Point const &p) { this->pos = p; if (this->item) { SP_CTRL(this->item)->moveto(p); } } Geom::Point SPKnot::position() const { return this->pos; } void SPKnot::setFlag(guint flag, bool set) { if (set) { this->flags |= flag; } else { this->flags &= ~flag; } switch (flag) { case SP_KNOT_VISIBLE: if (set) { sp_canvas_item_show(this->item); } else { sp_canvas_item_hide(this->item); } break; case SP_KNOT_MOUSEOVER: case SP_KNOT_DRAGGING: case SP_KNOT_SELECTED: this->_setCtrlState(); break; case SP_KNOT_GRABBED: break; default: g_assert_not_reached(); break; } } void SPKnot::updateCtrl() { if (!this->item) { return; } g_object_set(this->item, "shape", this->shape, NULL); g_object_set(this->item, "mode", this->mode, NULL); g_object_set(this->item, "size", this->size, NULL); g_object_set(this->item, "angle", this->angle, NULL); g_object_set(this->item, "anchor", this->anchor, NULL); if (this->pixbuf) { g_object_set(this->item, "pixbuf", this->pixbuf, NULL); } this->_setCtrlState(); } void SPKnot::_setCtrlState() { int state = SP_KNOT_STATE_NORMAL; if (this->flags & SP_KNOT_DRAGGING) { state = SP_KNOT_STATE_DRAGGING; } else if (this->flags & SP_KNOT_MOUSEOVER) { state = SP_KNOT_STATE_MOUSEOVER; } else if (this->flags & SP_KNOT_SELECTED) { state = SP_KNOT_STATE_SELECTED; } g_object_set(this->item, "fill_color", this->fill[state], NULL); g_object_set(this->item, "stroke_color", this->stroke[state], NULL); } void SPKnot::setSize(guint i) { size = i; } void SPKnot::setShape(guint i) { shape = (SPKnotShapeType) i; } void SPKnot::setAnchor(guint i) { anchor = (SPAnchorType) i; } void SPKnot::setMode(guint i) { mode = (SPKnotModeType) i; } void SPKnot::setPixbuf(gpointer p) { pixbuf = p; } void SPKnot::setAngle(double i) { angle = i; } void SPKnot::setFill(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected) { fill[SP_KNOT_STATE_NORMAL] = normal; fill[SP_KNOT_STATE_MOUSEOVER] = mouseover; fill[SP_KNOT_STATE_DRAGGING] = dragging; fill[SP_KNOT_STATE_SELECTED] = selected; } void SPKnot::setStroke(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected) { stroke[SP_KNOT_STATE_NORMAL] = normal; stroke[SP_KNOT_STATE_MOUSEOVER] = mouseover; stroke[SP_KNOT_STATE_DRAGGING] = dragging; stroke[SP_KNOT_STATE_SELECTED] = selected; } void SPKnot::setImage(guchar* normal, guchar* mouseover, guchar* dragging, guchar* selected) { image[SP_KNOT_STATE_NORMAL] = normal; image[SP_KNOT_STATE_MOUSEOVER] = mouseover; image[SP_KNOT_STATE_DRAGGING] = dragging; image[SP_KNOT_STATE_SELECTED] = selected; } void SPKnot::setCursor(GdkCursor* normal, GdkCursor* mouseover, GdkCursor* dragging, GdkCursor* selected) { if (cursor[SP_KNOT_STATE_NORMAL]) { g_object_unref(cursor[SP_KNOT_STATE_NORMAL]); } cursor[SP_KNOT_STATE_NORMAL] = normal; if (normal) { g_object_ref(normal); } if (cursor[SP_KNOT_STATE_MOUSEOVER]) { g_object_unref(cursor[SP_KNOT_STATE_MOUSEOVER]); } cursor[SP_KNOT_STATE_MOUSEOVER] = mouseover; if (mouseover) { g_object_ref(mouseover); } if (cursor[SP_KNOT_STATE_DRAGGING]) { g_object_unref(cursor[SP_KNOT_STATE_DRAGGING]); } cursor[SP_KNOT_STATE_DRAGGING] = dragging; if (dragging) { g_object_ref(dragging); } if (cursor[SP_KNOT_STATE_SELECTED]) { g_object_unref(cursor[SP_KNOT_STATE_SELECTED]); } cursor[SP_KNOT_STATE_SELECTED] = selected; if (selected) { g_object_ref(selected); } } /* 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 :