From cca66b9ec4e494c1d919bff0f71a820d8afab1fa Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 20:24:48 +0200 Subject: Adding upstream version 1.2.2. Signed-off-by: Daniel Baumann --- src/ui/tool/README | 29 + src/ui/tool/commit-events.h | 52 + src/ui/tool/control-point-selection.cpp | 784 ++++++++++++ src/ui/tool/control-point-selection.h | 179 +++ src/ui/tool/control-point.cpp | 597 ++++++++++ src/ui/tool/control-point.h | 414 +++++++ src/ui/tool/curve-drag-point.cpp | 247 ++++ src/ui/tool/curve-drag-point.h | 77 ++ src/ui/tool/event-utils.cpp | 93 ++ src/ui/tool/event-utils.h | 129 ++ src/ui/tool/manipulator.h | 174 +++ src/ui/tool/modifier-tracker.cpp | 94 ++ src/ui/tool/modifier-tracker.h | 55 + src/ui/tool/multi-path-manipulator.cpp | 899 ++++++++++++++ src/ui/tool/multi-path-manipulator.h | 157 +++ src/ui/tool/node-types.h | 57 + src/ui/tool/node.cpp | 1923 ++++++++++++++++++++++++++++++ src/ui/tool/node.h | 532 +++++++++ src/ui/tool/path-manipulator.cpp | 1804 ++++++++++++++++++++++++++++ src/ui/tool/path-manipulator.h | 186 +++ src/ui/tool/selectable-control-point.cpp | 150 +++ src/ui/tool/selectable-control-point.h | 80 ++ src/ui/tool/selector.cpp | 153 +++ src/ui/tool/selector.h | 59 + src/ui/tool/shape-record.h | 65 + src/ui/tool/transform-handle-set.cpp | 827 +++++++++++++ src/ui/tool/transform-handle-set.h | 147 +++ 27 files changed, 9963 insertions(+) create mode 100644 src/ui/tool/README create mode 100644 src/ui/tool/commit-events.h create mode 100644 src/ui/tool/control-point-selection.cpp create mode 100644 src/ui/tool/control-point-selection.h create mode 100644 src/ui/tool/control-point.cpp create mode 100644 src/ui/tool/control-point.h create mode 100644 src/ui/tool/curve-drag-point.cpp create mode 100644 src/ui/tool/curve-drag-point.h create mode 100644 src/ui/tool/event-utils.cpp create mode 100644 src/ui/tool/event-utils.h create mode 100644 src/ui/tool/manipulator.h create mode 100644 src/ui/tool/modifier-tracker.cpp create mode 100644 src/ui/tool/modifier-tracker.h create mode 100644 src/ui/tool/multi-path-manipulator.cpp create mode 100644 src/ui/tool/multi-path-manipulator.h create mode 100644 src/ui/tool/node-types.h create mode 100644 src/ui/tool/node.cpp create mode 100644 src/ui/tool/node.h create mode 100644 src/ui/tool/path-manipulator.cpp create mode 100644 src/ui/tool/path-manipulator.h create mode 100644 src/ui/tool/selectable-control-point.cpp create mode 100644 src/ui/tool/selectable-control-point.h create mode 100644 src/ui/tool/selector.cpp create mode 100644 src/ui/tool/selector.h create mode 100644 src/ui/tool/shape-record.h create mode 100644 src/ui/tool/transform-handle-set.cpp create mode 100644 src/ui/tool/transform-handle-set.h (limited to 'src/ui/tool') diff --git a/src/ui/tool/README b/src/ui/tool/README new file mode 100644 index 0000000..8a1c41a --- /dev/null +++ b/src/ui/tool/README @@ -0,0 +1,29 @@ + + +This directory contains code related to on-screen editing (nodes, handles, etc.). + +Note that there are classes with similar functionality based on the SPKnot class in src/ui/knot. + +Classes here: + + * ControlPoint + ** CurveDragPoint + ** Handle + ** RotationHandle + ** SelectableContrlPoint + *** Node + ** SelectorPoint, + ** TransformHandle + *** RotateHandle + *** ScaleHandle + **** ScaleCornerHandle + **** ScaleSideHandle + *** SkewHandle + + * Manipulator + ** PointManipulator + *** MultiManipulator + *** PathManipulator + *** MultiPathManipulator + ** Selector + ** TransformHandleSet diff --git a/src/ui/tool/commit-events.h b/src/ui/tool/commit-events.h new file mode 100644 index 0000000..37fb861 --- /dev/null +++ b/src/ui/tool/commit-events.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Commit events. + */ +/* Authors: + * Krzysztof Kosiński + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_COMMIT_EVENTS_H +#define SEEN_UI_TOOL_COMMIT_EVENTS_H + +namespace Inkscape { +namespace UI { + +/// This is used to provide sensible messages on the undo stack. +enum CommitEvent { + COMMIT_MOUSE_MOVE, + COMMIT_KEYBOARD_MOVE_X, + COMMIT_KEYBOARD_MOVE_Y, + COMMIT_MOUSE_SCALE, + COMMIT_MOUSE_SCALE_UNIFORM, + COMMIT_KEYBOARD_SCALE_UNIFORM, + COMMIT_KEYBOARD_SCALE_X, + COMMIT_KEYBOARD_SCALE_Y, + COMMIT_MOUSE_ROTATE, + COMMIT_KEYBOARD_ROTATE, + COMMIT_MOUSE_SKEW_X, + COMMIT_MOUSE_SKEW_Y, + COMMIT_KEYBOARD_SKEW_X, + COMMIT_KEYBOARD_SKEW_Y, + COMMIT_FLIP_X, + COMMIT_FLIP_Y +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + 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/tool/control-point-selection.cpp b/src/ui/tool/control-point-selection.cpp new file mode 100644 index 0000000..3f910de --- /dev/null +++ b/src/ui/tool/control-point-selection.cpp @@ -0,0 +1,784 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Node selection - implementation. + */ +/* Authors: + * Krzysztof Kosiński + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "ui/tool/selectable-control-point.h" +#include <2geom/transforms.h> +#include "desktop.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/transform-handle-set.h" +#include "ui/tool/node.h" +#include "display/control/snap-indicator.h" +#include "ui/widget/canvas.h" + + + +#include + +namespace Inkscape { +namespace UI { + +/** + * @class ControlPointSelection + * Group of selected control points. + * + * Some operations can be performed on all selected points regardless of their type, therefore + * this class is also a Manipulator. It handles the transformations of points using + * the keyboard. + * + * The exposed interface is similar to that of an STL set. Internally, a hash map is used. + * @todo Correct iterators (that don't expose the connection list) + */ + +/** @var ControlPointSelection::signal_update + * Fires when the display needs to be updated to reflect changes. + */ +/** @var ControlPointSelection::signal_point_changed + * Fires when a control point is added to or removed from the selection. + * The first param contains a pointer to the control point that changed sel. state. + * The second says whether the point is currently selected. + */ +/** @var ControlPointSelection::signal_commit + * Fires when a change that needs to be committed to XML happens. + */ + +ControlPointSelection::ControlPointSelection(SPDesktop *d, Inkscape::CanvasItemGroup *th_group) + : Manipulator(d) + , _handles(new TransformHandleSet(d, th_group)) + , _dragging(false) + , _handles_visible(true) + , _one_node_handles(false) +{ + signal_update.connect( sigc::bind( + sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles), + true)); + ControlPoint::signal_mouseover_change.connect( + sigc::hide( + sigc::mem_fun(*this, &ControlPointSelection::_mouseoverChanged))); + _handles->signal_transform.connect( + sigc::mem_fun(*this, &ControlPointSelection::transform)); + _handles->signal_commit.connect( + sigc::mem_fun(*this, &ControlPointSelection::_commitHandlesTransform)); +} + +ControlPointSelection::~ControlPointSelection() +{ + clear(); + delete _handles; +} + +/** Add a control point to the selection. */ +std::pair ControlPointSelection::insert(const value_type &x, bool notify, bool to_update) +{ + iterator found = _points.find(x); + if (found != _points.end()) { + return std::pair(found, false); + } + + found = _points.insert(x).first; + _points_list.push_back(x); + + x->updateState(); + + if (to_update) { + _update(); + } + if (notify) { + signal_selection_changed.emit(std::vector(1, x), true); + } + + return std::pair(found, true); +} + +/** Remove a point from the selection. */ +void ControlPointSelection::erase(iterator pos, bool to_update) +{ + SelectableControlPoint *erased = *pos; + _points_list.remove(*pos); + _points.erase(pos); + erased->updateState(); + if (to_update) { + _update(); + } +} +ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k, bool notify) +{ + iterator pos = _points.find(k); + if (pos == _points.end()) return 0; + erase(pos); + + if (notify) { + signal_selection_changed.emit(std::vector(1, k), false); + } + return 1; +} +void ControlPointSelection::erase(iterator first, iterator last) +{ + std::vector out(first, last); + while (first != last) { + erase(first++, false); + } + _update(); + signal_selection_changed.emit(out, false); +} + +/** Remove all points from the selection, making it empty. */ +void ControlPointSelection::clear() +{ + if (empty()) { + return; + } + + std::vector out(begin(), end()); // begin() takes from _points + _points.clear(); + _points_list.clear(); + for (auto erased : out) { + erased->updateState(); + } + + _update(); + signal_selection_changed.emit(out, false); +} + +/** Select all points that this selection can contain. */ +void ControlPointSelection::selectAll() +{ + for (auto _all_point : _all_points) { + insert(_all_point, false, false); + } + std::vector out(_all_points.begin(), _all_points.end()); + if (!out.empty()) { + _update(); + signal_selection_changed.emit(out, true); + } +} +/** Select all points inside the given rectangle (in desktop coordinates). */ +void ControlPointSelection::selectArea(Geom::Rect const &r, bool invert) +{ + std::vector out; + for (auto _all_point : _all_points) { + if (r.contains(*_all_point)) { + if (invert) { + erase(_all_point); + } else { + insert(_all_point, false, false); + } + out.push_back(_all_point); + } + } + if (!out.empty()) { + _update(); + signal_selection_changed.emit(out, true); + } +} +/** Unselect all selected points and select all unselected points. */ +void ControlPointSelection::invertSelection() +{ + std::vector in, out; + for (auto _all_point : _all_points) { + if (_all_point->selected()) { + in.push_back(_all_point); + erase(_all_point); + } + else { + out.push_back(_all_point); + insert(_all_point, false, false); + } + } + _update(); + if (!in.empty()) + signal_selection_changed.emit(in, false); + if (!out.empty()) + signal_selection_changed.emit(out, true); +} +void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir) +{ + bool grow = (dir > 0); + Geom::Point p = origin->position(); + double best_dist = grow ? HUGE_VAL : 0; + SelectableControlPoint *match = nullptr; + for (auto _all_point : _all_points) { + bool selected = _all_point->selected(); + if (grow && !selected) { + double dist = Geom::distance(_all_point->position(), p); + if (dist < best_dist) { + best_dist = dist; + match = _all_point; + } + } + if (!grow && selected) { + double dist = Geom::distance(_all_point->position(), p); + // use >= to also deselect the origin node when it's the last one selected + if (dist >= best_dist) { + best_dist = dist; + match = _all_point; + } + } + } + if (match) { + if (grow) insert(match); + else erase(match); + signal_selection_changed.emit(std::vector(1, match), grow); + } +} + +/** Transform all selected control points by the given affine transformation. */ +void ControlPointSelection::transform(Geom::Affine const &m) +{ + for (auto cur : _points) { + cur->transform(m); + } + for (auto cur : _points) { + cur->fixNeighbors(); + } + + _updateBounds(); + // TODO preserving the rotation radius needs some rethinking... + if (_rot_radius) (*_rot_radius) *= m.descrim(); + if (_mouseover_rot_radius) (*_mouseover_rot_radius) *= m.descrim(); + signal_update.emit(); +} + +/** Align control points on the specified axis. */ +void ControlPointSelection::align(Geom::Dim2 axis, AlignTargetNode target) +{ + if (empty()) return; + Geom::Dim2 d = static_cast((axis + 1) % 2); + + Geom::OptInterval bound; + for (auto _point : _points) { + bound.unionWith(Geom::OptInterval(_point->position()[d])); + } + + if (!bound) { return; } + + double new_coord; + switch (target) { + case AlignTargetNode::FIRST_NODE: + new_coord=(_points_list.front())->position()[d]; + break; + case AlignTargetNode::LAST_NODE: + new_coord=(_points_list.back())->position()[d]; + break; + case AlignTargetNode::MID_NODE: + new_coord=bound->middle(); + break; + case AlignTargetNode::MIN_NODE: + new_coord=bound->min(); + break; + case AlignTargetNode::MAX_NODE: + new_coord=bound->max(); + break; + default: + return; + } + + for (auto _point : _points) { + Geom::Point pos = _point->position(); + pos[d] = new_coord; + _point->move(pos); + } +} + +/** Equdistantly distribute control points by moving them in the specified dimension. */ +void ControlPointSelection::distribute(Geom::Dim2 d) +{ + if (empty()) return; + + // this needs to be a multimap, otherwise it will fail when some points have the same coord + typedef std::multimap SortMap; + + SortMap sm; + Geom::OptInterval bound; + // first we insert all points into a multimap keyed by the aligned coord to sort them + // simultaneously we compute the extent of selection + for (auto _point : _points) { + Geom::Point pos = _point->position(); + sm.insert(std::make_pair(pos[d], _point)); + bound.unionWith(Geom::OptInterval(pos[d])); + } + + if (!bound) { return; } + + // now we iterate over the multimap and set aligned positions. + double step = size() == 1 ? 0 : bound->extent() / (size() - 1); + double start = bound->min(); + unsigned num = 0; + for (SortMap::iterator i = sm.begin(); i != sm.end(); ++i, ++num) { + Geom::Point pos = i->second->position(); + pos[d] = start + num * step; + i->second->move(pos); + } +} + +/** Get the bounds of the selection. + * @return Smallest rectangle containing the positions of all selected points, + * or nothing if the selection is empty */ +Geom::OptRect ControlPointSelection::pointwiseBounds() +{ + return _bounds; +} + +Geom::OptRect ControlPointSelection::bounds() +{ + return size() == 1 ? (*_points.begin())->bounds() : _bounds; +} + +void ControlPointSelection::showTransformHandles(bool v, bool one_node) +{ + _one_node_handles = one_node; + _handles_visible = v; + _updateTransformHandles(false); +} + +void ControlPointSelection::hideTransformHandles() +{ + _handles->setVisible(false); +} +void ControlPointSelection::restoreTransformHandles() +{ + _updateTransformHandles(true); +} + +void ControlPointSelection::toggleTransformHandlesMode() +{ + if (_handles->mode() == TransformHandleSet::MODE_SCALE) { + _handles->setMode(TransformHandleSet::MODE_ROTATE_SKEW); + if (size() == 1) { + _handles->rotationCenter().setVisible(false); + } + } else { + _handles->setMode(TransformHandleSet::MODE_SCALE); + } +} + +void ControlPointSelection::_pointGrabbed(SelectableControlPoint *point) +{ + hideTransformHandles(); + _dragging = true; + _grabbed_point = point; + _farthest_point = point; + double maxdist = 0; + Geom::Affine m; + m.setIdentity(); + for (auto _point : _points) { + _original_positions.insert(std::make_pair(_point, _point->position())); + _last_trans.insert(std::make_pair(_point, m)); + double dist = Geom::distance(*_grabbed_point, *_point); + if (dist > maxdist) { + maxdist = dist; + _farthest_point = _point; + } + } +} + +void ControlPointSelection::_pointDragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + Geom::Point abs_delta = new_pos - _original_positions[_grabbed_point]; + double fdist = Geom::distance(_original_positions[_grabbed_point], _original_positions[_farthest_point]); + if (held_only_alt(*event) && fdist > 0) { + // Sculpting + for (auto cur : _points) { + Geom::Affine trans; + trans.setIdentity(); + double dist = Geom::distance(_original_positions[cur], _original_positions[_grabbed_point]); + double deltafrac = 0.5 + 0.5 * cos(M_PI * dist/fdist); + if (dist != 0.0) { + // The sculpting transformation is not affine, but it can be + // locally approximated by one. Here we compute the local + // affine approximation of the sculpting transformation near + // the currently transformed point. We then transform the point + // by this approximation. This gives us sensible behavior for node handles. + // NOTE: probably it would be better to transform the node handles, + // but ControlPointSelection is supposed to work for any + // SelectableControlPoints, not only Nodes. We could create a specialized + // NodeSelection class that inherits from this one and move sculpting there. + Geom::Point origdx(Geom::EPSILON, 0); + Geom::Point origdy(0, Geom::EPSILON); + Geom::Point origp = _original_positions[cur]; + Geom::Point origpx = _original_positions[cur] + origdx; + Geom::Point origpy = _original_positions[cur] + origdy; + double distdx = Geom::distance(origpx, _original_positions[_grabbed_point]); + double distdy = Geom::distance(origpy, _original_positions[_grabbed_point]); + double deltafracdx = 0.5 + 0.5 * cos(M_PI * distdx/fdist); + double deltafracdy = 0.5 + 0.5 * cos(M_PI * distdy/fdist); + Geom::Point newp = origp + abs_delta * deltafrac; + Geom::Point newpx = origpx + abs_delta * deltafracdx; + Geom::Point newpy = origpy + abs_delta * deltafracdy; + Geom::Point newdx = (newpx - newp) / Geom::EPSILON; + Geom::Point newdy = (newpy - newp) / Geom::EPSILON; + + Geom::Affine itrans(newdx[Geom::X], newdx[Geom::Y], newdy[Geom::X], newdy[Geom::Y], 0, 0); + if (itrans.isSingular()) + itrans.setIdentity(); + + trans *= Geom::Translate(-cur->position()); + trans *= _last_trans[cur].inverse(); + trans *= itrans; + trans *= Geom::Translate(_original_positions[cur] + abs_delta * deltafrac); + _last_trans[cur] = itrans; + } else { + trans *= Geom::Translate(-cur->position() + _original_positions[cur] + abs_delta * deltafrac); + } + cur->transform(trans); + //cur->move(_original_positions[cur] + abs_delta * deltafrac); + } + } else { + Geom::Point delta = new_pos - _grabbed_point->position(); + for (auto cur : _points) { + cur->move(_original_positions[cur] + abs_delta); + } + _handles->rotationCenter().move(_handles->rotationCenter().position() + delta); + } + for (auto cur : _points) { + cur->fixNeighbors(); + } + signal_update.emit(); +} + +void ControlPointSelection::_pointUngrabbed() +{ + _desktop->snapindicator->remove_snaptarget(); + _original_positions.clear(); + _last_trans.clear(); + _dragging = false; + _grabbed_point = _farthest_point = nullptr; + _updateBounds(); + restoreTransformHandles(); + signal_commit.emit(COMMIT_MOUSE_MOVE); +} + +bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventButton *event) +{ + // clicking a selected node should toggle the transform handles between rotate and scale mode, + // if they are visible + if (held_no_modifiers(*event) && _handles_visible && p->selected()) { + toggleTransformHandlesMode(); + return true; + } + return false; +} + +void ControlPointSelection::_mouseoverChanged() +{ + _mouseover_rot_radius = std::nullopt; +} + +void ControlPointSelection::_update() +{ + _updateBounds(); + _updateTransformHandles(false); + if (_bounds) { + _handles->rotationCenter().move(_bounds->midpoint()); + } +} + +void ControlPointSelection::_updateBounds() +{ + _rot_radius = std::nullopt; + _bounds = Geom::OptRect(); + for (auto cur : _points) { + Geom::Point p = cur->position(); + if (!_bounds) { + _bounds = Geom::Rect(p, p); + } else { + _bounds->expandTo(p); + } + } +} + +void ControlPointSelection::_updateTransformHandles(bool preserve_center) +{ + if (_dragging) return; + + if (_handles_visible && size() > 1) { + _handles->setBounds(*bounds(), preserve_center); + _handles->setVisible(true); + } else if (_one_node_handles && size() == 1) { // only one control point in selection + SelectableControlPoint *p = *begin(); + _handles->setBounds(p->bounds()); + _handles->rotationCenter().move(p->position()); + _handles->rotationCenter().setVisible(false); + _handles->setVisible(true); + } else { + _handles->setVisible(false); + } +} + +/** Moves the selected points along the supplied unit vector according to + * the modifier state of the supplied event. */ +bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir) +{ + if (held_control(event)) return false; + unsigned num = 1 + _desktop->canvas->gobble_key_events(shortcut_key(event), 0); + + Geom::Point delta = dir * num; + if (held_shift(event)) delta *= 10; + if (held_alt(event)) { + delta /= _desktop->current_zoom(); + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); + delta *= nudge; + } + + transform(Geom::Translate(delta)); + if (fabs(dir[Geom::X]) > 0) { + signal_commit.emit(COMMIT_KEYBOARD_MOVE_X); + } else { + signal_commit.emit(COMMIT_KEYBOARD_MOVE_Y); + } + return true; +} + +/** + * Computes the distance to the farthest corner of the bounding box. + * Used to determine what it means to "rotate by one pixel". + */ +double ControlPointSelection::_rotationRadius(Geom::Point const &rc) +{ + if (empty()) return 1.0; // some safe value + Geom::Rect b = *bounds(); + double maxlen = 0; + for (unsigned i = 0; i < 4; ++i) { + double len = Geom::distance(b.corner(i), rc); + if (len > maxlen) maxlen = len; + } + return maxlen; +} + +/** + * Rotates the selected points in the given direction according to the modifier state + * from the supplied event. + * @param event Key event to take modifier state from + * @param dir Direction of rotation (math convention: 1 = counterclockwise, -1 = clockwise) + */ +bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir) +{ + if (empty()) return false; + + Geom::Point rc; + + // rotate around the mouseovered point, or the selection's rotation center + // if nothing is mouseovered + double radius; + SelectableControlPoint *scp = + dynamic_cast(ControlPoint::mouseovered_point); + if (scp) { + rc = scp->position(); + if (!_mouseover_rot_radius) { + _mouseover_rot_radius = _rotationRadius(rc); + } + radius = *_mouseover_rot_radius; + } else { + rc = _handles->rotationCenter(); + if (!_rot_radius) { + _rot_radius = _rotationRadius(rc); + } + radius = *_rot_radius; + } + + double angle; + if (held_alt(event)) { + // Rotate by "one pixel". We interpret this as rotating by an angle that causes + // the topmost point of a circle circumscribed about the selection's bounding box + // to move on an arc 1 screen pixel long. + angle = atan2(1.0 / _desktop->current_zoom(), radius) * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + angle = M_PI * dir / snaps; + } + + // translate to origin, rotate, translate back to original position + Geom::Affine m = Geom::Translate(-rc) + * Geom::Rotate(angle) * Geom::Translate(rc); + transform(m); + signal_commit.emit(COMMIT_KEYBOARD_ROTATE); + return true; +} + + +bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir) +{ + if (empty()) return false; + + double maxext = bounds()->maxExtent(); + if (Geom::are_near(maxext, 0)) return false; + + Geom::Point center; + SelectableControlPoint *scp = + dynamic_cast(ControlPoint::mouseovered_point); + if (scp) { + center = scp->position(); + } else { + center = _handles->rotationCenter().position(); + } + + double length_change; + if (held_alt(event)) { + // Scale by "one pixel". It means shrink/grow 1px for the larger dimension + // of the bounding box. + length_change = 1.0 / _desktop->current_zoom() * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000, "px"); + length_change *= dir; + } + double scale = (maxext + length_change) / maxext; + + Geom::Affine m = Geom::Translate(-center) * Geom::Scale(scale) * Geom::Translate(center); + transform(m); + signal_commit.emit(COMMIT_KEYBOARD_SCALE_UNIFORM); + return true; +} + +bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d) +{ + if (empty()) return false; + + Geom::Scale scale_transform(1, 1); + if (d == Geom::X) { + scale_transform = Geom::Scale(-1, 1); + } else { + scale_transform = Geom::Scale(1, -1); + } + + SelectableControlPoint *scp = + dynamic_cast(ControlPoint::mouseovered_point); + Geom::Point center = scp ? scp->position() : _handles->rotationCenter().position(); + + Geom::Affine m = Geom::Translate(-center) * scale_transform * Geom::Translate(center); + transform(m); + signal_commit.emit(d == Geom::X ? COMMIT_FLIP_X : COMMIT_FLIP_Y); + return true; +} + +void ControlPointSelection::_commitHandlesTransform(CommitEvent ce) +{ + _updateBounds(); + _updateTransformHandles(true); + signal_commit.emit(ce); +} + +bool ControlPointSelection::event(Inkscape::UI::Tools::ToolBase * /*event_context*/, GdkEvent *event) +{ + // implement generic event handling that should apply for all control point selections here; + // for example, keyboard moves and transformations. This way this functionality doesn't need + // to be duplicated in many places + // Later split out so that it can be reused in object selection + + switch (event->type) { + case GDK_KEY_PRESS: + // do not handle key events if the selection is empty + if (empty()) break; + + switch(shortcut_key(event->key)) { + // moves + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + return _keyboardMove(event->key, Geom::Point(0, -_desktop->yaxisdir())); + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + return _keyboardMove(event->key, Geom::Point(0, _desktop->yaxisdir())); + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + return _keyboardMove(event->key, Geom::Point(1, 0)); + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + return _keyboardMove(event->key, Geom::Point(-1, 0)); + + // rotates + case GDK_KEY_bracketleft: + return _keyboardRotate(event->key, -_desktop->yaxisdir()); + case GDK_KEY_bracketright: + return _keyboardRotate(event->key, _desktop->yaxisdir()); + + // scaling + case GDK_KEY_less: + case GDK_KEY_comma: + return _keyboardScale(event->key, -1); + case GDK_KEY_greater: + case GDK_KEY_period: + return _keyboardScale(event->key, 1); + + // TODO: skewing + + // flipping + // NOTE: H is horizontal flip, while Shift+H switches transform handle mode! + case GDK_KEY_h: + case GDK_KEY_H: + if (held_shift(event->key)) { + toggleTransformHandlesMode(); + return true; + } + // any modifiers except shift should cause no action + if (held_any_modifiers(event->key)) break; + return _keyboardFlip(Geom::X); + case GDK_KEY_v: + case GDK_KEY_V: + if (held_any_modifiers(event->key)) break; + return _keyboardFlip(Geom::Y); + default: break; + } + break; + default: break; + } + return false; +} + +void ControlPointSelection::getOriginalPoints(std::vector &pts) +{ + pts.clear(); + for (auto _point : _points) { + pts.emplace_back(_original_positions[_point], SNAPSOURCE_NODE_HANDLE); + } +} + +void ControlPointSelection::getUnselectedPoints(std::vector &pts) +{ + pts.clear(); + ControlPointSelection::Set &nodes = this->allPoints(); + for (auto node : nodes) { + if (!node->selected()) { + Node *n = static_cast(node); + pts.push_back(n->snapCandidatePoint()); + } + } +} + +void ControlPointSelection::setOriginalPoints() +{ + _original_positions.clear(); + for (auto _point : _points) { + _original_positions.insert(std::make_pair(_point, _point->position())); + } +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/control-point-selection.h b/src/ui/tool/control-point-selection.h new file mode 100644 index 0000000..dc58211 --- /dev/null +++ b/src/ui/tool/control-point-selection.h @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Control point selection - stores a set of control points and applies transformations + * to them + */ +/* Authors: + * Krzysztof Kosiński + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_CONTROL_POINT_SELECTION_H +#define SEEN_UI_TOOL_CONTROL_POINT_SELECTION_H + +#include +#include +#include +#include +#include +#include +#include +#include <2geom/forward.h> +#include <2geom/point.h> +#include <2geom/rect.h> +#include "ui/tool/commit-events.h" +#include "ui/tool/manipulator.h" +#include "ui/tool/node-types.h" +#include "snap-candidate.h" + +class SPDesktop; + +namespace Inkscape { +class CanvasItemGroup; +namespace UI { +class TransformHandleSet; +class SelectableControlPoint; +} +} + +namespace Inkscape { +namespace UI { + +class ControlPointSelection : public Manipulator, public sigc::trackable { +public: + ControlPointSelection(SPDesktop *d, Inkscape::CanvasItemGroup *th_group); + ~ControlPointSelection() override; + typedef std::unordered_set set_type; + typedef set_type Set; // convenience alias + + typedef set_type::iterator iterator; + typedef set_type::const_iterator const_iterator; + typedef set_type::size_type size_type; + typedef SelectableControlPoint *value_type; + typedef SelectableControlPoint *key_type; + + // size + bool empty() { return _points.empty(); } + size_type size() { return _points.size(); } + + // iterators + iterator begin() { return _points.begin(); } + const_iterator begin() const { return _points.begin(); } + iterator end() { return _points.end(); } + const_iterator end() const { return _points.end(); } + + // insert + std::pair insert(const value_type& x, bool notify = true, bool to_update = true); + template + void insert(InputIterator first, InputIterator last) { + for (; first != last; ++first) { + insert(*first, false, false); + } + _update(); + signal_selection_changed.emit(std::vector(first, last), true); + } + + // erase + void clear(); + void erase(iterator pos, bool to_update = true); + size_type erase(const key_type& k, bool notify = true); + void erase(iterator first, iterator last); + + // find + iterator find(const key_type &k) { + return _points.find(k); + } + + // Sometimes it is very useful to keep a list of all selectable points. + set_type const &allPoints() const { return _all_points; } + set_type &allPoints() { return _all_points; } + // ...for example in these methods. Another useful case is snapping. + void selectAll(); + void selectArea(Geom::Rect const &, bool invert = false); + void invertSelection(); + void spatialGrow(SelectableControlPoint *origin, int dir); + + bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override; + + void transform(Geom::Affine const &m); + void align(Geom::Dim2 d, AlignTargetNode target = AlignTargetNode::MID_NODE); + void distribute(Geom::Dim2 d); + + Geom::OptRect pointwiseBounds(); + Geom::OptRect bounds(); + + bool transformHandlesEnabled() { return _handles_visible; } + void showTransformHandles(bool v, bool one_node); + // the two methods below do not modify the state; they are for use in manipulators + // that need to temporarily hide the handles, for example when moving a node + void hideTransformHandles(); + void restoreTransformHandles(); + void toggleTransformHandlesMode(); + + sigc::signal signal_update; + // It turns out that emitting a signal after every point is selected or deselected is not too efficient, + // so this can be done in a massive group once the selection is finally changed. + sigc::signal, bool> signal_selection_changed; + sigc::signal signal_commit; + + void getOriginalPoints(std::vector &pts); + void getUnselectedPoints(std::vector &pts); + void setOriginalPoints(); + //the purpose of this list is to keep track of first and last selected + std::list _points_list; + +private: + // The functions below are invoked from SelectableControlPoint. + // Previously they were connected to handlers when selecting, but this + // creates problems when dragging a point that was not selected. + void _pointGrabbed(SelectableControlPoint *); + void _pointDragged(Geom::Point &, GdkEventMotion *); + void _pointUngrabbed(); + bool _pointClicked(SelectableControlPoint *, GdkEventButton *); + void _mouseoverChanged(); + + void _update(); + void _updateTransformHandles(bool preserve_center); + void _updateBounds(); + bool _keyboardMove(GdkEventKey const &, Geom::Point const &); + bool _keyboardRotate(GdkEventKey const &, int); + bool _keyboardScale(GdkEventKey const &, int); + bool _keyboardFlip(Geom::Dim2); + void _keyboardTransform(Geom::Affine const &); + void _commitHandlesTransform(CommitEvent ce); + double _rotationRadius(Geom::Point const &); + + set_type _points; + + set_type _all_points; + std::unordered_map _original_positions; + std::unordered_map _last_trans; + std::optional _rot_radius; + std::optional _mouseover_rot_radius; + Geom::OptRect _bounds; + TransformHandleSet *_handles; + SelectableControlPoint *_grabbed_point, *_farthest_point; + unsigned _dragging : 1; + unsigned _handles_visible : 1; + unsigned _one_node_handles : 1; + + friend class SelectableControlPoint; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/ui/tool/control-point.cpp b/src/ui/tool/control-point.cpp new file mode 100644 index 0000000..da6bdaf --- /dev/null +++ b/src/ui/tool/control-point.cpp @@ -0,0 +1,597 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof Kosiński + * Jon A. Cruz + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include + +#include <2geom/point.h> + +#include "desktop.h" +#include "message-context.h" + +#include "display/control/canvas-item-enums.h" +#include "display/control/snap-indicator.h" + +#include "object/sp-namedview.h" + +#include "ui/tools/tool-base.h" +#include "ui/tool/control-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/transform-handle-set.h" + +#include "ui/widget/canvas.h" // Forced redraws + +namespace Inkscape { +namespace UI { + + +// Default colors for control points +ControlPoint::ColorSet ControlPoint::_default_color_set = { + {0xffffff00, 0x01000000}, // normal fill, stroke + {0xff0000ff, 0x01000000}, // mouseover fill, stroke + {0x0000ffff, 0x01000000}, // clicked fill, stroke + // + {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected + {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected + {0xff000000, 0x000000ff} // clicked fill, stroke when selected +}; + +ControlPoint *ControlPoint::mouseovered_point = nullptr; + +sigc::signal ControlPoint::signal_mouseover_change; + +Geom::Point ControlPoint::_drag_event_origin(Geom::infinity(), Geom::infinity()); + +Geom::Point ControlPoint::_drag_origin(Geom::infinity(), Geom::infinity()); + +Gdk::EventMask const ControlPoint::_grab_event_mask = (Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | + Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::SCROLL_MASK | + Gdk::SMOOTH_SCROLL_MASK ); + +bool ControlPoint::_drag_initiated = false; +bool ControlPoint::_event_grab = false; + +ControlPoint::ColorSet ControlPoint::invisible_cset = { + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000} +}; + +ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Glib::RefPtr pixbuf, + ColorSet const &cset, + Inkscape::CanvasItemGroup *group) + : _desktop(d) + , _cset(cset) + , _position(initial_pos) +{ + _canvas_item_ctrl = new Inkscape::CanvasItemCtrl(group ? group : _desktop->getCanvasControls(), + Inkscape::CANVAS_ITEM_CTRL_SHAPE_BITMAP); + _canvas_item_ctrl->set_name("CanvasItemCtrl:ControlPoint"); + _canvas_item_ctrl->set_pixbuf(pixbuf->gobj()); + _canvas_item_ctrl->set_fill( _cset.normal.fill); + _canvas_item_ctrl->set_stroke(_cset.normal.stroke); + _canvas_item_ctrl->set_anchor(anchor); + + _commonInit(); +} + +ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Inkscape::CanvasItemCtrlType type, + ColorSet const &cset, + Inkscape::CanvasItemGroup *group) + : _desktop(d) + , _cset(cset) + , _position(initial_pos) +{ + _canvas_item_ctrl = new Inkscape::CanvasItemCtrl(group ? group : _desktop->getCanvasControls(), type); + _canvas_item_ctrl->set_name("CanvasItemCtrl:ControlPoint"); + _canvas_item_ctrl->set_fill( _cset.normal.fill); + _canvas_item_ctrl->set_stroke(_cset.normal.stroke); + _canvas_item_ctrl->set_anchor(anchor); + + _commonInit(); +} + +ControlPoint::~ControlPoint() +{ + // avoid storing invalid points in mouseovered_point + if (this == mouseovered_point) { + _clearMouseover(); + } + + //g_signal_handler_disconnect(G_OBJECT(_canvas_item_ctrl), _event_handler_connection); + _event_handler_connection.disconnect(); + _canvas_item_ctrl->hide(); + delete _canvas_item_ctrl; +} + +void ControlPoint::_commonInit() +{ + _canvas_item_ctrl->set_position(_position); + _event_handler_connection = + _canvas_item_ctrl->connect_event(sigc::bind(sigc::ptr_fun(_event_handler), this)); + // _event_handler_connection = g_signal_connect(G_OBJECT(_canvas_item_ctrl), "event", + // G_CALLBACK(_event_handler), this); +} + +void ControlPoint::setPosition(Geom::Point const &pos) +{ + _position = pos; + _canvas_item_ctrl->set_position(_position); +} + +void ControlPoint::move(Geom::Point const &pos) +{ + setPosition(pos); +} + +void ControlPoint::transform(Geom::Affine const &m) { + move(position() * m); +} + +bool ControlPoint::visible() const +{ + return _canvas_item_ctrl->is_visible(); +} + +void ControlPoint::setVisible(bool v) +{ + if (v) { + _canvas_item_ctrl->show(); + } else { + _canvas_item_ctrl->hide(); + } +} + +Glib::ustring ControlPoint::format_tip(char const *format, ...) +{ + va_list args; + va_start(args, format); + char *dyntip = g_strdup_vprintf(format, args); + va_end(args); + Glib::ustring ret = dyntip; + g_free(dyntip); + return ret; +} + + +// ===== Setters ===== + +void ControlPoint::_setSize(unsigned int size) +{ + _canvas_item_ctrl->set_size(size); +} + +void ControlPoint::_setControlType(Inkscape::CanvasItemCtrlType type) +{ + _canvas_item_ctrl->set_type(type); +} + +void ControlPoint::_setAnchor(SPAnchorType anchor) +{ +// g_object_set(_canvas_item_ctrl, "anchor", anchor, nullptr); +} + +void ControlPoint::_setPixbuf(Glib::RefPtr p) +{ + _canvas_item_ctrl->set_pixbuf(Glib::unwrap(p)); +} + +// re-routes events into the virtual function TODO: Refactor this nonsense. +bool ControlPoint::_event_handler(GdkEvent *event, ControlPoint *point) +{ + if ((point == nullptr) || (point->_desktop == nullptr)) { + return false; + } + return point->_eventHandler(point->_desktop->event_context, event); +} + +// main event callback, which emits all other callbacks. +bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + // NOTE the static variables below are shared for all points! + // TODO handle clicks and drags from other buttons too + + if (event == nullptr) + { + return false; + } + + if (event_context == nullptr) + { + return false; + } + if (_desktop == nullptr) + { + return false; + } + if(event_context->getDesktop() !=_desktop) + { + g_warning ("ControlPoint: desktop pointers not equal!"); + //return false; + } + // offset from the pointer hotspot to the center of the grabbed knot in desktop coords + static Geom::Point pointer_offset; + // number of last doubleclicked button + static unsigned next_release_doubleclick = 0; + _double_clicked = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + switch(event->type) + { + case GDK_BUTTON_PRESS: + next_release_doubleclick = 0; + if (event->button.button == 1 && !event_context->is_space_panning()) { + // 1st mouse button click. internally, start dragging, but do not emit signals + // or change position until drag tolerance is exceeded. + _drag_event_origin[Geom::X] = event->button.x; + _drag_event_origin[Geom::Y] = event->button.y; + pointer_offset = _position - _desktop->w2d(_drag_event_origin); + _drag_initiated = false; + // route all events to this handler + _canvas_item_ctrl->grab(_grab_event_mask, nullptr); // cursor is null + _event_grab = true; + _setState(STATE_CLICKED); + return true; + } + return _event_grab; + + case GDK_2BUTTON_PRESS: + // store the button number for next release + next_release_doubleclick = event->button.button; + return true; + + case GDK_MOTION_NOTIFY: + if (_event_grab && ! event_context->is_space_panning()) { + _desktop->snapindicator->remove_snaptarget(); + bool transferred = false; + if (!_drag_initiated) { + bool t = fabs(event->motion.x - _drag_event_origin[Geom::X]) <= drag_tolerance && + fabs(event->motion.y - _drag_event_origin[Geom::Y]) <= drag_tolerance; + if (t){ + return true; + } + + // if we are here, it means the tolerance was just exceeded. + _drag_origin = _position; + transferred = grabbed(&event->motion); + // _drag_initiated might change during the above virtual call + _drag_initiated = true; + } + + if (!transferred) { + // dragging in progress + Geom::Point new_pos = _desktop->w2d(event_point(event->motion)) + pointer_offset; + // the new position is passed by reference and can be changed in the handlers. + dragged(new_pos, &event->motion); + move(new_pos); + _updateDragTip(&event->motion); // update dragging tip after moving to new position + + _desktop->scroll_to_point(new_pos); + _desktop->set_coordinate_status(_position); + sp_event_context_snap_delay_handler(event_context, nullptr, + (gpointer) this, &event->motion, + Inkscape::UI::Tools::DelayedSnapEvent::CONTROL_POINT_HANDLER); + } + return true; + } + break; + + case GDK_BUTTON_RELEASE: + if (_event_grab && event->button.button == 1) { + // If we have any pending snap event, then invoke it now! + // (This is needed because we might not have snapped on the latest GDK_MOTION_NOTIFY event + // if the mouse speed was too high. This is inherent to the snap-delay mechanism. + // We must snap at some point in time though, and this is our last chance) + // PS: For other contexts this is handled already in start_item_handler or start_root_handler + // if (_desktop && _desktop->event_context && _desktop->event_context->_delayed_snap_event) { + if (event_context->_delayed_snap_event) { + sp_event_context_snap_watchdog_callback(event_context->_delayed_snap_event); + } + + _canvas_item_ctrl->ungrab(); + _setMouseover(this, event->button.state); + _event_grab = false; + + if (_drag_initiated) { + // it is the end of a drag + _drag_initiated = false; + ungrabbed(&event->button); + return true; + } else { + // it is the end of a click + if (next_release_doubleclick) { + _double_clicked = true; + return doubleclicked(&event->button); + } else { + return clicked(&event->button); + } + } + } + break; + + case GDK_ENTER_NOTIFY: + _setMouseover(this, event->crossing.state); + return true; + case GDK_LEAVE_NOTIFY: + _clearMouseover(); + return true; + + case GDK_GRAB_BROKEN: + if (_event_grab && !event->grab_broken.keyboard) { + { + ungrabbed(nullptr); + } + _setState(STATE_NORMAL); + _event_grab = false; + _drag_initiated = false; + return true; + } + break; + + // update tips on modifier state change + // TODO add ESC keybinding as drag cancel + case GDK_KEY_PRESS: + switch (Inkscape::UI::Tools::get_latin_keyval(&event->key)) + { + case GDK_KEY_Escape: { + // ignore Escape if this is not a drag + if (!_drag_initiated) break; + + // temporarily disable snapping - we might snap to a different place than we were initially + event_context->discard_delayed_snap_event(); + SnapPreferences &snapprefs = _desktop->namedview->snap_manager.snapprefs; + bool snap_save = snapprefs.getSnapEnabledGlobally(); + snapprefs.setSnapEnabledGlobally(false); + + Geom::Point new_pos = _drag_origin; + + // make a fake event for dragging + // ASSUMPTION: dragging a point without modifiers will never prevent us from moving it + // to its original position + GdkEventMotion fake; + fake.type = GDK_MOTION_NOTIFY; + fake.window = event->key.window; + fake.send_event = event->key.send_event; + fake.time = event->key.time; + fake.x = _drag_event_origin[Geom::X]; // these two are normally not used in handlers + fake.y = _drag_event_origin[Geom::Y]; // (and shouldn't be) + fake.axes = nullptr; + fake.state = 0; // unconstrained drag + fake.is_hint = FALSE; + fake.device = nullptr; + fake.x_root = -1; // not used in handlers (and shouldn't be) + fake.y_root = -1; // can be used as a flag to check for cancelled drag + + dragged(new_pos, &fake); + + _canvas_item_ctrl->ungrab(); + _clearMouseover(); // this will also reset state to normal + _event_grab = false; + _drag_initiated = false; + + ungrabbed(nullptr); // ungrabbed handlers can handle a NULL event + snapprefs.setSnapEnabledGlobally(snap_save); + } + return true; + case GDK_KEY_Tab: + {// Downcast from ControlPoint to TransformHandle, if possible + // This is an ugly hack; we should have the transform handle intercept the keystrokes itself + TransformHandle *th = dynamic_cast(this); + if (th) { + th->getNextClosestPoint(false); + return true; + } + break; + } + case GDK_KEY_ISO_Left_Tab: + {// Downcast from ControlPoint to TransformHandle, if possible + // This is an ugly hack; we should have the transform handle intercept the keystrokes itself + TransformHandle *th = dynamic_cast(this); + if (th) { + th->getNextClosestPoint(true); + return true; + } + break; + } + default: + break; + } + // Do not break here, to allow for updating tooltips and such + case GDK_KEY_RELEASE: + if (mouseovered_point != this){ + return false; + } + if (_drag_initiated) { + return true; // this prevents the tool from overwriting the drag tip + } else { + unsigned state = state_after_event(event); + if (state != event->key.state) { + // we need to return true if there was a tip available, otherwise the tool's + // handler will process this event and set the tool's message, overwriting + // the point's message + return _updateTip(state); + } + } + break; + + default: break; + } + + // do not propagate events during grab - it might cause problems + return _event_grab; +} + +void ControlPoint::_setMouseover(ControlPoint *p, unsigned state) +{ + bool visible = p->visible(); + if (visible) { // invisible points shouldn't get mouseovered + p->_setState(STATE_MOUSEOVER); + } + p->_updateTip(state); + + if (visible && mouseovered_point != p) { + mouseovered_point = p; + signal_mouseover_change.emit(mouseovered_point); + } +} + +bool ControlPoint::_updateTip(unsigned state) +{ + Glib::ustring tip = _getTip(state); + if (!tip.empty()) { + _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, + tip.data()); + return true; + } else { + _desktop->event_context->defaultMessageContext()->clear(); + return false; + } +} + +bool ControlPoint::_updateDragTip(GdkEventMotion *event) +{ + if (!_hasDragTips()) { + return false; + } + Glib::ustring tip = _getDragTip(event); + if (!tip.empty()) { + _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, + tip.data()); + return true; + } else { + _desktop->event_context->defaultMessageContext()->clear(); + return false; + } +} + +void ControlPoint::_clearMouseover() +{ + if (mouseovered_point) { + mouseovered_point->_desktop->event_context->defaultMessageContext()->clear(); + mouseovered_point->_setState(STATE_NORMAL); + mouseovered_point = nullptr; + signal_mouseover_change.emit(mouseovered_point); + } +} + +void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event) +{ + if (!_event_grab) return; + + grabbed(event); + prev_point->_canvas_item_ctrl->ungrab(); + _canvas_item_ctrl->grab(_grab_event_mask, nullptr); // cursor is null + + _drag_initiated = true; + + prev_point->_setState(STATE_NORMAL); + _setMouseover(this, event->state); +} + +void ControlPoint::_setState(State state) +{ + ColorEntry current = {0, 0}; + ColorSet const &activeCset = (_isLurking()) ? invisible_cset : _cset; + switch(state) { + case STATE_NORMAL: + current = activeCset.normal; + break; + case STATE_MOUSEOVER: + current = activeCset.mouseover; + break; + case STATE_CLICKED: + current = activeCset.clicked; + break; + }; + _setColors(current); + _state = state; +} + +// TODO: RENAME +void ControlPoint::_handleControlStyling() +{ + _canvas_item_ctrl->set_size_default(); +} + +void ControlPoint::_setColors(ColorEntry colors) +{ + _canvas_item_ctrl->set_fill(colors.fill); + _canvas_item_ctrl->set_stroke(colors.stroke); +} + +bool ControlPoint::_isLurking() +{ + return _lurking; +} + +void ControlPoint::_setLurking(bool lurking) +{ + if (lurking != _lurking) { + _lurking = lurking; + _setState(_state); // TODO refactor out common part + } +} + + +bool ControlPoint::_is_drag_cancelled(GdkEventMotion *event) +{ + return !event || event->x_root == -1; +} + +// dummy implementations for handlers + +bool ControlPoint::grabbed(GdkEventMotion * /*event*/) +{ + return false; +} + +void ControlPoint::dragged(Geom::Point &/*new_pos*/, GdkEventMotion * /*event*/) +{ +} + +void ControlPoint::ungrabbed(GdkEventButton * /*event*/) +{ +} + +bool ControlPoint::clicked(GdkEventButton * /*event*/) +{ + return false; +} + +bool ControlPoint::doubleclicked(GdkEventButton * /*event*/) +{ + return false; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/control-point.h b/src/ui/tool/control-point.h new file mode 100644 index 0000000..7cc3977 --- /dev/null +++ b/src/ui/tool/control-point.h @@ -0,0 +1,414 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof Kosiński + * Jon A. Cruz + * + * Copyright (C) 2012 Authors + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_CONTROL_POINT_H +#define SEEN_UI_TOOL_CONTROL_POINT_H + +#include +#include +#include +#include +#include +#include <2geom/point.h> + +// #include "ui/control-types.h" +#include "display/control/canvas-item-ctrl.h" +#include "display/control/canvas-item-enums.h" + +#include "enums.h" // TEMP TEMP + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ToolBase; + +} +} +} + +namespace Inkscape { +namespace UI { + +/** + * Draggable point, the workhorse of on-canvas editing. + * + * Control points (formerly known as knots) are graphical representations of some significant + * point in the drawing. The drawing can be changed by dragging the point and the things that are + * attached to it with the mouse. Example things that could be edited with draggable points + * are gradient stops, the place where text is attached to a path, text kerns, nodes and handles + * in a path, and many more. + * + * @par Control point event handlers + * @par + * The control point has several virtual methods which allow you to react to things that + * happen to it. The most important ones are the grabbed, dragged, ungrabbed and moved functions. + * When a drag happens, the order of calls is as follows: + * - grabbed() + * - dragged() + * - dragged() + * - dragged() + * - ... + * - dragged() + * - ungrabbed() + * + * The control point can also respond to clicks and double clicks. On a double click, + * clicked() is called, followed by doubleclicked(). When deriving from SelectableControlPoint, + * you need to manually call the superclass version at the appropriate point in your handler. + * + * @par Which method to override? + * @par + * You might wonder which hook to use when you want to do things when the point is relocated. + * Here are some tips: + * - If the point is used to edit an object, override the move() method. + * - If the point can usually be dragged wherever you like but can optionally be constrained + * to axes or the like, add a handler for signal_dragged that modifies its new + * position argument. + * - If the point has additional canvas items tied to it (like handle lines), override + * the setPosition() method. + */ +class ControlPoint : boost::noncopyable, public sigc::trackable { +public: + + /** + * Enumeration representing the possible states of the control point, used to determine + * its appearance. + * + * @todo resolve this to be in sync with the five standard GTK states. + */ + enum State { + /** Normal state. */ + STATE_NORMAL, + + /** Mouse is hovering over the control point. */ + STATE_MOUSEOVER, + + /** First mouse button pressed over the control point. */ + STATE_CLICKED + }; + + /** + * Destructor + */ + virtual ~ControlPoint(); + + /// @name Adjust the position of the control point + /// @{ + /** Current position of the control point. */ + Geom::Point const &position() const { return _position; } + + operator Geom::Point const &() { return _position; } + + /** + * Move the control point to new position with side effects. + * This is called after each drag. Override this method if only some positions make sense + * for a control point (like a point that must always be on a path and can't modify it), + * or when moving a control point changes the positions of other points. + */ + virtual void move(Geom::Point const &pos); + + /** + * Relocate the control point without side effects. + * Overload this method only if there is an additional graphical representation + * that must be updated (like the lines that connect handles to nodes). If you override it, + * you must also call the superclass implementation of the method. + * @todo Investigate whether this method should be protected + */ + virtual void setPosition(Geom::Point const &pos); + + /** + * Apply an arbitrary affine transformation to a control point. This is used + * by ControlPointSelection, and is important for things like nodes with handles. + * The default implementation simply moves the point according to the transform. + */ + virtual void transform(Geom::Affine const &m); + + /** + * Apply any node repairs, by default no fixing is applied but Nodes will update + * smooth nodes to make sure nodes are kept consistent. + */ + virtual void fixNeighbors() {}; + + /// @} + + /// @name Toggle the point's visibility + /// @{ + bool visible() const; + + /** + * Set the visibility of the control point. An invisible point is not drawn on the canvas + * and cannot receive any events. If you want to have an invisible point that can respond + * to events, use invisible_cset as its color set. + */ + virtual void setVisible(bool v); + /// @} + + /// @name Transfer grab from another event handler + /// @{ + /** + * Transfer the grab to another point. This method allows one to create a draggable point + * that should be dragged instead of the one that received the grabbed signal. + * This is used to implement dragging out handles in the new node tool, for example. + * + * This method will NOT emit the ungrab signal of @c prev_point, because this would complicate + * using it with selectable control points. If you use this method while dragging, you must emit + * the ungrab signal yourself. + * + * Note that this will break horribly if you try to transfer grab between points in different + * desktops, which doesn't make much sense anyway. + */ + void transferGrab(ControlPoint *from, GdkEventMotion *event); + /// @} + + /// @name Inspect the state of the control point + /// @{ + State state() const { return _state; } + + bool mouseovered() const { return this == mouseovered_point; } + /// @} + + /** Holds the currently mouseovered control point. */ + static ControlPoint *mouseovered_point; + + /** + * Emitted when the mouseovered point changes. The parameter is the new mouseovered point. + * When a point ceases to be mouseovered, the parameter will be NULL. + */ + static sigc::signal signal_mouseover_change; + + static Glib::ustring format_tip(char const *format, ...) G_GNUC_PRINTF(1,2); + + // temporarily public, until snap delay is refactored a little + virtual bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event); + SPDesktop *const _desktop; ///< The desktop this control point resides on. + + bool doubleClicked() {return _double_clicked;} + +protected: + + struct ColorEntry { + guint32 fill; + guint32 stroke; + }; + + /** + * Color entries for each possible state. + * @todo resolve this to be in sync with the five standard GTK states. + */ + struct ColorSet { + ColorEntry normal; + ColorEntry mouseover; + ColorEntry clicked; + ColorEntry selected_normal; + ColorEntry selected_mouseover; + ColorEntry selected_clicked; + }; + + /** + * A color set which you can use to create an invisible control that can still receive events. + */ + static ColorSet invisible_cset; + + /** + * Create a regular control point. + * Derive to have constructors with a reasonable number of parameters. + * + * @param d Desktop for this control + * @param initial_pos Initial position of the control point in desktop coordinates + * @param anchor Where is the control point rendered relative to its desktop coordinates + * @param type Logical type of the control point. + * @param cset Colors of the point + * @param group The canvas group the point's canvas item should be created in + */ + ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Inkscape::CanvasItemCtrlType type, + ColorSet const &cset = _default_color_set, + Inkscape::CanvasItemGroup *group = nullptr); + + /** + * Create a control point with a pixbuf-based visual representation. + * + * @param d Desktop for this control + * @param initial_pos Initial position of the control point in desktop coordinates + * @param anchor Where is the control point rendered relative to its desktop coordinates + * @param pixbuf Pixbuf to be used as the visual representation + * @param cset Colors of the point + * @param group The canvas group the point's canvas item should be created in + */ + ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Glib::RefPtr pixbuf, + ColorSet const &cset = _default_color_set, + Inkscape::CanvasItemGroup *group = nullptr); + + /// @name Handle control point events in subclasses + /// @{ + /** + * Called when the user moves the point beyond the drag tolerance with the first button held + * down. + * + * @param event Motion event when drag tolerance was exceeded. + * @return true if you called transferGrab() during this method. + */ + virtual bool grabbed(GdkEventMotion *event); + + /** + * Called while dragging, but before moving the knot to new position. + * + * @param pos Old position, always equal to position() + * @param new_pos New position (after drag). This is passed as a non-const reference, + * so you can change it from the handler - that's how constrained dragging is implemented. + * @param event Motion event. + */ + virtual void dragged(Geom::Point &new_pos, GdkEventMotion *event); + + /** + * Called when the control point finishes a drag. + * + * @param event Button release event + */ + virtual void ungrabbed(GdkEventButton *event); + + /** + * Called when the control point is clicked, at mouse button release. + * Improperly implementing this method can cause the default context menu not to appear when a control + * point is right-clicked. + * + * @param event Button release event + * @return true if the click had some effect, false if it did nothing. + */ + virtual bool clicked(GdkEventButton *event); + + /** + * Called when the control point is doubleclicked, at mouse button release. + * + * @param event Button release event + */ + virtual bool doubleclicked(GdkEventButton *event); + /// @} + + /// @name Manipulate the control point's appearance in subclasses + /// @{ + + /** + * Change the state of the knot. + * Alters the appearance of the knot to match one of the states: normal, mouseover + * or clicked. + */ + virtual void _setState(State state); + + void _handleControlStyling(); + + void _setColors(ColorEntry c); + + void _setSize(unsigned int size); + + void _setControlType(Inkscape::CanvasItemCtrlType type); + + void _setAnchor(SPAnchorType anchor); + + void _setPixbuf(Glib::RefPtr); + + /** + * Determines if the control point is not visible yet still reacting to events. + * + * @return true if non-visible, false otherwise. + */ + bool _isLurking(); + + /** + * Sets the control point to be non-visible yet still reacting to events. + * + * @param lurking true to make non-visible, false otherwise. + */ + void _setLurking(bool lurking); + + /// @} + + virtual Glib::ustring _getTip(unsigned /*state*/) const { return ""; } + + virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) const { return ""; } + + virtual bool _hasDragTips() const { return false; } + + + Inkscape::CanvasItemCtrl * _canvas_item_ctrl = nullptr; ///< Visual representation of the control point. + + ColorSet const &_cset; ///< Colors used to represent the point + + State _state = STATE_NORMAL; + + static Geom::Point const &_last_click_event_point() { return _drag_event_origin; } + + static Geom::Point const &_last_drag_origin() { return _drag_origin; } + + static bool _is_drag_cancelled(GdkEventMotion *event); + + /** Events which should be captured when a handle is being dragged. */ + static Gdk::EventMask const _grab_event_mask; + + static bool _drag_initiated; + +private: + + ControlPoint(ControlPoint const &other); + + void operator=(ControlPoint const &other); + + static bool _event_handler(GdkEvent *event, ControlPoint *point); + + static void _setMouseover(ControlPoint *, unsigned state); + + static void _clearMouseover(); + + bool _updateTip(unsigned state); + + bool _updateDragTip(GdkEventMotion *event); + + void _setDefaultColors(); + + void _commonInit(); + + Geom::Point _position; ///< Current position in desktop coordinates + + sigc::connection _event_handler_connection; + + bool _lurking = false; + + static ColorSet _default_color_set; + + /** Stores the window point over which the cursor was during the last mouse button press. */ + static Geom::Point _drag_event_origin; + + /** Stores the desktop point from which the last drag was initiated. */ + static Geom::Point _drag_origin; + + static bool _event_grab; + + bool _double_clicked = false; +}; + + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + 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/tool/curve-drag-point.cpp b/src/ui/tool/curve-drag-point.cpp new file mode 100644 index 0000000..acf1299 --- /dev/null +++ b/src/ui/tool/curve-drag-point.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof Kosiński + * Jon A. Cruz + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tool/curve-drag-point.h" +#include +#include "desktop.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" + +#include "object/sp-namedview.h" +#include "object/sp-path.h" + +namespace Inkscape { +namespace UI { + + +bool CurveDragPoint::_drags_stroke = false; +bool CurveDragPoint::_segment_was_degenerate = false; + +CurveDragPoint::CurveDragPoint(PathManipulator &pm) : + ControlPoint(pm._multi_path_manipulator._path_data.node_data.desktop, Geom::Point(), SP_ANCHOR_CENTER, + Inkscape::CANVAS_ITEM_CTRL_TYPE_INVISIPOINT, + invisible_cset, pm._multi_path_manipulator._path_data.dragpoint_group), + _pm(pm) +{ + _canvas_item_ctrl->set_name("CanvasItemCtrl:CurveDragPoint"); + setVisible(false); +} + +bool CurveDragPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + // do not process any events when the manipulator is empty + if (_pm.empty()) { + setVisible(false); + return false; + } + return ControlPoint::_eventHandler(event_context, event); +} + +bool CurveDragPoint::grabbed(GdkEventMotion */*event*/) +{ + _pm._selection.hideTransformHandles(); + NodeList::iterator second = first.next(); + + // move the handles to 1/3 the length of the segment for line segments + if (first->front()->isDegenerate() && second->back()->isDegenerate()) { + _segment_was_degenerate = true; + + // delta is a vector equal 1/3 of distance from first to second + Geom::Point delta = (second->position() - first->position()) / 3.0; + // only update the nodes if the mode is bspline + if(!_pm._isBSpline()){ + first->front()->move(first->front()->position() + delta); + second->back()->move(second->back()->position() - delta); + } + _pm.update(); + } else { + _segment_was_degenerate = false; + } + return false; +} + +void CurveDragPoint::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + if (!first || !first.next()) return; + NodeList::iterator second = first.next(); + + // special cancel handling - retract handles when if the segment was degenerate + if (_is_drag_cancelled(event) && _segment_was_degenerate) { + first->front()->retract(); + second->back()->retract(); + _pm.update(); + return; + } + + if (_drag_initiated && !(event->state & GDK_SHIFT_MASK)) { + SnapManager &m = _desktop->namedview->snap_manager; + SPItem *path = static_cast(_pm._path); + m.setup(_desktop, true, path); // We will not try to snap to "path" itself + Inkscape::SnapCandidatePoint scp(new_pos, Inkscape::SNAPSOURCE_OTHER_HANDLE); + Inkscape::SnappedPoint sp = m.freeSnap(scp, Geom::OptRect(), false); + new_pos = sp.getPoint(); + m.unSetup(); + } + + // Magic Bezier Drag Equations follow! + // "weight" describes how the influence of the drag should be distributed + // among the handles; 0 = front handle only, 1 = back handle only. + double weight, t = _t; + if (t <= 1.0 / 6.0) weight = 0; + else if (t <= 0.5) weight = (pow((6 * t - 1) / 2.0, 3)) / 2; + else if (t <= 5.0 / 6.0) weight = (1 - pow((6 * (1-t) - 1) / 2.0, 3)) / 2 + 0.5; + else weight = 1; + + Geom::Point delta = new_pos - position(); + Geom::Point offset0 = ((1-weight)/(3*t*(1-t)*(1-t))) * delta; + Geom::Point offset1 = (weight/(3*t*t*(1-t))) * delta; + + //modified so that, if the trace is bspline, it only acts if the SHIFT key is pressed + if(!_pm._isBSpline()){ + first->front()->move(first->front()->position() + offset0); + second->back()->move(second->back()->position() + offset1); + }else if(weight>=0.8){ + if(held_shift(*event)){ + second->back()->move(new_pos); + } else { + second->move(second->position() + delta); + } + }else if(weight<=0.2){ + if(held_shift(*event)){ + first->back()->move(new_pos); + } else { + first->move(first->position() + delta); + } + }else{ + first->move(first->position() + delta); + second->move(second->position() + delta); + } + _pm.update(); +} + +void CurveDragPoint::ungrabbed(GdkEventButton *) +{ + _pm._updateDragPoint(_desktop->d2w(position())); + _pm._commit(_("Drag curve")); + _pm._selection.restoreTransformHandles(); +} + +bool CurveDragPoint::clicked(GdkEventButton *event) +{ + // This check is probably redundant + if (!first || event->button != 1) return false; + // the next iterator can be invalid if we click very near the end of path + NodeList::iterator second = first.next(); + if (!second) return false; + + // insert nodes on Ctrl+Alt+click + if (held_control(*event) && held_alt(*event)) { + _insertNode(false); + return true; + } + + if (held_shift(*event)) { + // if both nodes of the segment are selected, deselect; + // otherwise add to selection + if (first->selected() && second->selected()) { + _pm._selection.erase(first.ptr()); + _pm._selection.erase(second.ptr()); + } else { + _pm._selection.insert(first.ptr()); + _pm._selection.insert(second.ptr()); + } + } else { + // without Shift, take selection + _pm._selection.clear(); + _pm._selection.insert(first.ptr()); + _pm._selection.insert(second.ptr()); + if (held_control(*event)) { + _pm.setSegmentType(Inkscape::UI::SEGMENT_STRAIGHT); + _pm.update(true); + _pm._commit(_("Straighten segments")); + } + } + return true; +} + +bool CurveDragPoint::doubleclicked(GdkEventButton *event) +{ + if (event->button != 1 || !first || !first.next()) return false; + if (held_control(*event)) { + _pm.deleteSegments(); + _pm.update(true); + _pm._commit(_("Remove segment")); + } else { + _insertNode(true); + } + return true; +} + +void CurveDragPoint::_insertNode(bool take_selection) +{ + // The purpose of this call is to make way for the just created node. + // Otherwise clicks on the new node would only work after the user moves the mouse a bit. + // PathManipulator will restore visibility when necessary. + setVisible(false); + + _pm.insertNode(first, _t, take_selection); +} + +Glib::ustring CurveDragPoint::_getTip(unsigned state) const +{ + if (_pm.empty()) return ""; + if (!first || !first.next()) return ""; + bool linear = first->front()->isDegenerate() && first.next()->back()->isDegenerate(); + if(state_held_shift(state) && _pm._isBSpline()){ + return C_("Path segment tip", + "Shift: drag to open or move BSpline handles"); + } + if (state_held_shift(state)) { + return C_("Path segment tip", + "Shift: click to toggle segment selection"); + } + if (state_held_control(state) && state_held_alt(state)) { + return C_("Path segment tip", + "Ctrl+Alt: click to insert a node"); + } + if (state_held_control(state)) { + return C_("Path segment tip", + "Ctrl: click to change line type"); + } + if(_pm._isBSpline()){ + return C_("Path segment tip", + "BSpline segment: drag to shape the segment, doubleclick to insert node, " + "click to select (more: Shift, Ctrl+Alt)"); + } + if (linear) { + return C_("Path segment tip", + "Linear segment: drag to convert to a Bezier segment, " + "doubleclick to insert node, click to select (more: Shift, Ctrl+Alt)"); + } else { + return C_("Path segment tip", + "Bezier segment: drag to shape the segment, doubleclick to insert node, " + "click to select (more: Shift, Ctrl+Alt)"); + } +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/curve-drag-point.h b/src/ui/tool/curve-drag-point.h new file mode 100644 index 0000000..bfe0ad7 --- /dev/null +++ b/src/ui/tool/curve-drag-point.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof Kosiński + * Jon A. Cruz + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_CURVE_DRAG_POINT_H +#define SEEN_UI_TOOL_CURVE_DRAG_POINT_H + +#include "ui/tool/control-point.h" +#include "ui/tool/node.h" + +class SPDesktop; +namespace Inkscape { +namespace UI { + +class PathManipulator; +struct PathSharedData; + +// This point should be invisible to the user - use the invisible_cset from control-point.h +// TODO make some methods from path-manipulator.cpp public so that this point doesn't have +// to be declared as a friend +/** + * An invisible point used to drag curves. This point is used by PathManipulator to allow editing + * of path segments by dragging them. It is defined in a separate file so that the node tool + * can check if the mouseovered control point is a curve drag point and update the cursor + * accordingly, without the need to drag in the full PathManipulator header. + */ +class CurveDragPoint : public ControlPoint { +public: + + CurveDragPoint(PathManipulator &pm); + void setSize(double sz) { _setSize(sz); } + void setTimeValue(double t) { _t = t; } + double getTimeValue() { return _t; } + void setIterator(NodeList::iterator i) { first = i; } + NodeList::iterator getIterator() { return first; } + bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override; + +protected: + + Glib::ustring _getTip(unsigned state) const override; + void dragged(Geom::Point &, GdkEventMotion *) override; + bool grabbed(GdkEventMotion *) override; + void ungrabbed(GdkEventButton *) override; + bool clicked(GdkEventButton *) override; + bool doubleclicked(GdkEventButton *) override; + +private: + double _t; + PathManipulator &_pm; + NodeList::iterator first; + + static bool _drags_stroke; + static bool _segment_was_degenerate; + static Geom::Point _stroke_drag_origin; + void _insertNode(bool take_selection); +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + 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/tool/event-utils.cpp b/src/ui/tool/event-utils.cpp new file mode 100644 index 0000000..f131d4f --- /dev/null +++ b/src/ui/tool/event-utils.cpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Collection of shorthands to deal with GDK events. + */ +/* Authors: + * Krzysztof Kosiński + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include "ui/tool/event-utils.h" + +namespace Inkscape { +namespace UI { + + +guint shortcut_key(GdkEventKey const &event) +{ + guint shortcut_key = 0; + gdk_keymap_translate_keyboard_state( + Gdk::Display::get_default()->get_keymap(), + event.hardware_keycode, + (GdkModifierType) event.state, + 0 /*event->key.group*/, + &shortcut_key, nullptr, nullptr, nullptr); + return shortcut_key; +} + +/** Returns the modifier state valid after this event. Use this when you process events + * that change the modifier state. Currently handles only Shift, Ctrl, Alt. */ +unsigned state_after_event(GdkEvent *event) +{ + unsigned state = 0; + switch (event->type) { + case GDK_KEY_PRESS: + state = event->key.state; + switch(shortcut_key(event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + state |= GDK_SHIFT_MASK; + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + state |= GDK_CONTROL_MASK; + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + state |= GDK_MOD1_MASK; + break; + default: break; + } + break; + case GDK_KEY_RELEASE: + state = event->key.state; + switch(shortcut_key(event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + state &= ~GDK_SHIFT_MASK; + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + state &= ~GDK_CONTROL_MASK; + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + state &= ~GDK_MOD1_MASK; + break; + default: break; + } + break; + default: break; + } + return state; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/event-utils.h b/src/ui/tool/event-utils.h new file mode 100644 index 0000000..37961e3 --- /dev/null +++ b/src/ui/tool/event-utils.h @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Collection of shorthands to deal with GDK events. + */ +/* Authors: + * Krzysztof Kosiński + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_EVENT_UTILS_H +#define SEEN_UI_TOOL_EVENT_UTILS_H + +#include +#include <2geom/point.h> + +namespace Inkscape { +namespace UI { + +inline bool state_held_shift(unsigned state) { + return state & GDK_SHIFT_MASK; +} +inline bool state_held_control(unsigned state) { + return state & GDK_CONTROL_MASK; +} +inline bool state_held_alt(unsigned state) { + return state & GDK_MOD1_MASK; +} +inline bool state_held_only_shift(unsigned state) { + return (state & GDK_SHIFT_MASK) && !(state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)); +} +inline bool state_held_only_control(unsigned state) { + return (state & GDK_CONTROL_MASK) && !(state & (GDK_SHIFT_MASK | GDK_MOD1_MASK)); +} +inline bool state_held_only_alt(unsigned state) { + return (state & GDK_MOD1_MASK) && !(state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)); +} +inline bool state_held_any_modifiers(unsigned state) { + return state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK); +} +inline bool state_held_no_modifiers(unsigned state) { + return !state_held_any_modifiers(state); +} +template +inline bool state_held_button(unsigned state) { + return (button == 0 || button > 5) ? false : state & (GDK_BUTTON1_MASK << (button-1)); +} + + +/** Checks whether Shift was held when the event was generated. */ +template +inline bool held_shift(E const &event) { + return state_held_shift(event.state); +} + +/** Checks whether Control was held when the event was generated. */ +template +inline bool held_control(E const &event) { + return state_held_control(event.state); +} + +/** Checks whether Alt was held when the event was generated. */ +template +inline bool held_alt(E const &event) { + return state_held_alt(event.state); +} + +/** True if from the set of Ctrl, Shift and Alt only Ctrl was held when the event + * was generated. */ +template +inline bool held_only_control(E const &event) { + return state_held_only_control(event.state); +} + +/** True if from the set of Ctrl, Shift and Alt only Shift was held when the event + * was generated. */ +template +inline bool held_only_shift(E const &event) { + return state_held_only_shift(event.state); +} + +/** True if from the set of Ctrl, Shift and Alt only Alt was held when the event + * was generated. */ +template +inline bool held_only_alt(E const &event) { + return state_held_only_alt(event.state); +} + +template +inline bool held_no_modifiers(E const &event) { + return state_held_no_modifiers(event.state); +} + +template +inline bool held_any_modifiers(E const &event) { + return state_held_any_modifiers(event.state); +} + +template +inline Geom::Point event_point(E const &event) { + return Geom::Point(event.x, event.y); +} + +/** Use like this: + * @code if (held_button<2>(event->motion)) { ... @endcode */ +template +inline bool held_button(E const &event) { + return state_held_button