summaryrefslogtreecommitdiffstats
path: root/src/ui/knot
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/ui/knot
parentInitial commit. (diff)
downloadinkscape-upstream.tar.xz
inkscape-upstream.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/ui/knot')
-rw-r--r--src/ui/knot/README62
-rw-r--r--src/ui/knot/knot-enums.h49
-rw-r--r--src/ui/knot/knot-holder-entity.cpp633
-rw-r--r--src/ui/knot/knot-holder-entity.h254
-rw-r--r--src/ui/knot/knot-holder.cpp512
-rw-r--r--src/ui/knot/knot-holder.h123
-rw-r--r--src/ui/knot/knot-ptr.cpp34
-rw-r--r--src/ui/knot/knot-ptr.h17
-rw-r--r--src/ui/knot/knot.cpp515
-rw-r--r--src/ui/knot/knot.h208
10 files changed, 2407 insertions, 0 deletions
diff --git a/src/ui/knot/README b/src/ui/knot/README
new file mode 100644
index 0000000..7a76593
--- /dev/null
+++ b/src/ui/knot/README
@@ -0,0 +1,62 @@
+
+
+This directory contains code related to on-screen editing knots.
+
+Note that there are classes with similar functionality based on the ControlPoint class in src/ui/tool.
+
+Classes here:
+
+ * SPKnot A class that describes a knot (size, type, position, state, etc.) with some signals.
+
+ * KnotHolderEntity A class that has an SPKnot with some signals connections.
+
+ Derived classes:
+
+ LPEKnotHolderEntity
+ PatternKnotHolderEntity
+ HatchKnotHolderEntity
+ FilterKnotHolderEntity
+ RectKnotHolderEntityRX
+ RectKnotHolderEntityRY
+ RectKnotHolderEntityWH
+ RectKnotHolderEntityXY
+ RectKnotHolderEntityCenter
+ Box3DKnotHolderEntity
+ Box3DKnotHolderEntityCenter
+ ArcKnotHolderEntityStart
+ ArcKnotHolderEntityEnd
+ ArcKnotHolderEntityRX
+ ArcKnotHolderEntityRY
+ ArcKnotHolderEntityCenter
+ StarKnotHolderEntity1
+ StarKnotHolderEntity2
+ StarKnotHolderEntityCenter
+ SpiralKnotHolderEntityInner
+ SpiralKnotHolderEntityOuter
+ SpiralKnotHolderEntityCenter
+ OffsetKnotHolderEntity
+ TextKnotHolderEntityInlineSize
+ TextKnotHolderEntityShapeInside
+ FilletChamferKnotHolderEntity
+ TransformedPointParamKnotHolderEntity_Vector
+ PowerStrokePointArrayParamKnotHolderEntity
+ PowerStrokePointArrayParamKnotHolderEntity
+ PointParamKnotHolderEntity
+ VectorParamKnotHolderEntity_Origin
+ VectorParamKnotHolderEntity_Vector
+
+ And many classes derived from above!
+
+ * KnotHolder A class that has one or more overlapping knots via KnotHolderEntity's.
+
+ Derived classes:
+
+ ArcKnotHolder
+ Box3DKnotHolder
+ FlowtextKnotHolder
+ MiscKnotHolder
+ OffsetKnotHolder
+ RectKnotHolder
+ SpiralKnotHolder
+ StarKnotHolder
+ TextKnotHolder
diff --git a/src/ui/knot/knot-enums.h b/src/ui/knot/knot-enums.h
new file mode 100644
index 0000000..4f12cd7
--- /dev/null
+++ b/src/ui/knot/knot-enums.h
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_KNOT_ENUMS_H
+#define SEEN_KNOT_ENUMS_H
+
+/**
+ * @file
+ * Some enums used by SPKnot and by related types \& functions.
+ */
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 1999-2002 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+enum SPKnotStateType {
+ SP_KNOT_STATE_NORMAL,
+ SP_KNOT_STATE_MOUSEOVER,
+ SP_KNOT_STATE_DRAGGING,
+ SP_KNOT_STATE_SELECTED,
+ SP_KNOT_STATE_HIDDEN
+};
+
+#define SP_KNOT_VISIBLE_STATES 4
+
+enum {
+ SP_KNOT_VISIBLE = 1 << 0,
+ SP_KNOT_MOUSEOVER = 1 << 1,
+ SP_KNOT_DRAGGING = 1 << 2,
+ SP_KNOT_GRABBED = 1 << 3,
+ SP_KNOT_SELECTED = 1 << 4
+};
+
+
+#endif /* !SEEN_KNOT_ENUMS_H */
+
+/*
+ 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 :
diff --git a/src/ui/knot/knot-holder-entity.cpp b/src/ui/knot/knot-holder-entity.cpp
new file mode 100644
index 0000000..e75c628
--- /dev/null
+++ b/src/ui/knot/knot-holder-entity.cpp
@@ -0,0 +1,633 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * KnotHolderEntity definition.
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2001 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2001 Mitsuru Oka
+ * Copyright (C) 2004 Monash University
+ * Copyright (C) 2008 Maximilian Albert
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "knot-holder-entity.h"
+
+#include "desktop.h"
+#include "display/control/canvas-item-ctrl.h"
+#include "inkscape.h"
+#include "knot-holder.h"
+#include "live_effects/effect.h"
+#include "object/sp-hatch.h"
+#include "object/sp-item.h"
+#include "object/sp-marker.h"
+#include "object/sp-namedview.h"
+#include "object/sp-pattern.h"
+#include "object/filters/gaussian-blur.h"
+#include "preferences.h"
+#include "snap.h"
+#include "style.h"
+#include "object/sp-marker.h"
+
+#include "display/control/canvas-item-ctrl.h"
+#include <glibmm/i18n.h>
+
+void KnotHolderEntity::create(SPDesktop *desktop, SPItem *item, KnotHolder *parent,
+ Inkscape::CanvasItemCtrlType type,
+ Glib::ustring const & name,
+ const gchar *tip, guint32 color)
+{
+ if (!desktop) {
+ desktop = parent->getDesktop();
+ }
+
+ g_assert(item == parent->getItem());
+ g_assert(desktop && desktop == parent->getDesktop());
+ g_assert(knot == nullptr);
+
+ parent_holder = parent;
+ this->item = item; // TODO: remove the item either from here or from knotholder.cpp
+ this->desktop = desktop;
+
+ my_counter = KnotHolderEntity::counter++;
+
+ knot = new SPKnot(desktop, tip, type, name);
+ knot->fill [SP_KNOT_STATE_NORMAL] = color;
+ knot->ctrl->set_fill(color);
+ on_created();
+ update_knot();
+ knot->show();
+
+ _mousedown_connection = knot->mousedown_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_mousedown_handler));
+ _moved_connection = knot->moved_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_moved_handler));
+ _click_connection = knot->click_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_clicked_handler));
+ _ungrabbed_connection = knot->ungrabbed_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_ungrabbed_handler));
+}
+
+KnotHolderEntity::~KnotHolderEntity()
+{
+ _mousedown_connection.disconnect();
+ _moved_connection.disconnect();
+ _click_connection.disconnect();
+ _ungrabbed_connection.disconnect();
+
+ /* unref should call destroy */
+ if (knot) {
+ //g_object_unref(knot);
+ knot_unref(knot);
+ } else {
+ // FIXME: This shouldn't occur. Perhaps it is caused by LPE PointParams being knotholder entities, too
+ // If so, it will likely be fixed with upcoming refactoring efforts.
+ g_return_if_fail(knot);
+ }
+}
+
+void
+KnotHolderEntity::update_knot()
+{
+ Geom::Point knot_pos(knot_get());
+ if (knot_pos.isFinite()) {
+ Geom::Point dp(knot_pos * parent_holder->getEditTransform() * item->i2dt_affine());
+
+ _moved_connection.block();
+ knot->setPosition(dp, SP_KNOT_STATE_NORMAL);
+ _moved_connection.unblock();
+ } else {
+ // knot coords are non-finite, hide knot
+ knot->hide();
+ }
+}
+
+Geom::Point
+KnotHolderEntity::snap_knot_position(Geom::Point const &p, guint state)
+{
+ if (state & GDK_SHIFT_MASK) { // Don't snap when shift-key is held
+ return p;
+ }
+
+ Geom::Affine const i2dt (parent_holder->getEditTransform() * item->i2dt_affine());
+ Geom::Point s = p * i2dt;
+
+ if (!desktop) std::cerr << "No desktop" << std::endl;
+ if (!desktop->namedview) std::cerr << "No named view" << std::endl;
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop, true, item);
+ m.freeSnapReturnByRef(s, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ return s * i2dt.inverse();
+}
+
+Geom::Point
+KnotHolderEntity::snap_knot_position_constrained(Geom::Point const &p, Inkscape::Snapper::SnapConstraint const &constraint, guint state)
+{
+ if (state & GDK_SHIFT_MASK) { // Don't snap when shift-key is held
+ return p;
+ }
+
+ Geom::Affine const i2d (parent_holder->getEditTransform() * item->i2dt_affine());
+ Geom::Point s = p * i2d;
+
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop, true, item);
+
+ // constrainedSnap() will first project the point p onto the constraint line and then try to snap along that line.
+ // This way the constraint is already enforced, no need to worry about that later on
+ Inkscape::Snapper::SnapConstraint transformed_constraint = Inkscape::Snapper::SnapConstraint(constraint.getPoint() * i2d, (constraint.getPoint() + constraint.getDirection()) * i2d - constraint.getPoint() * i2d);
+ m.constrainedSnapReturnByRef(s, Inkscape::SNAPSOURCE_NODE_HANDLE, transformed_constraint);
+ m.unSetup();
+
+ return s * i2d.inverse();
+}
+
+void
+LPEKnotHolderEntity::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state)
+{
+ if (_effect) {
+ _effect->refresh_widgets = true;
+ _effect->makeUndoDone(_("Move handle"));
+ }
+}
+
+/* Pattern manipulation */
+
+void PatternKnotHolderEntity::on_created()
+{
+ // Setup an initial pattern transformation in the center
+ if (auto rect = item->documentGeometricBounds()) {
+ _cell = offset_to_cell(rect->midpoint());
+ }
+}
+
+/**
+ * Returns the position based on the pattern's origin, shifted by the percent x/y of it's size.
+ */
+Geom::Point PatternKnotHolderEntity::_get_pos(gdouble x, gdouble y, bool transform) const
+{
+ auto pat = _pattern();
+ auto pt = Geom::Point((_cell[Geom::X] + x) * pat->width(),
+ (_cell[Geom::Y] + y) * pat->height());
+ return transform ? pt * pat->getTransform() : pt;
+}
+
+bool PatternKnotHolderEntity::set_item_clickpos(Geom::Point loc)
+{
+ _cell = offset_to_cell(loc);
+ update_knot();
+ return true;
+}
+
+void PatternKnotHolderEntity::update_knot() {
+ KnotHolderEntity::update_knot();
+}
+
+Geom::IntPoint PatternKnotHolderEntity::offset_to_cell(Geom::Point loc) const {
+ auto pat = _pattern();
+
+ // 1. Turn the location into the pattern grid coordinate
+ auto scale = Geom::Scale(pat->width(), pat->height());
+ auto d2i = item->i2doc_affine().inverse();
+ auto i2p = pat->getTransform().inverse();
+
+ // Get grid index of nearest pattern repetition.
+ return (loc * d2i * i2p * scale.inverse()).floor();
+}
+
+
+SPPattern *PatternKnotHolderEntity::_pattern() const
+{
+ return _fill ? cast<SPPattern>(item->style->getFillPaintServer()) : cast<SPPattern>(item->style->getStrokePaintServer());
+}
+
+bool PatternKnotHolderEntity::knot_missing() const
+{
+ return !_pattern();
+}
+
+/* Pattern X/Y knot */
+
+void PatternKnotHolderEntityXY::on_created()
+{
+ PatternKnotHolderEntity::on_created();
+ // TODO: Move to constructor when desktop is generally available
+ _quad = make_canvasitem<Inkscape::CanvasItemQuad>(desktop->getCanvasControls());
+ _quad->lower_to_bottom();
+ _quad->set_fill(0x00000000);
+ _quad->set_stroke(0x808080ff);
+ _quad->set_inverted(true);
+ _quad->hide();
+}
+
+void PatternKnotHolderEntityXY::update_knot()
+{
+ PatternKnotHolderEntity::update_knot();
+ auto tr = item->i2dt_affine();
+ _quad->set_coords(_get_pos(0, 0) * tr, _get_pos(0, 1) * tr,
+ _get_pos(1, 1) * tr, _get_pos(1, 0) * tr);
+ _quad->show();
+}
+
+Geom::Point PatternKnotHolderEntityXY::knot_get() const
+{
+ return _get_pos(0, 0);
+}
+
+void
+PatternKnotHolderEntityXY::knot_set(Geom::Point const &p, Geom::Point const &origin, guint state)
+{
+ // FIXME: this snapping should be done together with knowing whether control was pressed. If GDK_CONTROL_MASK, then constrained snapping should be used.
+ Geom::Point p_snapped = snap_knot_position(p, state);
+
+ if ( state & GDK_CONTROL_MASK ) {
+ if (fabs((p - origin)[Geom::X]) > fabs((p - origin)[Geom::Y])) {
+ p_snapped[Geom::Y] = origin[Geom::Y];
+ } else {
+ p_snapped[Geom::X] = origin[Geom::X];
+ }
+ }
+
+ if (state) {
+ Geom::Point const q = p_snapped - knot_get();
+ item->adjust_pattern(Geom::Translate(q), false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/* Pattern Angle knot */
+
+Geom::Point PatternKnotHolderEntityAngle::knot_get() const
+{
+ return _get_pos(1.0, 0);
+}
+
+void
+PatternKnotHolderEntityAngle::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ // get the angle from pattern 0,0 to the cursor pos
+ Geom::Point transform_origin = _get_pos(0, 0);
+ gdouble theta = atan2(p - transform_origin);
+ gdouble theta_old = atan2(knot_get() - transform_origin);
+
+ if ( state & GDK_CONTROL_MASK ) {
+ /* Snap theta */
+ double snaps_radian = M_PI/snaps;
+ theta = std::round(theta/snaps_radian) * snaps_radian;
+ }
+
+ Geom::Affine rot = Geom::Translate(-transform_origin)
+ * Geom::Rotate(theta - theta_old)
+ * Geom::Translate(transform_origin);
+ item->adjust_pattern(rot, false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/* Pattern scale knot */
+
+Geom::Point PatternKnotHolderEntityScale::knot_get() const
+{
+ return _get_pos(1.0, 1.0);
+}
+
+/** Store pattern geometry info when the scale knot is first grabbed. */
+void PatternKnotHolderEntityScale::knot_grabbed(Geom::Point const &grab_pos, unsigned)
+{
+ _cached_transform = _pattern()->getTransform();
+ _cached_origin = _get_pos(0, 0);
+ _cached_inverse_linear = _cached_transform.withoutTranslation().inverse();
+ _cached_diagonal = (grab_pos - _cached_origin) * _cached_inverse_linear;
+
+ if (auto bounding_box = item->documentVisualBounds()) {
+ // Compare the areas of the pattern and the item to find the number of repetitions.
+ double const pattern_area = std::abs(_cached_diagonal[Geom::X] * _cached_diagonal[Geom::Y]);
+ double const item_area = bounding_box->area() * _cached_inverse_linear.descrim2() /
+ (item->i2doc_affine().descrim2() ?: 1e-3);
+ _cached_min_scale = std::sqrt(item_area / (pattern_area * MAX_REPETITIONS));
+ } else {
+ _cached_min_scale = 1e-6;
+ }
+}
+
+void
+PatternKnotHolderEntityScale::knot_set(Geom::Point const &p, Geom::Point const &, guint state)
+{
+ using namespace Geom;
+ // FIXME: this snapping should be done together with knowing whether control was pressed.
+ // If GDK_CONTROL_MASK, then constrained snapping should be used.
+ Point p_snapped = snap_knot_position(p, state);
+
+ Point const new_extent = (p_snapped - _cached_origin) * _cached_inverse_linear;
+
+ // 1. Calculate absolute scale factor first
+ double scale_x = std::clamp(new_extent[X] / _cached_diagonal[X], _cached_min_scale, 1e9);
+ double scale_y = std::clamp(new_extent[Y] / _cached_diagonal[Y], _cached_min_scale, 1e9);
+
+ Affine new_transform = (state & GDK_CONTROL_MASK) ? Scale(lerp(0.5, scale_x, scale_y))
+ : Scale(scale_x, scale_y);
+
+ // 2. Calculate offset to keep pattern origin aligned
+ new_transform *= _cached_transform;
+ auto const new_uncompensated_origin = _get_pos(0, 0, false) * new_transform;
+ new_transform *= Translate(_cached_origin - new_uncompensated_origin);
+
+ item->adjust_pattern(new_transform, true, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/* Hatch manipulation */
+bool HatchKnotHolderEntity::knot_missing() const
+{
+ SPHatch *hatch = _hatch();
+ return (hatch == nullptr);
+}
+
+SPHatch *HatchKnotHolderEntity::_hatch() const
+{
+ return _fill ? cast<SPHatch>(item->style->getFillPaintServer()) : cast<SPHatch>(item->style->getStrokePaintServer());
+}
+
+static Geom::Point sp_hatch_knot_get(SPHatch const *hatch, gdouble x, gdouble y)
+{
+ return Geom::Point(x, y) * hatch->hatchTransform();
+}
+
+Geom::Point HatchKnotHolderEntityXY::knot_get() const
+{
+ SPHatch *hatch = _hatch();
+ return sp_hatch_knot_get(hatch, 0, 0);
+}
+
+Geom::Point HatchKnotHolderEntityAngle::knot_get() const
+{
+ SPHatch *hatch = _hatch();
+ return sp_hatch_knot_get(hatch, hatch->pitch(), 0);
+}
+
+Geom::Point HatchKnotHolderEntityScale::knot_get() const
+{
+ SPHatch *hatch = _hatch();
+ return sp_hatch_knot_get(hatch, hatch->pitch(), hatch->pitch());
+}
+
+void HatchKnotHolderEntityXY::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ Geom::Point p_snapped = snap_knot_position(p, state);
+
+ if (state & GDK_CONTROL_MASK) {
+ if (fabs((p - origin)[Geom::X]) > fabs((p - origin)[Geom::Y])) {
+ p_snapped[Geom::Y] = origin[Geom::Y];
+ } else {
+ p_snapped[Geom::X] = origin[Geom::X];
+ }
+ }
+
+ if (state) {
+ Geom::Point const q = p_snapped - knot_get();
+ item->adjust_hatch(Geom::Translate(q), false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void HatchKnotHolderEntityAngle::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPHatch *hatch = _hatch();
+
+ // get the angle from hatch 0,0 to the cursor pos
+ Geom::Point transform_origin = sp_hatch_knot_get(hatch, 0, 0);
+ gdouble theta = atan2(p - transform_origin);
+ gdouble theta_old = atan2(knot_get() - transform_origin);
+
+ if (state & GDK_CONTROL_MASK) {
+ /* Snap theta */
+ double snaps_radian = M_PI/snaps;
+ theta = std::round(theta/snaps_radian) * snaps_radian;
+ }
+
+ Geom::Affine rot =
+ Geom::Translate(-transform_origin) * Geom::Rotate(theta - theta_old) * Geom::Translate(transform_origin);
+ item->adjust_hatch(rot, false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void HatchKnotHolderEntityScale::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ SPHatch *hatch = _hatch();
+
+ // FIXME: this snapping should be done together with knowing whether control was pressed.
+ // If GDK_CONTROL_MASK, then constrained snapping should be used.
+ Geom::Point p_snapped = snap_knot_position(p, state);
+
+ // Get the new scale from the position of the knotholder
+ Geom::Affine transform = hatch->hatchTransform();
+ Geom::Affine transform_inverse = transform.inverse();
+ Geom::Point d = p_snapped * transform_inverse;
+ Geom::Point d_origin = origin * transform_inverse;
+ Geom::Point origin_dt;
+ gdouble hatch_pitch = hatch->pitch();
+ if (state & GDK_CONTROL_MASK) {
+ // if ctrl is pressed: use 1:1 scaling
+ d = d_origin * (d.length() / d_origin.length());
+ }
+
+ Geom::Affine scale = Geom::Translate(-origin_dt) * Geom::Scale(d.x() / hatch_pitch, d.y() / hatch_pitch) *
+ Geom::Translate(origin_dt) * transform;
+
+ item->adjust_hatch(scale, true, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/* Filter visible size manipulation */
+void FilterKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ // FIXME: this snapping should be done together with knowing whether control was pressed. If GDK_CONTROL_MASK, then constrained snapping should be used.
+ Geom::Point p_snapped = snap_knot_position(p, state);
+
+ if ( state & GDK_CONTROL_MASK ) {
+ if (fabs((p - origin)[Geom::X]) > fabs((p - origin)[Geom::Y])) {
+ p_snapped[Geom::Y] = origin[Geom::Y];
+ } else {
+ p_snapped[Geom::X] = origin[Geom::X];
+ }
+ }
+
+ if (state) {
+ SPFilter *filter = (item->style) ? item->style->getFilter() : nullptr;
+ if(!filter) return;
+ Geom::OptRect orig_bbox = item->visualBounds();
+ std::unique_ptr<Geom::Rect> new_bbox(_topleft ? new Geom::Rect(p,orig_bbox->max()) : new Geom::Rect(orig_bbox->min(), p));
+
+ if (!filter->width._set) {
+ filter->width.set(SVGLength::PERCENT, 1.2);
+ }
+ if (!filter->height._set) {
+ filter->height.set(SVGLength::PERCENT, 1.2);
+ }
+ if (!filter->x._set) {
+ filter->x.set(SVGLength::PERCENT, -0.1);
+ }
+ if (!filter->y._set) {
+ filter->y.set(SVGLength::PERCENT, -0.1);
+ }
+
+ if(_topleft) {
+ float x_a = filter->width.computed;
+ float y_a = filter->height.computed;
+ filter->height.scale(new_bbox->height()/orig_bbox->height());
+ filter->width.scale(new_bbox->width()/orig_bbox->width());
+ float x_b = filter->width.computed;
+ float y_b = filter->height.computed;
+ filter->x.set(filter->x.unit, filter->x.computed + x_a - x_b);
+ filter->y.set(filter->y.unit, filter->y.computed + y_a - y_b);
+ } else {
+ filter->height.scale(new_bbox->height()/orig_bbox->height());
+ filter->width.scale(new_bbox->width()/orig_bbox->width());
+ }
+ filter->auto_region = false;
+ filter->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point FilterKnotHolderEntity::knot_get() const
+{
+ SPFilter *filter = (item->style) ? item->style->getFilter() : nullptr;
+ if(!filter) return Geom::Point(Geom::infinity(), Geom::infinity());
+ Geom::OptRect r = item->visualBounds();
+ if (_topleft) return Geom::Point(r->min());
+ else return Geom::Point(r->max());
+}
+
+/* Blur manipulation */
+
+void BlurKnotHolderEntity::on_created()
+{
+ KnotHolderEntity::on_created();
+ // TODO: Move to constructor when desktop is generally available
+
+ _line = make_canvasitem<Inkscape::CanvasItemCurve>(desktop->getCanvasControls());
+ _line->set_z_position(0);
+ _line->set_stroke(0x0033cccc);
+ _line->hide();
+
+ // This watcher makes sure that adding or removing a blur results in updated knots.
+ _watch_filter = item->style->signal_filter_changed.connect([=] (auto old_obj, auto obj) {
+ update_knot();
+ });
+}
+
+void BlurKnotHolderEntity::update_knot()
+{
+ auto blur = _blur();
+ if (blur) {
+ knot->show();
+ // This watcher makes sure anything outside that modifies the blur changes the knot.
+ _watch_blur = blur->connectModified([=](auto item, int flags) {
+ KnotHolderEntity::update_knot();
+ });
+
+ } else {
+ knot->hide();
+ _watch_blur.disconnect();
+ _line->hide();
+ }
+ KnotHolderEntity::update_knot();
+}
+
+
+
+/* Return the first blur primitive of any applied filter. */
+SPGaussianBlur *BlurKnotHolderEntity::_blur() const
+{
+ if (auto filter = item->style->getFilter()) {
+ for (auto &primitive : filter->children) {
+ if (auto blur = cast<SPGaussianBlur>(&primitive)) {
+ return blur;
+ }
+ }
+ }
+ return nullptr;
+}
+
+Geom::Point BlurKnotHolderEntity::_pos() const
+{
+ auto box = item->bbox(Geom::identity(), SPItem::VISUAL_BBOX);
+ if (_dir == Geom::Y) {
+ return Geom::Point(box->midpoint()[Geom::X], box->top());
+ }
+ return Geom::Point(box->right(), box->midpoint()[Geom::Y]);
+}
+
+Geom::Point BlurKnotHolderEntity::knot_get() const
+{
+ auto blur = _blur();
+ if (!blur)
+ return Geom::Point(0, 0);
+
+ // First let's find where the gradient is
+ auto tr = item->i2dt_affine();
+ auto dev = blur->get_std_deviation();
+
+ // Blur visibility is 2.4 times the deviation in that direction.
+ double x = dev.getNumber();
+ double y = dev.getOptNumber(true);
+
+ auto p0 = _pos();
+ auto p1 = p0 + Geom::Point(x * 2.4, 0);
+ if (_dir == Geom::Y) {
+ p1 = p0 - Geom::Point(0, y * 2.4);
+ }
+ _line->show();
+ _line->set_coords(p0 * tr, p1 * tr);
+
+ return p1;
+}
+void BlurKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, guint state)
+{
+ auto blur = _blur();
+ if (!blur)
+ return;
+
+ NumberOptNumber dev = blur->get_std_deviation();
+ auto dp = Geom::Point(dev.getNumber(), dev.getOptNumber(true));
+ auto val = std::max(0.0, (((p - _pos()) * Geom::Scale(1, -1))[_dir]) / 2.4);
+
+ if (state & GDK_CONTROL_MASK) {
+ if (state & GDK_SHIFT_MASK) {
+ dp[!_dir] *= (val / dp[_dir]);
+ } else {
+ dp[!_dir] = val;
+ }
+ }
+ dp[_dir] = val;
+
+ // When X is set to zero the Opt blur disapears
+ dev.setNumber(std::max(0.001, dp[Geom::X]));
+ dev.setOptNumber(std::max(0.0, dp[Geom::Y]));
+
+ blur->set_deviation(dev);
+}
+
+/*
+ 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 :
diff --git a/src/ui/knot/knot-holder-entity.h b/src/ui/knot/knot-holder-entity.h
new file mode 100644
index 0000000..62a0906
--- /dev/null
+++ b/src/ui/knot/knot-holder-entity.h
@@ -0,0 +1,254 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_KNOT_HOLDER_ENTITY_H
+#define SEEN_KNOT_HOLDER_ENTITY_H
+/*
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 1999-2001 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2001 Mitsuru Oka
+ * Copyright (C) 2004 Monash University
+ * Copyright (C) 2008 Maximilian Albert
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/forward.h>
+
+#include "knot.h"
+#include "snapper.h"
+
+#include "display/control/canvas-item-enums.h"
+#include "display/control/canvas-item-quad.h"
+#include "display/control/canvas-item-curve.h"
+#include "helper/auto-connection.h"
+#include "display/control/canvas-item-ptr.h"
+
+class SPHatch;
+class SPItem;
+class SPKnot;
+class SPDesktop;
+class SPPattern;
+class SPGaussianBlur;
+class KnotHolder;
+
+namespace Inkscape {
+namespace LivePathEffect {
+ class Effect;
+} // namespace LivePathEffect
+} // namespace Inkscape
+
+typedef void (* SPKnotHolderSetFunc) (SPItem *item, Geom::Point const &p, Geom::Point const &origin, unsigned int state);
+typedef Geom::Point (* SPKnotHolderGetFunc) (SPItem *item);
+
+/**
+ * KnotHolderEntity definition.
+ */
+class KnotHolderEntity {
+public:
+ KnotHolderEntity() {}
+ virtual ~KnotHolderEntity();
+
+ void create(SPDesktop *desktop, SPItem *item, KnotHolder *parent,
+ Inkscape::CanvasItemCtrlType type = Inkscape::CANVAS_ITEM_CTRL_TYPE_DEFAULT,
+ Glib::ustring const & name = Glib::ustring("unknown"),
+ char const *tip = "",
+ guint32 color = 0xffffff00);
+
+ /* the get/set/click handlers are virtual functions; each handler class for a knot
+ should be derived from KnotHolderEntity and override these functions */
+ virtual void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) = 0;
+ virtual void knot_grabbed(Geom::Point const &/*grab_position*/, unsigned /*state*/) {}
+ virtual void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, unsigned int state) = 0;
+ virtual bool knot_missing() const { return false; }
+ virtual Geom::Point knot_get() const = 0;
+ virtual void knot_click(unsigned int /*state*/) {}
+ virtual bool set_item_clickpos(Geom::Point loc) { return false; }
+
+ virtual void on_created() {}
+ virtual void update_knot();
+
+ // private:
+ Geom::Point snap_knot_position(Geom::Point const &p, unsigned int state);
+ Geom::Point snap_knot_position_constrained(Geom::Point const &p, Inkscape::Snapper::SnapConstraint const &constraint, unsigned int state);
+
+ SPKnot *knot = nullptr;
+ SPItem *item = nullptr;
+ SPDesktop *desktop = nullptr;
+ KnotHolder *parent_holder = nullptr;
+
+ int my_counter = 0;
+ inline static int counter = 0;
+
+ /** Connection to \a knot's "moved" signal. */
+ unsigned int handler_id = 0;
+ /** Connection to \a knot's "clicked" signal. */
+ unsigned int _click_handler_id = 0;
+ /** Connection to \a knot's "ungrabbed" signal. */
+ unsigned int _ungrab_handler_id = 0;
+
+private:
+ sigc::connection _mousedown_connection;
+ sigc::connection _moved_connection;
+ sigc::connection _click_connection;
+ sigc::connection _ungrabbed_connection;
+};
+
+// derived KnotHolderEntity class for LPEs
+class LPEKnotHolderEntity : public KnotHolderEntity {
+public:
+ LPEKnotHolderEntity(Inkscape::LivePathEffect::Effect *effect) : _effect(effect) {};
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override;
+protected:
+ Inkscape::LivePathEffect::Effect *_effect;
+};
+
+/* pattern manipulation */
+
+class PatternKnotHolderEntity : public KnotHolderEntity {
+ public:
+ PatternKnotHolderEntity(bool fill) : KnotHolderEntity(), _fill(fill) {}
+ void on_created() override;
+ bool knot_missing() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override{};
+ bool set_item_clickpos(Geom::Point loc) override;
+ void update_knot() override;
+
+protected:
+ // true if the entity tracks fill, false for stroke
+ bool _fill;
+ SPPattern *_pattern() const;
+ Geom::Point _get_pos(gdouble x, gdouble y, bool transform = true) const;
+ Geom::IntPoint offset_to_cell(Geom::Point loc) const;
+ Geom::IntPoint _cell;
+};
+
+class PatternKnotHolderEntityXY : public PatternKnotHolderEntity {
+public:
+ PatternKnotHolderEntityXY(bool fill) : PatternKnotHolderEntity(fill) {}
+ ~PatternKnotHolderEntityXY() override = default;
+
+ void on_created() override;
+ void update_knot() override;
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+
+private:
+ // Extra visual element to show the pattern editing area
+ CanvasItemPtr<Inkscape::CanvasItemQuad> _quad;
+};
+
+class PatternKnotHolderEntityAngle : public PatternKnotHolderEntity {
+public:
+ PatternKnotHolderEntityAngle(bool fill) : PatternKnotHolderEntity(fill) {}
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class PatternKnotHolderEntityScale : public PatternKnotHolderEntity {
+public:
+ PatternKnotHolderEntityScale(bool fill) : PatternKnotHolderEntity(fill) {}
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_grabbed(Geom::Point const &grab_pos, unsigned state) override;
+
+private:
+ /// Maximum number of pattern repetitons allowed in an item
+ inline static double const MAX_REPETITIONS = 1e6;
+ Geom::Affine _cached_transform, _cached_inverse_linear;
+ Geom::Point _cached_origin, _cached_diagonal;
+ double _cached_min_scale;
+};
+
+/* Hatch manipulation */
+class HatchKnotHolderEntity : public KnotHolderEntity {
+ public:
+ HatchKnotHolderEntity(bool fill)
+ : KnotHolderEntity()
+ , _fill(fill)
+ {
+ }
+ bool knot_missing() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override{};
+
+ protected:
+ // true if the entity tracks fill, false for stroke
+ bool _fill;
+ SPHatch *_hatch() const;
+};
+
+class HatchKnotHolderEntityXY : public HatchKnotHolderEntity {
+ public:
+ HatchKnotHolderEntityXY(bool fill)
+ : HatchKnotHolderEntity(fill)
+ {
+ }
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class HatchKnotHolderEntityAngle : public HatchKnotHolderEntity {
+ public:
+ HatchKnotHolderEntityAngle(bool fill)
+ : HatchKnotHolderEntity(fill)
+ {
+ }
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class HatchKnotHolderEntityScale : public HatchKnotHolderEntity {
+ public:
+ HatchKnotHolderEntityScale(bool fill)
+ : HatchKnotHolderEntity(fill)
+ {
+ }
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+
+/* Filter manipulation */
+class FilterKnotHolderEntity : public KnotHolderEntity {
+ public:
+ FilterKnotHolderEntity(bool topleft) : KnotHolderEntity(), _topleft(topleft) {}
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+private:
+ bool _topleft; // true for topleft point, false for bottomright
+};
+
+class BlurKnotHolderEntity : public KnotHolderEntity {
+ public:
+ BlurKnotHolderEntity(int direction) : KnotHolderEntity(), _dir(direction) {}
+ void on_created() override;
+ void update_knot() override;
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+
+ private:
+ SPGaussianBlur *_blur() const;
+ Geom::Point _pos() const;
+
+ int _dir;
+ CanvasItemPtr<Inkscape::CanvasItemCurve> _line;
+ Inkscape::auto_connection _watch_filter;
+ Inkscape::auto_connection _watch_blur;
+};
+
+#endif /* !SEEN_KNOT_HOLDER_ENTITY_H */
+
+/*
+ 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 :
diff --git a/src/ui/knot/knot-holder.cpp b/src/ui/knot/knot-holder.cpp
new file mode 100644
index 0000000..138d809
--- /dev/null
+++ b/src/ui/knot/knot-holder.cpp
@@ -0,0 +1,512 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Container for SPKnot visual handles.
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * bulia byak <buliabyak@users.sf.net>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2001-2008 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "knot-holder.h"
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "knot-holder-entity.h"
+#include "knot.h"
+
+#include "live_effects/effect.h"
+#include "live_effects/lpeobject.h"
+
+#include "object/box3d.h"
+#include "object/sp-ellipse.h"
+#include "object/sp-hatch.h"
+#include "object/sp-offset.h"
+#include "object/sp-pattern.h"
+#include "object/sp-rect.h"
+#include "object/sp-shape.h"
+#include "object/sp-spiral.h"
+#include "object/sp-star.h"
+#include "object/sp-marker.h"
+#include "object/filters/gaussian-blur.h"
+#include "style.h"
+
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+#include "ui/tools/arc-tool.h"
+#include "ui/tools/node-tool.h"
+#include "ui/tools/rect-tool.h"
+#include "ui/tools/spiral-tool.h"
+#include "ui/tools/tweak-tool.h"
+
+#include "display/control/snap-indicator.h"
+
+// TODO due to internal breakage in glibmm headers, this must be last:
+#include <glibmm/i18n.h>
+
+using Inkscape::DocumentUndo;
+
+class SPDesktop;
+
+KnotHolder::KnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ desktop(desktop),
+ item(item),
+ //XML Tree being used directly for item->getRepr() while it shouldn't be...
+ repr(item ? item->getRepr() : nullptr),
+ entity(),
+ released(relhandler),
+ local_change(FALSE),
+ dragging(false),
+ _edit_transform(Geom::identity())
+{
+ if (!desktop || !item) {
+ g_warning ("Error! Throw an exception, please!");
+ }
+
+ sp_object_ref(item);
+}
+
+KnotHolder::~KnotHolder() {
+ sp_object_unref(item);
+
+ for (auto & i : entity) {
+ delete i;
+ }
+ entity.clear(); // is this necessary?
+}
+
+void
+KnotHolder::setEditTransform(Geom::Affine edit_transform)
+{
+ _edit_transform = edit_transform;
+}
+
+void KnotHolder::update_knots()
+{
+ for (auto e = entity.begin(); e != entity.end(); ) {
+ // check if pattern was removed without deleting the knot
+ if ((*e)->knot_missing()) {
+ delete (*e);
+ e = entity.erase(e);
+ } else {
+ (*e)->update_knot();
+ ++e;
+ }
+ }
+}
+
+/**
+ * Returns true if at least one of the KnotHolderEntities has the mouse hovering above it.
+ */
+bool KnotHolder::knot_mouseover() const {
+ for (auto i : entity) {
+ const SPKnot *knot = i->knot;
+
+ if (knot && knot->is_mouseover()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Returns true if at least one of the KnotHolderEntities is selected
+ */
+bool KnotHolder::knot_selected() const {
+ for (auto i : entity) {
+ const SPKnot *knot = i->knot;
+
+ if (knot && knot->is_selected()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void
+KnotHolder::knot_mousedown_handler(SPKnot *knot, guint state)
+{
+ if (!(state & GDK_SHIFT_MASK)) {
+ unselect_knots();
+ }
+ for(auto e : this->entity) {
+ if (!(state & GDK_SHIFT_MASK)) {
+ e->knot->selectKnot(false);
+ }
+ if (e->knot == knot) {
+ if (!(e->knot->is_selected()) || !(state & GDK_SHIFT_MASK)){
+ e->knot->selectKnot(true);
+ } else {
+ e->knot->selectKnot(false);
+ }
+ }
+ }
+}
+
+void
+KnotHolder::knot_clicked_handler(SPKnot *knot, guint state)
+{
+ SPItem *saved_item = this->item;
+
+ for(auto e : this->entity) {
+ if (e->knot == knot)
+ // no need to test whether knot_click exists since it's virtual now
+ e->knot_click(state);
+ }
+
+ {
+ auto savedShape = cast<SPShape>(saved_item);
+ if (savedShape) {
+ savedShape->set_shape();
+ }
+ }
+
+ this->update_knots();
+
+ Glib::ustring icon_name;
+
+ // TODO extract duplicated blocks;
+ if (is<SPRect>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-rectangle");
+ } else if (is<SPBox3D>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-cuboid");
+ } else if (is<SPGenericEllipse>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-ellipse");
+ } else if (is<SPStar>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-polygon-star");
+ } else if (is<SPSpiral>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-spiral");
+ } else if (is<SPMarker>(saved_item)) {
+ icon_name = INKSCAPE_ICON("tool-pointer");
+ } else {
+ auto offset = cast<SPOffset>(saved_item);
+ if (offset) {
+ if (offset->sourceHref) {
+ icon_name = INKSCAPE_ICON("path-offset-linked");
+ } else {
+ icon_name = INKSCAPE_ICON("path-offset-dynamic");
+ }
+ }
+ }
+
+ // for drag, this is done by ungrabbed_handler, but for click we must do it here
+
+ if (saved_item && saved_item->document) { // increasingly aggressive sanity checks
+ DocumentUndo::done(saved_item->document, _("Change handle"), icon_name);
+ } else {
+ std::terminate();
+ }
+}
+
+void
+KnotHolder::transform_selected(Geom::Affine transform){
+ for (auto & i : entity) {
+ SPKnot *knot = i->knot;
+ if (knot->is_selected()) {
+ knot_moved_handler(knot, knot->pos * transform , 0);
+ knot->selectKnot(true);
+ }
+ }
+}
+
+void
+KnotHolder::unselect_knots(){
+ Inkscape::UI::Tools::NodeTool *nt = dynamic_cast<Inkscape::UI::Tools::NodeTool*>(desktop->event_context);
+ if (nt) {
+ for (auto &_shape_editor : nt->_shape_editors) {
+ Inkscape::UI::ShapeEditor *shape_editor = _shape_editor.second.get();
+ if (shape_editor && shape_editor->has_knotholder()) {
+ KnotHolder * knotholder = shape_editor->knotholder;
+ if (knotholder) {
+ for (auto e : knotholder->entity) {
+ if (e->knot->is_selected()) {
+ e->knot->selectKnot(false);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/** Notifies an entity that its knot has just been grabbed. */
+void KnotHolder::knot_grabbed_handler(SPKnot *knot, unsigned state)
+{
+ auto grab_entity = std::find_if(entity.begin(), entity.end(),
+ [=](KnotHolderEntity *khe) -> bool { return khe->knot == knot; });
+ if (grab_entity == entity.end()) {
+ return;
+ }
+ auto const item_origin = (*grab_entity)->knot->drag_origin * item->dt2i_affine()
+ * _edit_transform.inverse();
+ (*grab_entity)->knot_grabbed(item_origin, state);
+}
+
+void
+KnotHolder::knot_moved_handler(SPKnot *knot, Geom::Point const &p, guint state)
+{
+ if (!dragging) {
+ // The knot has just been grabbed
+ knot_grabbed_handler(knot, state);
+ dragging = true;
+ }
+
+ // this was a local change and the knotholder does not need to be recreated:
+ this->local_change = TRUE;
+
+ for(auto e : this->entity) {
+ if (e->knot == knot) {
+ Geom::Point const q = p * item->i2dt_affine().inverse() * _edit_transform.inverse();
+ e->knot_set(q, e->knot->drag_origin * item->i2dt_affine().inverse() * _edit_transform.inverse(), state);
+ break;
+ }
+ }
+
+ auto shape = cast<SPShape>(item);
+ if (shape) {
+ shape->set_shape();
+ }
+
+ this->update_knots();
+}
+
+void
+KnotHolder::knot_ungrabbed_handler(SPKnot *knot, guint state)
+{
+ this->dragging = false;
+ desktop->snapindicator->remove_snaptarget();
+
+ if (this->released) {
+ this->released(this->item);
+ } else {
+ // if a point is dragged while not selected, it should select itself,
+ // even if it was just unselected in the mousedown event handler.
+ if (!(knot->is_selected())) {
+ knot->selectKnot(true);
+ } else {
+ for(auto e : this->entity) {
+ if (e->knot == knot) {
+ e->knot_ungrabbed(e->knot->position(), e->knot->drag_origin * item->i2dt_affine().inverse() * _edit_transform.inverse(), state);
+ if (e->knot->is_lpe) {
+ return;
+ }
+ break;
+ }
+ }
+ }
+
+ SPObject *object = (SPObject *) this->item;
+
+ // Caution: this call involves a screen update, which may process events, and as a
+ // result the knotholder may be destructed. So, after the updateRepr, we cannot use any
+ // fields of this knotholder (such as this->item), but only values we have saved beforehand
+ // (such as object).
+ object->updateRepr();
+
+
+ SPFilter *filter = (object->style) ? object->style->getFilter() : nullptr;
+ if (filter) {
+ filter->updateRepr();
+ }
+ Glib::ustring icon_name;
+
+ // TODO extract duplicated blocks;
+ if (is<SPRect>(object)) {
+ icon_name = INKSCAPE_ICON("draw-rectangle");
+ } else if (is<SPBox3D>(object)) {
+ icon_name = INKSCAPE_ICON("draw-cuboid");
+ } else if (is<SPGenericEllipse>(object)) {
+ icon_name = INKSCAPE_ICON("draw-ellipse");
+ } else if (is<SPStar>(object)) {
+ icon_name = INKSCAPE_ICON("draw-polygon-star");
+ } else if (is<SPSpiral>(object)) {
+ icon_name = INKSCAPE_ICON("draw-spiral");
+ } else if (is<SPMarker>(object)) {
+ icon_name = INKSCAPE_ICON("tool-pointer");
+ } else {
+ auto offset = cast<SPOffset>(object);
+ if (offset) {
+ if (offset->sourceHref) {
+ icon_name = INKSCAPE_ICON("path-offset-linked");
+ } else {
+ icon_name = INKSCAPE_ICON("path-offset-dynamic");
+ }
+ }
+ }
+ DocumentUndo::done(object->document, _("Move handle"), icon_name);
+ }
+}
+
+void KnotHolder::add(KnotHolderEntity *e)
+{
+ // g_message("Adding a knot at %p", e);
+ entity.push_back(e);
+}
+
+void KnotHolder::add_pattern_knotholder()
+{
+ if (is<SPPattern>(item->style->getFillPaintServer())) {
+ auto entity_xy = new PatternKnotHolderEntityXY(true);
+ auto entity_angle = new PatternKnotHolderEntityAngle(true);
+ auto entity_scale = new PatternKnotHolderEntityScale(true);
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Pattern:Fill:xy",
+ // TRANSLATORS: This refers to the pattern that's inside the object
+ _("<b>Move</b> the pattern fill inside the object"));
+
+ entity_scale->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Pattern:Fill:scale",
+ _("<b>Scale</b> the pattern fill; uniformly if with <b>Ctrl</b>"));
+
+ entity_angle->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Pattern:Fill:angle",
+ _("<b>Rotate</b> the pattern fill; with <b>Ctrl</b> to snap angle"));
+
+ entity.push_back(entity_xy);
+ entity.push_back(entity_angle);
+ entity.push_back(entity_scale);
+ }
+
+ if (is<SPPattern>(item->style->getStrokePaintServer())) {
+ auto entity_xy = new PatternKnotHolderEntityXY(false);
+ auto entity_angle = new PatternKnotHolderEntityAngle(false);
+ auto entity_scale = new PatternKnotHolderEntityScale(false);
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Pattern:Stroke:xy",
+ // TRANSLATORS: This refers to the pattern that's inside the object
+ _("<b>Move</b> the stroke's pattern inside the object"));
+
+ entity_scale->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Pattern:Stroke:scale",
+ _("<b>Scale</b> the stroke's pattern; uniformly if with <b>Ctrl</b>"));
+
+ entity_angle->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Pattern:Stroke:angle",
+ _("<b>Rotate</b> the stroke's pattern; with <b>Ctrl</b> to snap angle"));
+
+ entity.push_back(entity_xy);
+ entity.push_back(entity_angle);
+ entity.push_back(entity_scale);
+ }
+
+ // watch patterns and update knots when they change
+ install_modification_watch();
+}
+
+void KnotHolder::add_hatch_knotholder()
+{
+ if ((item->style->fill.isPaintserver()) && cast<SPHatch>(item->style->getFillPaintServer())) {
+ HatchKnotHolderEntityXY *entity_xy = new HatchKnotHolderEntityXY(true);
+ HatchKnotHolderEntityAngle *entity_angle = new HatchKnotHolderEntityAngle(true);
+ HatchKnotHolderEntityScale *entity_scale = new HatchKnotHolderEntityScale(true);
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Hatch:Fill:xy",
+ // TRANSLATORS: This refers to the hatch that's inside the object
+ _("<b>Move</b> the hatch fill inside the object"));
+
+ entity_scale->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Hatch:Fill:scale",
+ _("<b>Scale</b> the hatch fill; uniformly if with <b>Ctrl</b>"));
+
+ entity_angle->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Hatch:Fill:angle",
+ _("<b>Rotate</b> the hatch fill; with <b>Ctrl</b> to snap angle"));
+
+ entity.push_back(entity_xy);
+ entity.push_back(entity_angle);
+ entity.push_back(entity_scale);
+ }
+
+ if ((item->style->stroke.isPaintserver()) && cast<SPHatch>(item->style->getStrokePaintServer())) {
+ HatchKnotHolderEntityXY *entity_xy = new HatchKnotHolderEntityXY(false);
+ HatchKnotHolderEntityAngle *entity_angle = new HatchKnotHolderEntityAngle(false);
+ HatchKnotHolderEntityScale *entity_scale = new HatchKnotHolderEntityScale(false);
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Hatch:Stroke:xy",
+ // TRANSLATORS: This refers to the pattern that's inside the object
+ _("<b>Move</b> the hatch stroke inside the object"));
+
+ entity_scale->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Hatch:Stroke:scale",
+ _("<b>Scale</b> the hatch stroke; uniformly if with <b>Ctrl</b>"));
+
+ entity_angle->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Hatch:Stroke:angle",
+ _("<b>Rotate</b> the hatch stroke; with <b>Ctrl</b> to snap angle"));
+
+ entity.push_back(entity_xy);
+ entity.push_back(entity_angle);
+ entity.push_back(entity_scale);
+ }
+}
+
+void KnotHolder::add_filter_knotholder() {
+ if (auto filter = item->style->getFilter()) {
+ if (!filter->auto_region) {
+ auto entity_tl = new FilterKnotHolderEntity(true);
+ auto entity_br = new FilterKnotHolderEntity(false);
+ entity_tl->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Filter:TopLeft",
+ _("<b>Resize</b> the filter effect region"));
+ entity_br->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Filter:BottomRight",
+ _("<b>Resize</b> the filter effect region"));
+ entity.push_back(entity_tl);
+ entity.push_back(entity_br);
+ }
+ }
+
+ // always install blur nodes, they default to disabled.
+ auto entity_x = new BlurKnotHolderEntity(Geom::X);
+ auto entity_y = new BlurKnotHolderEntity(Geom::Y);
+ entity_x->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Filter:BlurX",
+ _("<b>Drag</b> to <b>adjust</b> blur in x direction; <b>Ctrl</b>+<b>Drag</b> makes x equal to y; <b>Shift</b>+<b>Ctrl</b>+<b>Drag</b> scales blur proportionately "));
+ entity_y->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Filter:BlurY",
+ _("<b>Drag</b> to <b>adjust</b> blur in y direction; <b>Ctrl</b>+<b>Drag</b> makes y equal to x; <b>Shift</b>+<b>Ctrl</b>+<b>Drag</b> scales blur proportionately "));
+ entity.push_back(entity_x);
+ entity.push_back(entity_y);
+}
+
+/**
+ * When editing an object, this extra information tells out knots
+ * where the user has clicked on the item.
+ */
+bool KnotHolder::set_item_clickpos(Geom::Point loc)
+{
+ bool ret = false;
+ for (auto i : entity) {
+ ret = i->set_item_clickpos(loc) || ret;
+ }
+ return ret;
+}
+
+/**
+ * When object being edited has some attributes changed (fill, stroke)
+ * update what objects we watch
+ */
+void KnotHolder::install_modification_watch() {
+ g_assert(item);
+
+ if (auto pattern = cast<SPPattern>(item->style->getFillPaintServer())) {
+ _watch_fill = pattern->connectModified([=](SPObject*, unsigned int){
+ update_knots();
+ });
+ }
+ else {
+ _watch_fill.disconnect();
+ }
+
+ if (auto pattern = cast<SPPattern>(item->style->getStrokePaintServer())) {
+ _watch_stroke = pattern->connectModified([=](SPObject*, unsigned int){
+ update_knots();
+ });
+ }
+ else {
+ _watch_stroke.disconnect();
+ }
+}
+
+/*
+ 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 :
diff --git a/src/ui/knot/knot-holder.h b/src/ui/knot/knot-holder.h
new file mode 100644
index 0000000..0584182
--- /dev/null
+++ b/src/ui/knot/knot-holder.h
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_KNOTHOLDER_H
+#define SEEN_SP_KNOTHOLDER_H
+
+/*
+ * KnotHolder - Hold SPKnot list and manage signals
+ *
+ * Author:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 1999-2001 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2001 Mitsuru Oka
+ * Copyright (C) 2008 Maximilian Albert
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#include <2geom/forward.h>
+#include <2geom/affine.h>
+#include <list>
+#include <sigc++/connection.h>
+#include "helper/auto-connection.h"
+
+namespace Inkscape {
+namespace UI {
+class ShapeEditor;
+}
+namespace XML {
+class Node;
+}
+namespace LivePathEffect {
+class PowerStrokePointArrayParamKnotHolderEntity;
+class NodeSatelliteArrayParam;
+class FilletChamferKnotHolderEntity;
+}
+}
+
+class KnotHolderEntity;
+class SPItem;
+class SPDesktop;
+class SPKnot;
+
+/* fixme: Think how to make callbacks most sensitive (Lauris) */
+typedef void (* SPKnotHolderReleasedFunc) (SPItem *item);
+
+class KnotHolder {
+public:
+ KnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ virtual ~KnotHolder();
+
+ KnotHolder() = delete; // declared but not defined
+
+ void update_knots();
+ void unselect_knots();
+ void knot_mousedown_handler(SPKnot *knot, unsigned int state);
+ void knot_moved_handler(SPKnot *knot, Geom::Point const &p, unsigned int state);
+ void knot_clicked_handler(SPKnot *knot, unsigned int state);
+ void knot_grabbed_handler(SPKnot *knot, unsigned state);
+ void knot_ungrabbed_handler(SPKnot *knot, unsigned int state);
+ void transform_selected(Geom::Affine transform);
+ void add(KnotHolderEntity *e);
+
+ void add_pattern_knotholder();
+ void add_hatch_knotholder();
+ void add_filter_knotholder();
+
+ void setEditTransform(Geom::Affine edit_transform);
+ Geom::Affine getEditTransform() const { return _edit_transform; }
+
+ bool knot_selected() const;
+ bool knot_mouseover() const;
+
+ SPDesktop *getDesktop() { return desktop; }
+ SPItem *getItem() { return item; }
+ bool is_dragging() const { return dragging; }
+
+ bool set_item_clickpos(Geom::Point loc);
+ void install_modification_watch();
+
+ std::list<KnotHolderEntity *> entity;
+ friend class Inkscape::UI::ShapeEditor; // FIXME why?
+ friend class Inkscape::LivePathEffect::NodeSatelliteArrayParam; // why?
+ friend class Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity; // why?
+ friend class Inkscape::LivePathEffect::FilletChamferKnotHolderEntity; // why?
+
+protected:
+
+ SPDesktop *desktop;
+ SPItem *item; // TODO: Remove this and keep the actual item (e.g., SPRect etc.) in the item-specific knotholders
+ Inkscape::XML::Node *repr; ///< repr of the item, for setting and releasing listeners.
+
+ SPKnotHolderReleasedFunc released;
+
+ bool local_change; ///< if true, no need to recreate knotholder if repr was changed.
+
+ bool dragging;
+
+ Geom::Affine _edit_transform;
+ Inkscape::auto_connection _watch_fill;
+ Inkscape::auto_connection _watch_stroke;
+};
+
+/**
+void knot_clicked_handler(SPKnot *knot, guint state, gpointer data);
+void knot_moved_handler(SPKnot *knot, Geom::Point const *p, guint state, gpointer data);
+void knot_ungrabbed_handler(SPKnot *knot, unsigned int state, KnotHolder *kh);
+**/
+
+#endif // SEEN_SP_KNOTHOLDER_H
+
+/*
+ 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 :
diff --git a/src/ui/knot/knot-ptr.cpp b/src/ui/knot/knot-ptr.cpp
new file mode 100644
index 0000000..8e275ac
--- /dev/null
+++ b/src/ui/knot/knot-ptr.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include <algorithm>
+#include <glib.h>
+#include <list>
+#include "knot-ptr.h"
+
+static std::list<void*> deleted_knots;
+
+void knot_deleted_callback(void* knot) {
+ if (std::find(deleted_knots.begin(), deleted_knots.end(), knot) == deleted_knots.end()) {
+ deleted_knots.push_back(knot);
+ }
+}
+
+void knot_created_callback(void* knot) {
+ std::list<void*>::iterator it = std::find(deleted_knots.begin(), deleted_knots.end(), knot);
+ if (it != deleted_knots.end()) {
+ deleted_knots.erase(it);
+ }
+}
+
+void check_if_knot_deleted(void* knot) {
+ if (std::find(deleted_knots.begin(), deleted_knots.end(), knot) != deleted_knots.end()) {
+ g_warning("Accessed knot after it was freed at %p", knot);
+ }
+}
diff --git a/src/ui/knot/knot-ptr.h b/src/ui/knot/knot-ptr.h
new file mode 100644
index 0000000..5141822
--- /dev/null
+++ b/src/ui/knot/knot-ptr.h
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef KNOT_PTR_DETECTOR
+#define KNOT_PTR_DETECTOR
+
+void knot_deleted_callback(void* knot);
+void knot_created_callback(void* knot);
+void check_if_knot_deleted(void* knot);
+
+#endif
diff --git a/src/ui/knot/knot.cpp b/src/ui/knot/knot.cpp
new file mode 100644
index 0000000..24fefb4
--- /dev/null
+++ b/src/ui/knot/knot.cpp
@@ -0,0 +1,515 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * SPKnot implementation
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * 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 <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.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 "display/control/canvas-item-ctrl.h"
+#include "ui/tools/tool-base.h"
+#include "ui/tools/node-tool.h"
+#include "ui/widget/canvas.h" // autoscroll
+
+using Inkscape::DocumentUndo;
+
+Gdk::EventMask KNOT_EVENT_MASK (
+ Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK |
+ Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK);
+
+const gchar *nograbenv = getenv("INKSCAPE_NO_GRAB");
+static bool nograb = (nograbenv && *nograbenv && (*nograbenv != '0'));
+
+
+void knot_ref(SPKnot* knot) {
+ knot->ref_count++;
+}
+
+void knot_unref(SPKnot* knot) {
+ if (--knot->ref_count < 1) {
+ delete knot;
+ }
+}
+
+SPKnot::SPKnot(SPDesktop *desktop, gchar const *tip, Inkscape::CanvasItemCtrlType type, Glib::ustring const & name)
+ : desktop(desktop)
+ , ref_count(1)
+{
+ if (tip) {
+ this->tip = g_strdup (tip);
+ }
+
+ fill[SP_KNOT_STATE_NORMAL] = 0xffffff00;
+ fill[SP_KNOT_STATE_MOUSEOVER] = 0xff0000ff;
+ fill[SP_KNOT_STATE_DRAGGING] = 0xff0000ff;
+ fill[SP_KNOT_STATE_SELECTED] = 0x0000ffff;
+
+ stroke[SP_KNOT_STATE_NORMAL] = 0x01000000;
+ stroke[SP_KNOT_STATE_MOUSEOVER] = 0x01000000;
+ stroke[SP_KNOT_STATE_DRAGGING] = 0x01000000;
+ stroke[SP_KNOT_STATE_SELECTED] = 0x01000000;
+
+ image[SP_KNOT_STATE_NORMAL] = nullptr;
+ image[SP_KNOT_STATE_MOUSEOVER] = nullptr;
+ image[SP_KNOT_STATE_DRAGGING] = nullptr;
+ image[SP_KNOT_STATE_SELECTED] = nullptr;
+
+ ctrl = make_canvasitem<Inkscape::CanvasItemCtrl>(desktop->getCanvasControls(), type); // Shape, mode set
+ ctrl->set_name("CanvasItemCtrl:Knot:" + name);
+
+ // Are these needed?
+ ctrl->set_fill(0xffffff00);
+ ctrl->set_stroke(0x01000000);
+
+ _event_connection = ctrl->connect_event(sigc::mem_fun(*this, &SPKnot::eventHandler));
+
+ 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);
+ }
+
+ // Make sure the knot is not grabbed, as it's destructing can be deferred causing
+ // issues like https://gitlab.com/inkscape/inkscape/-/issues/4239
+ ctrl->ungrab();
+ ctrl.reset();
+
+ if (this->tip) {
+ g_free(this->tip);
+ this->tip = nullptr;
+ }
+
+ // FIXME: cannot snap to destroyed knot (lp:1309050)
+ // this->desktop->event_context->discard_delayed_snap_event();
+ 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 && ctrl) {
+ ctrl->grab(KNOT_EVENT_MASK, _cursors[SP_KNOT_STATE_DRAGGING]);
+ }
+ this->setFlag(SP_KNOT_GRABBED, true);
+
+ grabbed = true;
+}
+
+void SPKnot::selectKnot(bool select)
+{
+ setFlag(SP_KNOT_SELECTED, select);
+}
+
+bool SPKnot::eventHandler(GdkEvent *event)
+{
+ /* Run client universal event handler, if present */
+ bool consumed = event_signal.emit(this, event);
+ if (consumed) {
+ return true;
+ }
+
+ bool key_press_event_unconsumed = false;
+
+ ref_count++;
+
+ 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) {
+ doubleclicked_signal.emit(this, event->button.state);
+
+ grabbed = false;
+ moved = false;
+ consumed = true;
+ }
+ break;
+ case GDK_BUTTON_PRESS:
+ if ((event->button.button == 1) && desktop && desktop->event_context && !desktop->event_context->is_space_panning()) {
+ Geom::Point const p = desktop->w2d(Geom::Point(event->button.x, event->button.y));
+ startDragging(p, (gint) event->button.x, (gint) event->button.y, event->button.time);
+ mousedown_signal.emit(this, event->button.state);
+ consumed = true;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1 &&
+ desktop &&
+ desktop->event_context &&
+ !desktop->event_context->is_space_panning()) {
+
+ // If we have any pending snap event, then invoke it now
+ desktop->event_context->process_delayed_snap_event();
+ pressure = 0;
+
+ if (transform_escaped) {
+ transform_escaped = false;
+ consumed = true;
+ } else {
+ setFlag(SP_KNOT_GRABBED, false);
+
+ if (!nograb && ctrl) {
+ ctrl->ungrab();
+ }
+
+ if (moved) {
+ setFlag(SP_KNOT_DRAGGING, false);
+ ungrabbed_signal.emit(this, event->button.state);
+ } else {
+ click_signal.emit(this, event->button.state);
+ }
+
+ grabbed = false;
+ moved = false;
+ consumed = true;
+ }
+ }
+ Inkscape::UI::Tools::sp_update_helperpath(desktop);
+ break;
+
+ case GDK_MOTION_NOTIFY:
+
+ if (!(event->motion.state & GDK_BUTTON1_MASK) && flags & SP_KNOT_DRAGGING) {
+ pressure = 0;
+
+ if (transform_escaped) {
+ transform_escaped = false;
+ consumed = true;
+ } else {
+ setFlag(SP_KNOT_GRABBED, false);
+
+ if (!nograb && ctrl) {
+ ctrl->ungrab();
+ }
+
+ if (moved) {
+ setFlag(SP_KNOT_DRAGGING, false);
+ ungrabbed_signal.emit(this, event->motion.state);
+ } else {
+ click_signal.emit(this, event->motion.state);
+ }
+
+ grabbed = false;
+ moved = false;
+ consumed = true;
+ Inkscape::UI::Tools::sp_update_helperpath(desktop);
+ }
+ } else if (grabbed && desktop && desktop->event_context &&
+ !desktop->event_context->is_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;
+
+ // Note: Synthesized events don't have a device.
+ if (event->motion.device && gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &pressure)) {
+ pressure = CLAMP (pressure, 0, 1);
+ } else {
+ pressure = 0.5;
+ }
+
+ if (!moved) {
+ setFlag(SP_KNOT_DRAGGING, true);
+ grabbed_signal.emit(this, event->button.state);
+ }
+
+ desktop->event_context->snap_delay_handler(nullptr, this, reinterpret_cast<GdkEventMotion*>(event),
+ Inkscape::UI::Tools::DelayedSnapEvent::KNOT_HANDLER);
+ sp_knot_handler_request_position(event, this);
+ moved = true;
+ }
+ break;
+ case GDK_ENTER_NOTIFY:
+ setFlag(SP_KNOT_MOUSEOVER, true);
+ setFlag(SP_KNOT_GRABBED, false);
+
+ if (tip && desktop && desktop->event_context) {
+ desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, tip);
+ }
+ desktop->event_context->use_cursor(_cursors[SP_KNOT_STATE_MOUSEOVER]);
+
+ grabbed = false;
+ moved = false;
+ consumed = true;
+ break;
+ case GDK_LEAVE_NOTIFY:
+ setFlag(SP_KNOT_MOUSEOVER, false);
+ setFlag(SP_KNOT_GRABBED, false);
+
+ if (tip && desktop && desktop->event_context) {
+ desktop->event_context->defaultMessageContext()->clear();
+ }
+ desktop->event_context->use_cursor(_cursors[SP_KNOT_STATE_NORMAL]);
+
+ 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:
+ setFlag(SP_KNOT_GRABBED, false);
+
+ if (!nograb && ctrl) {
+ ctrl->ungrab();
+ }
+
+ if (moved) {
+ setFlag(SP_KNOT_DRAGGING, false);
+
+ ungrabbed_signal.emit(this, event->button.state);
+
+ DocumentUndo::undo(desktop->getDocument());
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Node or handle drag canceled."));
+ transform_escaped = true;
+ consumed = true;
+ }
+
+ grabbed = false;
+ moved = false;
+
+ desktop->event_context->discard_delayed_snap_event();
+ break;
+ default:
+ consumed = false;
+ key_press_event_unconsumed = true;
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ knot_unref(this);
+
+ 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->getCanvas()->enable_autoscroll();
+ 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<Geom::Point&>(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 (ctrl) {
+ ctrl->set_position(p);
+ }
+
+ this->moved_signal.emit(this, p, state);
+}
+
+void SPKnot::moveto(Geom::Point const &p) {
+ this->pos = p;
+
+ if (ctrl) {
+ ctrl->set_position(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) {
+ if (ctrl) {
+ ctrl->show();
+ }
+ } else {
+ if (ctrl) {
+ ctrl->hide();
+ }
+ }
+ 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;
+ }
+}
+
+// TODO: Look at removing this and setting ctrl parameters directly.
+void SPKnot::updateCtrl() {
+
+ if (ctrl) {
+ if (shape_set) {
+ ctrl->set_shape(shape);
+ }
+ ctrl->set_mode(mode);
+ if (size_set) {
+ ctrl->set_size(size);
+ }
+ ctrl->set_angle(angle);
+ ctrl->set_anchor(anchor);
+ }
+
+ _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;
+ }
+ if (ctrl) {
+ ctrl->set_fill(fill[state]);
+ ctrl->set_stroke(stroke[state]);
+ }
+}
+
+
+void SPKnot::setSize(guint i) {
+ size = i;
+ size_set = true;
+}
+
+void SPKnot::setShape(Inkscape::CanvasItemCtrlShape s) {
+ shape = s;
+ shape_set = true;
+}
+
+void SPKnot::setAnchor(guint i) {
+ anchor = (SPAnchorType) i;
+}
+
+void SPKnot::setMode(Inkscape::CanvasItemCtrlMode m) {
+ mode = m;
+}
+
+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(SPKnotStateType type, Glib::RefPtr<Gdk::Cursor> cursor)
+{
+ _cursors[type] = cursor;
+}
+
+/*
+ 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 :
diff --git a/src/ui/knot/knot.h b/src/ui/knot/knot.h
new file mode 100644
index 0000000..e780d4e
--- /dev/null
+++ b/src/ui/knot/knot.h
@@ -0,0 +1,208 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_KNOT_H
+#define SEEN_SP_KNOT_H
+
+/** \file
+ * Declarations for SPKnot: Desktop-bound visual control object.
+ */
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 1999-2002 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+#include <sigc++/sigc++.h>
+#include <glibmm/ustring.h>
+#include <glibmm/refptr.h>
+#include <gdkmm/cursor.h>
+
+#include "knot-enums.h"
+#include "display/control/canvas-item-enums.h"
+#include "display/control/canvas-item-ptr.h"
+#include "enums.h"
+
+class SPDesktop;
+class SPItem;
+
+typedef union _GdkEvent GdkEvent;
+typedef unsigned int guint32;
+
+#define SP_KNOT(obj) (dynamic_cast<SPKnot*>(static_cast<SPKnot*>(obj)))
+#define SP_IS_KNOT(obj) (dynamic_cast<const SPKnot*>(static_cast<const SPKnot*>(obj)) != NULL)
+
+namespace Inkscape {
+class CanvasItemCtrl;
+}
+
+/**
+ * Desktop-bound visual control object.
+ *
+ * A knot is a draggable object, with callbacks to change something by
+ * dragging it, visually represented by a canvas item (mostly square).
+ *
+ * See also KnotHolderEntity.
+ * See also ControlPoint (which does the same kind of things).
+ */
+class SPKnot {
+public:
+ SPKnot(SPDesktop *desktop, char const *tip, Inkscape::CanvasItemCtrlType type, Glib::ustring const & name = Glib::ustring("unknown"));
+ virtual ~SPKnot();
+
+ SPKnot(SPKnot const&) = delete;
+ SPKnot& operator=(SPKnot const&) = delete;
+
+ int ref_count; // FIXME encapsulation
+
+ SPDesktop *desktop = nullptr; /**< Desktop we are on. */
+ CanvasItemPtr<Inkscape::CanvasItemCtrl> ctrl; /**< Our CanvasItemCtrl. */
+ SPItem *owner = nullptr; /**< Optional Owner Item */
+ SPItem *sub_owner = nullptr; /**< Optional SubOwner Item */
+ unsigned int flags = SP_KNOT_VISIBLE;
+
+ unsigned int size = 9; /**< Always square. Must be odd. */
+ bool size_set = false; /**< Use default size unless explicitly set. */
+ double angle = 0.0; /**< Angle of mesh handle. */
+ bool is_lpe = false; /**< is lpe knot. */
+ Geom::Point pos; /**< Our desktop coordinates. */
+ Geom::Point grabbed_rel_pos; /**< Grabbed relative position. */
+ Geom::Point drag_origin; /**< Origin of drag. */
+ SPAnchorType anchor = SP_ANCHOR_CENTER; /**< Anchor. */
+
+ bool grabbed = false;
+ bool moved = false;
+ int xp = 0.0; /**< Where drag started */
+ int yp = 0.0; /**< Where drag started */
+ int tolerance = 0;
+ bool within_tolerance = false;
+ bool transform_escaped = false; // true iff resize or rotate was cancelled by esc.
+
+ Inkscape::CanvasItemCtrlShape shape = Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE; /**< Shape type. */
+ bool shape_set = false; /**< Use default shape unless explicitly set. */
+ Inkscape::CanvasItemCtrlMode mode = Inkscape::CANVAS_ITEM_CTRL_MODE_XOR;
+
+ guint32 fill[SP_KNOT_VISIBLE_STATES];
+ guint32 stroke[SP_KNOT_VISIBLE_STATES];
+ unsigned char *image[SP_KNOT_VISIBLE_STATES];
+ Glib::RefPtr<Gdk::Cursor> _cursors[SP_KNOT_VISIBLE_STATES];
+
+ char *tip = nullptr;
+
+ sigc::connection _event_connection;
+
+ double pressure = 0.0; /**< The tablet pen pressure when the knot is being dragged. */
+
+ // FIXME: signals should NOT need to emit the object they came from, the callee should
+ // be able to figure that out
+ sigc::signal<void (SPKnot *, unsigned int)> click_signal;
+ sigc::signal<void (SPKnot*, unsigned int)> doubleclicked_signal;
+ sigc::signal<void (SPKnot*, unsigned int)> mousedown_signal;
+ sigc::signal<void (SPKnot*, unsigned int)> grabbed_signal;
+ sigc::signal<void (SPKnot *, unsigned int)> ungrabbed_signal;
+ sigc::signal<void (SPKnot *, Geom::Point const &, unsigned int)> moved_signal;
+ sigc::signal<bool (SPKnot*, GdkEvent*)> event_signal;
+
+ sigc::signal<bool (SPKnot*, Geom::Point*, unsigned int)> request_signal;
+
+
+ //TODO: all the members above should eventualle become private, accessible via setters/getters
+ void setSize(unsigned int i);
+ void setShape(Inkscape::CanvasItemCtrlShape s);
+ void setAnchor(unsigned int i);
+ void setMode(Inkscape::CanvasItemCtrlMode m);
+ void setAngle(double i);
+
+ void setFill(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected);
+ void setStroke(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected);
+ void setImage(unsigned char* normal, unsigned char* mouseover, unsigned char* dragging, unsigned char* selected);
+
+ void setCursor(SPKnotStateType type, Glib::RefPtr<Gdk::Cursor> cursor);
+
+ /**
+ * Show knot on its canvas.
+ */
+ void show();
+
+ /**
+ * Hide knot on its canvas.
+ */
+ void hide();
+
+ /**
+ * Set flag in knot, with side effects.
+ */
+ void setFlag(unsigned int flag, bool set);
+
+ /**
+ * Update knot's control state.
+ */
+ void updateCtrl();
+
+ /**
+ * Request or set new position for knot.
+ */
+ void requestPosition(Geom::Point const &pos, unsigned int state);
+
+ /**
+ * Update knot for dragging and tell canvas an item was grabbed.
+ */
+ void startDragging(Geom::Point const &p, int x, int y, guint32 etime);
+
+ /**
+ * Move knot to new position and emits "moved" signal.
+ */
+ void setPosition(Geom::Point const &p, unsigned int state);
+
+ /**
+ * Move knot to new position, without emitting a MOVED signal.
+ */
+ void moveto(Geom::Point const &p);
+ /**
+ * Select knot.
+ */
+ void selectKnot(bool select);
+
+ /**
+ * Returns position of knot.
+ */
+ Geom::Point position() const;
+
+ /**
+ * Event handler (from CanvasItem's).
+ */
+ bool eventHandler(GdkEvent *event);
+
+ bool is_visible() const { return (flags & SP_KNOT_VISIBLE) != 0; }
+ bool is_selected() const { return (flags & SP_KNOT_SELECTED) != 0; }
+ bool is_mouseover() const { return (flags & SP_KNOT_MOUSEOVER) != 0; }
+ bool is_dragging() const { return (flags & SP_KNOT_DRAGGING) != 0; }
+ bool is_grabbed() const { return (flags & SP_KNOT_GRABBED) != 0; }
+
+private:
+ /**
+ * Set knot control state (dragging/mouseover/normal).
+ */
+ void _setCtrlState();
+};
+
+void knot_ref(SPKnot* knot);
+void knot_unref(SPKnot* knot);
+
+void sp_knot_handler_request_position(GdkEvent *event, SPKnot *knot);
+
+#endif // SEEN_SP_KNOT_H
+
+/*
+ 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 :