diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
commit | c853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch) | |
tree | 7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/ui/knot | |
parent | Initial commit. (diff) | |
download | inkscape-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/README | 62 | ||||
-rw-r--r-- | src/ui/knot/knot-enums.h | 49 | ||||
-rw-r--r-- | src/ui/knot/knot-holder-entity.cpp | 633 | ||||
-rw-r--r-- | src/ui/knot/knot-holder-entity.h | 254 | ||||
-rw-r--r-- | src/ui/knot/knot-holder.cpp | 512 | ||||
-rw-r--r-- | src/ui/knot/knot-holder.h | 123 | ||||
-rw-r--r-- | src/ui/knot/knot-ptr.cpp | 34 | ||||
-rw-r--r-- | src/ui/knot/knot-ptr.h | 17 | ||||
-rw-r--r-- | src/ui/knot/knot.cpp | 515 | ||||
-rw-r--r-- | src/ui/knot/knot.h | 208 |
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 : |